idlesign/django-sitegate

View on GitHub
sitegate/models.py

Summary

Maintainability
A
45 mins
Test Coverage
from datetime import timedelta
from typing import Optional
from uuid import uuid4

from django.conf import settings
from django.db import models, IntegrityError
from django.utils import timezone
from django.utils.translation import gettext_lazy as _
from etc.models import InheritedModel

if False:  # pragma: nocover
    from django.contrib.auth.models import User  # noqa


USER_MODEL = getattr(settings, 'AUTH_USER_MODEL', 'auth.User')


class BlacklistedDomain(models.Model):

    domain = models.CharField(_('Domain name'), max_length=253, unique=True)

    enabled = models.BooleanField(
        _('Enabled'),
        help_text=_('If enabled visitors won\'t be able to sign up with this domain name in e-mail.'),
        db_index=True, default=True)

    class Meta:
        verbose_name = _('Blacklisted domain')
        verbose_name_plural = _('Blacklisted domains')

    @classmethod
    def is_blacklisted(cls, email: str) -> bool:
        """Checks whether the given e-mail is blacklisted.

        :param email:

        """
        domain = email.split('@', 1)[1].lower()

        # 'some.denied.co.uk' -> ['some.denied.co.uk', 'denied.co.uk', 'co.uk']
        sub_domains = [domain]
        for i in range(domain.count('.') - 1):
            sub_domains.append(sub_domains[i].split('.', 1)[-1])

        return cls.objects.filter(enabled=True, domain__in=sub_domains).exists()

    def __str__(self):
        return self.domain


class ModelWithCode(models.Model):

    code = models.CharField('dummy', max_length=128, unique=True, editable=False)
    time_created = models.DateTimeField(_('Date created'), auto_now_add=True)
    time_accepted = models.DateTimeField(_('Date accepted'), null=True, editable=False)
    expired = models.BooleanField(_('Expired'), help_text='dummy', db_index=True, default=False)

    class Meta:
        abstract = True

    def __str__(self):
        return self.code

    def save(self, force_insert=False, force_update=False, **kwargs):

        if self.code == '':
            while True:
                self.code = self.generate_code()
                try:
                    super(ModelWithCode, self).save(force_insert, force_update, **kwargs)

                except IntegrityError:
                    pass

                else:
                    break
        else:
            super(ModelWithCode, self).save(force_insert, force_update, **kwargs)

    @classmethod
    def is_valid(cls, code: str) -> bool:

        try:
            return cls.objects.get(code=code, expired=False)

        except (cls.MultipleObjectsReturned, cls.DoesNotExist):
            return False

    @staticmethod
    def generate_code() -> str:
        return str(uuid4()).replace('-', '')


class InvitationCode(InheritedModel, ModelWithCode):

    creator = models.ForeignKey(
        USER_MODEL, related_name='creators', verbose_name=_('Creator'), on_delete=models.CASCADE)

    acceptor = models.ForeignKey(
        USER_MODEL, related_name='acceptors', verbose_name=_('Acceptor'), null=True, blank=True,
        editable=False, on_delete=models.CASCADE)

    class Meta:
        verbose_name = _('Invitation code')
        verbose_name_plural = _('Invitation codes')

    class Fields:
        code = _('Invitation code')
        expired = {'help_text': _('Visitors won\'t be able to sign up with an expired code.')}

    @classmethod
    def add(cls, creator: 'User') -> 'InvitationCode':
        new_code = cls(creator=creator)
        new_code.save(force_insert=True)
        return new_code

    @classmethod
    def accept(cls, code: str, acceptor: 'User'):
        return cls.objects.filter(code=code).update(acceptor=acceptor, expired=True, time_accepted=timezone.now())


class EmailConfirmation(InheritedModel, ModelWithCode):

    user = models.ForeignKey(USER_MODEL, verbose_name=_('User'), on_delete=models.CASCADE)

    class Meta:
        verbose_name = _('Activation code')
        verbose_name_plural = _('Activation codes')

    class Fields:
        code = _('Activation code')
        expired = {'help_text': _('Expired codes couldn\'t be used for repeated account activations.')}

    @classmethod
    def add(cls, user: 'User') -> 'EmailConfirmation':
        new_code = cls(user=user)
        new_code.save(force_insert=True)
        return new_code

    def activate(self):
        self.expired = True
        self.time_accepted = timezone.now()
        self.save()

        user = self.user
        user.is_active = True
        user.save()


class RemoteRecord(ModelWithCode):
    """Stores data used for sign ins using remote services."""

    remote = models.CharField(_('Remote alias'), max_length=20, db_index=True)

    remote_id = models.CharField(
        _('Remote ID'), null=True, default=None, max_length=64, db_index=True, db_column='rid')

    user = models.ForeignKey(
        USER_MODEL, null=True, blank=True,
        related_name='remotes', verbose_name=_('User'), on_delete=models.CASCADE)

    expired = None

    class Meta:
        verbose_name = _('Remote record')
        verbose_name_plural = _('Remotes records')

    def __str__(self):
        return f'{self.remote} {self.code}'

    @classmethod
    def add(cls, *, remote: str, user: Optional['User']) -> 'RemoteRecord':
        record = cls(
            remote=remote,
            user=user,
        )
        record.save(force_insert=True)
        return record

    @classmethod
    def cleanup(cls, *, ago: int = None):
        """Removes remote records not linked to certain user.
        Useful for periodic background cleaning.

        :param ago: Days. Allows cleanup for stale records created X days ago.
            Defaults to None (cleanup all stale).

        """
        filter_kwargs = {
            'remote_id__isnull': True,
            'user_id__isnull': True,
        }

        if ago:
            filter_kwargs['time_created__lte'] = timezone.now() - timedelta(days=int(ago))

        cls.objects.filter(**filter_kwargs).delete()