Просмотр исходного кода

Rehashed migrations to include extra models for bans and online tracking

Rafał Pitoń 11 лет назад
Родитель
Сommit
af6b960a89

+ 43 - 0
misago/core/pgutils.py

@@ -0,0 +1,43 @@
+from django.db.migrations.operations import RunSQL
+
+
+class CreatePartialIndex(RunSQL):
+    CREATE_SQL = """
+CREATE INDEX %(index_name)s ON %(table)s (%(field)s)
+WHERE %(condition)s;
+"""
+
+    REMOVE_SQL = """
+DROP INDEX %(index_name)s
+"""
+
+    def __init__(self, field, index_name, condition):
+        self.model, self.field = field.split('.')
+        self.index_name = index_name
+        self.condition = condition
+
+    @property
+    def reversible(self):
+        return True
+
+    def state_forwards(self, app_label, state):
+        pass
+
+    def database_forwards(self, app_label, schema_editor, from_state, to_state):
+        apps = from_state.render()
+        model = apps.get_model(app_label, self.model)
+
+        statement = self.CREATE_SQL % {
+            'index_name': self.index_name,
+            'table': model._meta.db_table,
+            'field': self.field,
+            'condition': self.condition,
+        }
+
+        schema_editor.execute(statement)
+
+    def database_backwards(self, app_label, schema_editor, from_state, to_state):
+        schema_editor.execute(self.REMOVE_SQL % {'index_name': self.index_name})
+
+    def describe(self):
+        return "Create PostgreSQL partial index on field %s in %s for %s" % (self.field, self.model_name, self.values)

+ 25 - 0
misago/users/management/commands/bansmaintenance.py

@@ -0,0 +1,25 @@
+from django.core.management.base import BaseCommand
+from django.utils import timezone
+from misago.users.models import BanCache
+
+
+class Command(BaseCommand):
+    help = 'Runs maintenance on Misago bans system.'
+
+    def handle(self, *args, **options):
+        self.handle_expired_bans()
+        self.handle_bans_caches()
+
+    def handle_expired_bans(self):
+        queryset = Ban.objects.filter(is_valid=True, valid_until__isnull=False)
+        queryset = queryset.filter(valid_until__lte=timezone.now().date())
+
+        expired_count = queryset.update(is_valid=False)
+        self.stdout.write('Bans invalidated: %s' expired_count)
+
+    def handle_bans_caches(self):
+        queryset = BanCache.objects.filter(valid_until__isnull=False)
+        queryset = queryset.filter(valid_until__lte=timezone.now().date())
+
+        expired_count = queryset.delete()
+        self.stdout.write('Ban caches emptied: %s' expired_count)

+ 50 - 2
misago/users/migrations/0001_initial.py

@@ -4,6 +4,8 @@ from __future__ import unicode_literals
 from django.db import models, migrations
 import django.utils.timezone
 import django.db.models.deletion
+from django.conf import settings
+from misago.core.pgutils import CreatePartialIndex
 
 
 class Migration(migrations.Migration):
@@ -20,14 +22,16 @@ class Migration(migrations.Migration):
                 ('id', models.AutoField(verbose_name='ID', serialize=False, auto_created=True, primary_key=True)),
                 ('password', models.CharField(max_length=128, verbose_name='password')),
                 ('last_login', models.DateTimeField(default=django.utils.timezone.now, verbose_name='last login')),
-                ('is_superuser', models.BooleanField(default=False, help_text='Designates that this user has all permissions without explicitly assigning them.', verbose_name='superuser status')),
                 ('username', models.CharField(max_length=30)),
                 ('username_slug', models.CharField(unique=True, max_length=30)),
                 ('email', models.EmailField(max_length=255, db_index=True)),
                 ('email_hash', models.CharField(unique=True, max_length=32)),
                 ('joined_on', models.DateTimeField(default=django.utils.timezone.now, verbose_name='joined on')),
+                ('last_active', models.DateTimeField(null=True, blank=True)),
                 ('title', models.CharField(max_length=255, null=True, blank=True)),
-                ('is_staff', models.BooleanField(default=False, help_text='Designates whether the user can log into admin sites.', db_index=True, verbose_name='staff status')),
+                ('activation_requirement', models.PositiveIntegerField(default=0)),
+                ('is_staff', models.BooleanField(default=False, help_text='Designates whether the user can log into admin sites.', verbose_name='staff status')),
+                ('is_superuser', models.BooleanField(default=False, help_text='Designates that this user has all permissions without explicitly assigning them.', verbose_name='superuser status')),
                 ('acl_key', models.CharField(max_length=12, null=True, blank=True)),
                 ('groups', models.ManyToManyField(to='auth.Group', verbose_name='groups', blank=True)),
                 ('roles', models.ManyToManyField(to='misago_acl.Role')),
@@ -38,6 +42,26 @@ class Migration(migrations.Migration):
             },
             bases=(models.Model,),
         ),
+        CreatePartialIndex(
+            field='User.is_staff',
+            index_name='misago_users_user_is_staff_partial',
+            condition='is_staff = TRUE',
+        ),
+        CreatePartialIndex(
+            field='User.activation_requirement',
+            index_name='misago_users_user_activation_requirement_partial',
+            condition='activation_requirement > 0',
+        ),
+        migrations.CreateModel(
+            name='Online',
+            fields=[
+                ('last_click', models.DateTimeField(default=django.utils.timezone.now)),
+                ('user', models.OneToOneField(primary_key=True, serialize=False, to=settings.AUTH_USER_MODEL)),
+            ],
+            options={
+            },
+            bases=(models.Model,),
+        ),
         migrations.CreateModel(
             name='Rank',
             fields=[
@@ -64,4 +88,28 @@ class Migration(migrations.Migration):
             field=models.ForeignKey(on_delete=django.db.models.deletion.PROTECT, to_field='id', blank=True, to='misago_users.Rank', null=True),
             preserve_default=True,
         ),
+        migrations.CreateModel(
+            name='Ban',
+            fields=[
+                ('id', models.AutoField(verbose_name='ID', serialize=False, auto_created=True, primary_key=True)),
+                ('test', models.PositiveIntegerField(default=0, db_index=True)),
+                ('banned_value', models.CharField(max_length=255, db_index=True)),
+                ('reason_user', models.TextField(null=True, blank=True)),
+                ('reason_admin', models.TextField(null=True, blank=True)),
+                ('valid_until', models.DateField(null=True, blank=True, db_index=True)),
+                ('is_valid', models.BooleanField(default=False, db_index=True)),
+            ],
+            bases=(models.Model,),
+        ),
+        migrations.CreateModel(
+            name='BanCache',
+            fields=[
+                ('bans_version', models.PositiveIntegerField(default=0)),
+                ('valid_until', models.DateField(null=True, blank=True)),
+                ('user', models.OneToOneField(primary_key=True, serialize=False, to=settings.AUTH_USER_MODEL)),
+            ],
+            options={
+            },
+            bases=(models.Model,),
+        ),
     ]

+ 132 - 4
misago/users/models/usermodel.py → misago/users/models.py

@@ -1,14 +1,14 @@
 from hashlib import md5
+import re
 from django.contrib.auth.models import (AbstractBaseUser, PermissionsMixin,
                                         UserManager as BaseUserManager,
                                         AnonymousUser as DjangoAnonymousUser)
 from django.db import models, transaction
 from django.utils import timezone
 from django.utils.translation import ugettext_lazy as _
-from misago.acl import get_user_acl
+from misago.acl import get_user_acl, version as acl_version
 from misago.acl.models import Role
 from misago.core.utils import slugify
-from misago.users.models import Rank
 from misago.users.utils import hash_email
 from misago.users.validators import (validate_email, validate_password,
                                      validate_username)
@@ -94,11 +94,13 @@ class User(AbstractBaseUser, PermissionsMixin):
     email = models.EmailField(max_length=255, db_index=True)
     email_hash = models.CharField(max_length=32, unique=True)
     joined_on = models.DateTimeField(_('joined on'), default=timezone.now)
+    last_active = models.DateTimeField(null=True, blank=True)
     rank = models.ForeignKey(
-        'misago_users.Rank', null=True, blank=True, on_delete=models.PROTECT)
+        'Rank', null=True, blank=True, on_delete=models.PROTECT)
     title = models.CharField(max_length=255, null=True, blank=True)
+    activation_requirement = models.PositiveIntegerField(default=0)
     is_staff = models.BooleanField(
-        _('staff status'), default=False, db_index=True,
+        _('staff status'), default=False,
         help_text=_('Designates whether the user can log into admin sites.'))
     roles = models.ManyToManyField('misago_acl.Role')
     acl_key = models.CharField(max_length=12, null=True, blank=True)
@@ -184,6 +186,11 @@ class User(AbstractBaseUser, PermissionsMixin):
         self.acl_key = md5(','.join(roles_pks)).hexdigest()[:12]
 
 
+class Online(models.Model):
+    user = models.OneToOneField(User, primary_key=True, related_name='online')
+    last_click = models.DateTimeField(default=timezone.now)
+
+
 class AnonymousUser(DjangoAnonymousUser):
     acl_key = 'anonymous'
 
@@ -207,3 +214,124 @@ class AnonymousUser(DjangoAnonymousUser):
 
     def update_acl_key(self):
         raise TypeError("Can't update ACL key on anonymous users")
+
+
+"""
+Ranks
+"""
+class RankManager(models.Manager):
+    def get_default(self):
+        return self.get(is_default=True)
+
+    def make_rank_default(self, rank):
+        with transaction.atomic():
+            self.filter(is_default=True).update(is_default=False)
+            rank.is_default = True
+            rank.save(update_fields=['is_default'])
+
+
+class Rank(models.Model):
+    name = models.CharField(max_length=255)
+    slug = models.CharField(max_length=255)
+    description = models.TextField(null=True, blank=True)
+    title = models.CharField(max_length=255, null=True, blank=True)
+    roles = models.ManyToManyField('misago_acl.Role', null=True, blank=True)
+    css_class = models.CharField(max_length=255, null=True, blank=True)
+    is_default = models.BooleanField(default=False)
+    is_tab = models.BooleanField(default=False)
+    is_on_index = models.BooleanField(default=False)
+    order = models.IntegerField(default=0)
+
+    objects = RankManager()
+
+    class Meta:
+        get_latest_by = 'order'
+
+    def __unicode__(self):
+        return self.name
+
+    def save(self, *args, **kwargs):
+        if not self.pk:
+            self.set_order()
+        else:
+            acl_version.invalidate()
+        return super(Rank, self).save(*args, **kwargs)
+
+    def delete(self, *args, **kwargs):
+        acl_version.invalidate()
+        return super(Rank, self).delete(*args, **kwargs)
+
+    def set_name(self, name):
+        self.name = name
+        self.slug = slugify(name)
+
+    def set_order(self):
+        try:
+            self.order = Rank.objects.latest('order').order + 1
+        except Rank.DoesNotExist:
+            self.order = 0
+
+
+"""
+Bans
+"""
+BAN_NAME = 0
+BAN_EMAIL = 1
+BAN_IP = 2
+
+
+class BansManager(models.Manager):
+    def is_ip_banned(self, ip):
+        return self.check_ban(ip=ip)
+
+    def is_username_banned(self, username):
+        return self.check_ban(username=username)
+
+    def is_email_banned(self, email):
+        return self.check_ban(email=email)
+
+    def check_ban(self, username=None, email=None, ip=None):
+        tests = []
+
+        if username:
+            tests.append(BAN_NAME)
+        if email:
+            tests.append(BAN_EMAIL)
+        if ip:
+            tests.append(BAN_IP)
+
+        queryset = self.filter(is_valid=False)
+        if len(tests) == 1:
+            queryset = queryset.filter(test=tests[0])
+        elif tests:
+            queryset = queryset.filter(test__in=tests)
+
+        for ban in queryset.order_by('-id').iterator():
+            if username and ban.test == BAN_NAME and ban.test_value(username):
+                return ban
+            elif email and ban.test == BAN_EMAIL and ban.test_value(email):
+                return ban
+            elif ip and ban.test == BAN_IP and ban.test_value(ip):
+                return ban
+        return False
+
+
+class Ban(models.Model):
+    test = models.PositiveIntegerField(default=BAN_NAME, db_index=True)
+    banned_value = models.CharField(max_length=255, db_index=True)
+    reason_user = models.TextField(null=True, blank=True)
+    reason_admin = models.TextField(null=True, blank=True)
+    valid_until = models.DateField(null=True, blank=True, db_index=True)
+    is_valid = models.BooleanField(default=False, db_index=True)
+
+    objects = BansManager()
+
+    def test_value(self, value):
+        regex = '^' + re.escape(self.banned_value).replace('\*', '(.*?)') + '$'
+        return re.search(regex, value, flags=re.IGNORECASE)
+
+
+class BanCache(models.Model):
+    user = models.OneToOneField(User, primary_key=True)
+    bans_version = models.PositiveIntegerField(default=0)
+    valid_until = models.DateField(null=True, blank=True)

+ 0 - 3
misago/users/models/__init__.py

@@ -1,3 +0,0 @@
-# flake8: noqa
-from misago.users.models.rankmodel import Rank
-from misago.users.models.usermodel import User, AnonymousUser

+ 0 - 56
misago/users/models/rankmodel.py

@@ -1,56 +0,0 @@
-from django.db import models, transaction
-from misago.acl import version as acl_version
-from misago.core.utils import slugify
-
-
-class RankManager(models.Manager):
-    def get_default(self):
-        return self.get(is_default=True)
-
-    def make_rank_default(self, rank):
-        with transaction.atomic():
-            self.filter(is_default=True).update(is_default=False)
-            rank.is_default = True
-            rank.save(update_fields=['is_default'])
-
-
-class Rank(models.Model):
-    name = models.CharField(max_length=255)
-    slug = models.CharField(max_length=255)
-    description = models.TextField(null=True, blank=True)
-    title = models.CharField(max_length=255, null=True, blank=True)
-    roles = models.ManyToManyField('misago_acl.Role', null=True, blank=True)
-    css_class = models.CharField(max_length=255, null=True, blank=True)
-    is_default = models.BooleanField(default=False)
-    is_tab = models.BooleanField(default=False)
-    is_on_index = models.BooleanField(default=False)
-    order = models.IntegerField(default=0)
-
-    objects = RankManager()
-
-    class Meta:
-        get_latest_by = 'order'
-
-    def __unicode__(self):
-        return self.name
-
-    def save(self, *args, **kwargs):
-        if not self.pk:
-            self.set_order()
-        else:
-            acl_version.invalidate()
-        return super(Rank, self).save(*args, **kwargs)
-
-    def delete(self, *args, **kwargs):
-        acl_version.invalidate()
-        return super(Rank, self).delete(*args, **kwargs)
-
-    def set_name(self, name):
-        self.name = name
-        self.slug = slugify(name)
-
-    def set_order(self):
-        try:
-            self.order = Rank.objects.latest('order').order + 1
-        except Rank.DoesNotExist:
-            self.order = 0