Browse Source

Rehashed migrations to include extra models for bans and online tracking

Rafał Pitoń 11 years ago
parent
commit
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
 from django.db import models, migrations
 import django.utils.timezone
 import django.utils.timezone
 import django.db.models.deletion
 import django.db.models.deletion
+from django.conf import settings
+from misago.core.pgutils import CreatePartialIndex
 
 
 
 
 class Migration(migrations.Migration):
 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)),
                 ('id', models.AutoField(verbose_name='ID', serialize=False, auto_created=True, primary_key=True)),
                 ('password', models.CharField(max_length=128, verbose_name='password')),
                 ('password', models.CharField(max_length=128, verbose_name='password')),
                 ('last_login', models.DateTimeField(default=django.utils.timezone.now, verbose_name='last login')),
                 ('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', models.CharField(max_length=30)),
                 ('username_slug', models.CharField(unique=True, max_length=30)),
                 ('username_slug', models.CharField(unique=True, max_length=30)),
                 ('email', models.EmailField(max_length=255, db_index=True)),
                 ('email', models.EmailField(max_length=255, db_index=True)),
                 ('email_hash', models.CharField(unique=True, max_length=32)),
                 ('email_hash', models.CharField(unique=True, max_length=32)),
                 ('joined_on', models.DateTimeField(default=django.utils.timezone.now, verbose_name='joined on')),
                 ('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)),
                 ('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)),
                 ('acl_key', models.CharField(max_length=12, null=True, blank=True)),
                 ('groups', models.ManyToManyField(to='auth.Group', verbose_name='groups', blank=True)),
                 ('groups', models.ManyToManyField(to='auth.Group', verbose_name='groups', blank=True)),
                 ('roles', models.ManyToManyField(to='misago_acl.Role')),
                 ('roles', models.ManyToManyField(to='misago_acl.Role')),
@@ -38,6 +42,26 @@ class Migration(migrations.Migration):
             },
             },
             bases=(models.Model,),
             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(
         migrations.CreateModel(
             name='Rank',
             name='Rank',
             fields=[
             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),
             field=models.ForeignKey(on_delete=django.db.models.deletion.PROTECT, to_field='id', blank=True, to='misago_users.Rank', null=True),
             preserve_default=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
 from hashlib import md5
+import re
 from django.contrib.auth.models import (AbstractBaseUser, PermissionsMixin,
 from django.contrib.auth.models import (AbstractBaseUser, PermissionsMixin,
                                         UserManager as BaseUserManager,
                                         UserManager as BaseUserManager,
                                         AnonymousUser as DjangoAnonymousUser)
                                         AnonymousUser as DjangoAnonymousUser)
 from django.db import models, transaction
 from django.db import models, transaction
 from django.utils import timezone
 from django.utils import timezone
 from django.utils.translation import ugettext_lazy as _
 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.acl.models import Role
 from misago.core.utils import slugify
 from misago.core.utils import slugify
-from misago.users.models import Rank
 from misago.users.utils import hash_email
 from misago.users.utils import hash_email
 from misago.users.validators import (validate_email, validate_password,
 from misago.users.validators import (validate_email, validate_password,
                                      validate_username)
                                      validate_username)
@@ -94,11 +94,13 @@ class User(AbstractBaseUser, PermissionsMixin):
     email = models.EmailField(max_length=255, db_index=True)
     email = models.EmailField(max_length=255, db_index=True)
     email_hash = models.CharField(max_length=32, unique=True)
     email_hash = models.CharField(max_length=32, unique=True)
     joined_on = models.DateTimeField(_('joined on'), default=timezone.now)
     joined_on = models.DateTimeField(_('joined on'), default=timezone.now)
+    last_active = models.DateTimeField(null=True, blank=True)
     rank = models.ForeignKey(
     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)
     title = models.CharField(max_length=255, null=True, blank=True)
+    activation_requirement = models.PositiveIntegerField(default=0)
     is_staff = models.BooleanField(
     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.'))
         help_text=_('Designates whether the user can log into admin sites.'))
     roles = models.ManyToManyField('misago_acl.Role')
     roles = models.ManyToManyField('misago_acl.Role')
     acl_key = models.CharField(max_length=12, null=True, blank=True)
     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]
         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):
 class AnonymousUser(DjangoAnonymousUser):
     acl_key = 'anonymous'
     acl_key = 'anonymous'
 
 
@@ -207,3 +214,124 @@ class AnonymousUser(DjangoAnonymousUser):
 
 
     def update_acl_key(self):
     def update_acl_key(self):
         raise TypeError("Can't update ACL key on anonymous users")
         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