Browse Source

- Moved username change sync and user content deletion sync to signals.py
- Added mock session object for tests
- Added utils for using threads and posts functionality in tests
- Covered user_delete_content signal with tests

Ralfp 12 years ago
parent
commit
e2f4dea2c7

+ 13 - 0
misago/forums/models.py

@@ -4,6 +4,7 @@ from django.db import models
 from django.utils.translation import ugettext_lazy as _
 from django.utils.translation import ugettext_lazy as _
 from mptt.models import MPTTModel, TreeForeignKey
 from mptt.models import MPTTModel, TreeForeignKey
 from misago.roles.models import Role
 from misago.roles.models import Role
+from misago.users.signals import rename_user
 
 
 class ForumManager(models.Manager):
 class ForumManager(models.Manager):
     forums_tree = None
     forums_tree = None
@@ -184,3 +185,15 @@ class Forum(MPTTModel):
 
 
     def prune(self):
     def prune(self):
         pass
         pass
+
+
+"""
+Signals
+"""
+def rename_user_handler(sender, **kwargs):
+    Forum.objects.filter(last_poster=sender).update(
+                                                    last_poster_name=sender.username,
+                                                    last_poster_slug=sender.username_slug,
+                                                    )
+
+rename_user.connect(rename_user_handler, dispatch_uid="rename_forums_last_poster")

+ 8 - 0
misago/sessions/sessions.py

@@ -237,3 +237,11 @@ class SessionHuman(SessionMisago):
 
 
     def set_hidden(self, hidden=False):
     def set_hidden(self, hidden=False):
         self.hidden = hidden
         self.hidden = hidden
+
+
+class SessionMock(object):
+    def get_ip(self, request):
+        try:
+            return self.ip
+        except AttributeError:
+            return '127.0.0.1'

+ 13 - 3
misago/setup/management/commands/loaddata.py

@@ -12,6 +12,13 @@ class Command(BaseCommand):
     Loads Misago fixtures
     Loads Misago fixtures
     """
     """
     help = 'Load Misago fixtures'
     help = 'Load Misago fixtures'
+    option_list = BaseCommand.option_list + (
+        make_option('--quiet',
+            action='store_true',
+            dest='quiet',
+            default=False,
+            help='Dont display output from this message'),
+        )
     
     
     def handle(self, *args, **options):
     def handle(self, *args, **options):
         fixture_data = {}
         fixture_data = {}
@@ -23,10 +30,13 @@ class Command(BaseCommand):
             if app in fixture_data:
             if app in fixture_data:
                 if update_app_fixtures(app):
                 if update_app_fixtures(app):
                     updated += 1
                     updated += 1
-                    print 'Updating fixtures from %s' % app
+                    if not options['quiet']:
+                        self.stdout.write('Updating fixtures from %s' % app)
             else:
             else:
                 if load_app_fixtures(app):
                 if load_app_fixtures(app):
                     loaded += 1
                     loaded += 1
-                    print 'Loading fixtures from %s' % app
                     Fixture.objects.create(app_name=app)
                     Fixture.objects.create(app_name=app)
-        self.stdout.write('Loaded %s fixtures and updated %s fixtures.\n' % (loaded, updated))
+                    if not options['quiet']:
+                        self.stdout.write('Loading fixtures from %s' % app)
+        if not options['quiet']:
+            self.stdout.write('Loaded %s fixtures and updated %s fixtures.\n' % (loaded, updated))

+ 57 - 0
misago/threads/models.py

@@ -1,6 +1,7 @@
 from django.db import models
 from django.db import models
 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.users.signals import delete_user_content, rename_user
 from misago.utils import slugify
 from misago.utils import slugify
 
 
 class ThreadManager(models.Manager):
 class ThreadManager(models.Manager):
@@ -97,6 +98,7 @@ class Post(models.Model):
     post_preparsed = models.TextField()
     post_preparsed = models.TextField()
     upvotes = models.PositiveIntegerField(default=0)
     upvotes = models.PositiveIntegerField(default=0)
     downvotes = models.PositiveIntegerField(default=0)
     downvotes = models.PositiveIntegerField(default=0)
+    checkpoints = models.BooleanField(default=False, db_index=True)
     date = models.DateTimeField()
     date = models.DateTimeField()
     edits = models.PositiveIntegerField(default=0)
     edits = models.PositiveIntegerField(default=0)
     edit_date = models.DateTimeField(null=True, blank=True)
     edit_date = models.DateTimeField(null=True, blank=True)
@@ -118,6 +120,7 @@ class Post(models.Model):
 
 
     def set_checkpoint(self, request, action):
     def set_checkpoint(self, request, action):
         if request.user.is_authenticated():
         if request.user.is_authenticated():
+            self.checkpoints = True
             self.checkpoint_set.create(
             self.checkpoint_set.create(
                                        forum=self.forum,
                                        forum=self.forum,
                                        thread=self.thread,
                                        thread=self.thread,
@@ -161,3 +164,57 @@ class Checkpoint(models.Model):
     date = models.DateTimeField()
     date = models.DateTimeField()
     ip = models.GenericIPAddressField()
     ip = models.GenericIPAddressField()
     agent = models.CharField(max_length=255)
     agent = models.CharField(max_length=255)
+
+
+"""
+Signals
+"""
+def rename_user_handler(sender, **kwargs):
+    Thread.objects.filter(start_poster=sender).update(
+                                                     start_poster_name=sender.username,
+                                                     start_poster_slug=sender.username_slug,
+                                                     )
+    Thread.objects.filter(last_poster=sender).update(
+                                                     last_poster_name=sender.username,
+                                                     last_poster_slug=sender.username_slug,
+                                                     )
+    Post.objects.filter(user=sender).update(
+                                            user_name=sender.username,
+                                            )
+    Post.objects.filter(edit_user=sender).update(
+                                                 edit_user_name=sender.username,
+                                                 edit_user_slug=sender.username_slug,
+                                                 )
+    Change.objects.filter(user=sender).update(
+                                              user_name=sender.username,
+                                              user_slug=sender.username_slug,
+                                              )
+    Checkpoint.objects.filter(user=sender).update(
+                                                  user_name=sender.username,
+                                                  user_slug=sender.username_slug,
+                                                  )
+
+rename_user.connect(rename_user_handler, dispatch_uid="rename_user_threads")
+
+
+def delete_user_content_handler(sender, **kwargs):
+    Thread.objects.filter(start_poster=sender).delete()
+    threads = []
+    prev_posts = []
+    for post in sender.post_set.filter(checkpoints=True):
+        threads.append(post.thread_id)
+        prev_post = Post.objects.filter(thread=post.thread_id).exclude(user=sender).order_by('-id')[:1][0]
+        post.checkpoint_set.update(post=prev_post)
+        if not prev_post.pk in prev_posts:
+            prev_posts.append(prev_post.pk)
+    sender.post_set.all().delete()
+    Post.objects.filter(id__in=prev_posts).update(checkpoints=True)
+    for post in sender.post_set.distinct().values('thread_id').iterator():
+        if not post['thread_id'] in threads:
+            threads.append(post['thread_id'])
+    Post.objects.filter(user=sender).delete()
+    for thread in Thread.objects.filter(id__in=threads):
+        thread.sync()
+        thread.save(force_update=True)
+
+delete_user_content.connect(delete_user_content_handler, dispatch_uid="delete_user_threads_posts")

+ 91 - 0
misago/threads/tests.py

@@ -0,0 +1,91 @@
+from django.core.management import call_command
+from django.utils import timezone, unittest
+from django.test.client import RequestFactory
+from misago.settings.models import Setting
+from misago.forums.models import Forum
+from misago.sessions.sessions import SessionMock
+from misago.threads.models import Thread, Post, Change, Checkpoint
+from misago.threads.testutils import create_thread, create_post 
+from misago.users.models import User
+
+class DeleteThreadTestCase(unittest.TestCase):
+    def setUp(self):
+        call_command('loaddata', quiet=True)
+        self.factory = RequestFactory()
+        
+        Post.objects.all().delete()
+        Thread.objects.all().delete()
+        User.objects.all().delete()
+        self.user = User.objects.create_user('Neddart', 'ned@test.com', 'pass')
+        self.user_alt = User.objects.create_user('Robert', 'rob@test.com', 'pass')
+        self.forum = Forum.objects.get(id=1)
+        
+        self.thread = create_thread(self.forum)
+        self.post = create_post(self.thread, self.user)
+        
+    def test_deletion_owned(self):
+        """Check if user content delete results in correct deletion of thread"""
+        # Assert that test has been correctly initialised
+        self.assertEqual(Thread.objects.count(), 1)
+        self.assertEqual(Post.objects.count(), 1)
+        
+        # Run test
+        self.user.delete_content()
+        self.assertEqual(Thread.objects.count(), 0)
+        self.assertEqual(Post.objects.count(), 0)
+        
+    def test_deletion_other(self):
+        """Check if user content delete results in correct deletion of post"""
+        # Create second post
+        self.post = create_post(self.thread, self.user_alt)
+        
+        # Assert that test has been correctly initialised
+        self.assertEqual(Thread.objects.count(), 1)
+        self.assertEqual(Post.objects.count(), 2)
+        
+        # Run test
+        self.user_alt.delete_content()
+        self.assertEqual(Thread.objects.count(), 1)
+        self.assertEqual(Post.objects.count(), 1)
+        
+    def test_deletion_owned_other(self):
+        """Check if user content delete results in correct deletion of thread and posts"""
+        # Create second post
+        self.post = create_post(self.thread, self.user_alt)
+                
+        # Assert that test has been correctly initialised
+        self.assertEqual(Thread.objects.count(), 1)
+        self.assertEqual(Post.objects.count(), 2)
+        
+        # Run test
+        self.user.delete_content()
+        self.assertEqual(Thread.objects.count(), 0)
+        self.assertEqual(Post.objects.count(), 0)
+        
+    def test_deletion_checkpoints(self):
+        """Check if user content delete results in correct update of thread checkpoints"""
+        # Create an instance of a GET request.
+        request = self.factory.get('/customer/details')
+        request.session = SessionMock()
+        request.user = self.user_alt
+        request.META['HTTP_USER_AGENT'] = 'TestAgent'
+        
+        # Create second and third post
+        self.post = create_post(self.thread, self.user)
+        self.post_sec = create_post(self.thread, self.user_alt)
+        self.post_sec.set_checkpoint(request, 'locked')
+        self.post_sec.save(force_update=True)
+                
+        # Assert that test has been correctly initialised
+        self.assertEqual(Thread.objects.count(), 1)
+        self.assertEqual(Post.objects.count(), 3)
+        
+        # Run test
+        self.user_alt.delete_content()
+        self.assertEqual(Thread.objects.count(), 1)
+        self.assertEqual(Post.objects.count(), 2)
+        self.assertEqual(Checkpoint.objects.count(), 1)
+        self.assertEqual(Post.objects.filter(checkpoints=True).count(), 1)
+        self.assertEqual(Post.objects.get(id=self.post.pk).checkpoints, True)
+        self.assertEqual(Post.objects.get(id=self.post.pk).checkpoint_set.count(), 1)
+        

+ 40 - 0
misago/threads/testutils.py

@@ -0,0 +1,40 @@
+from django.utils import timezone
+from misago.threads.models import Thread, Post, Change, Checkpoint
+
+def create_thread(forum):
+    thread = Thread()
+    thread.forum = forum
+    thread.name = 'Test Thread'
+    thread.slug = 'test-thread'
+    thread.start = timezone.now()
+    thread.last = timezone.now()
+    thread.save(force_insert=True)
+    return thread
+
+
+def create_post(thread, user):
+    now = timezone.now()
+    post = Post()
+    post.forum = thread.forum
+    post.thread = thread
+    post.date = now
+    post.user = user
+    post.user_name = user.username
+    post.ip = '127.0.0.1'
+    post.agent = 'No matter'
+    post.post = 'No matter'
+    post.post_preparsed = 'No matter'
+    post.save(force_insert=True)
+    if not thread.start_post:
+        thread.start = now
+        thread.start_post = post
+        thread.start_poster = user
+        thread.start_poster_name = user.username
+        thread.start_poster_slug = user.username_slug
+    thread.last = now
+    thread.last_post = post
+    thread.last_poster = user
+    thread.last_poster_name = user.username
+    thread.last_poster_slug = user.username_slug
+    thread.save(force_update=True)
+    return post

+ 1 - 0
misago/threads/views/delete.py

@@ -72,6 +72,7 @@ class DeleteView(BaseView):
             self.thread.start_post.deleted = True
             self.thread.start_post.deleted = True
             self.thread.start_post.save(force_update=True)
             self.thread.start_post.save(force_update=True)
             self.thread.last_post.set_checkpoint(request, 'deleted')
             self.thread.last_post.set_checkpoint(request, 'deleted')
+            self.thread.last_post.save(force_update=True)
             self.thread.sync()
             self.thread.sync()
             self.thread.save(force_update=True)
             self.thread.save(force_update=True)
             self.forum.sync()
             self.forum.sync()

+ 14 - 0
misago/threads/views/list.py

@@ -132,6 +132,7 @@ class ThreadsView(BaseView):
 
 
     def action_accept(self, ids):
     def action_accept(self, ids):
         accepted = 0
         accepted = 0
+        last_posts = []
         users = []
         users = []
         for thread in self.threads:
         for thread in self.threads:
             if thread.pk in ids and thread.moderated:
             if thread.pk in ids and thread.moderated:
@@ -143,12 +144,14 @@ class ThreadsView(BaseView):
                 thread.start_post.moderated = False
                 thread.start_post.moderated = False
                 thread.start_post.save(force_update=True)
                 thread.start_post.save(force_update=True)
                 thread.last_post.set_checkpoint(self.request, 'accepted')
                 thread.last_post.set_checkpoint(self.request, 'accepted')
+                last_posts.append(thread.last_post.pk)
                 # Sync user
                 # Sync user
                 if thread.last_post.user:
                 if thread.last_post.user:
                     thread.start_post.user.threads += 1
                     thread.start_post.user.threads += 1
                     thread.start_post.user.posts += 1
                     thread.start_post.user.posts += 1
                     users.append(thread.start_post.user)
                     users.append(thread.start_post.user)
         if accepted:
         if accepted:
+            Post.objects.filter(id__in=last_posts).update(checkpoints=True)
             self.request.monitor['threads'] = int(self.request.monitor['threads']) + accepted
             self.request.monitor['threads'] = int(self.request.monitor['threads']) + accepted
             self.request.monitor['posts'] = int(self.request.monitor['posts']) + accepted
             self.request.monitor['posts'] = int(self.request.monitor['posts']) + accepted
             self.forum.threads_delta += 1
             self.forum.threads_delta += 1
@@ -272,26 +275,33 @@ class ThreadsView(BaseView):
 
 
     def action_open(self, ids):
     def action_open(self, ids):
         opened = []
         opened = []
+        last_posts = []
         for thread in self.threads:
         for thread in self.threads:
             if thread.pk in ids and thread.closed:
             if thread.pk in ids and thread.closed:
                 opened.append(thread.pk)
                 opened.append(thread.pk)
                 thread.last_post.set_checkpoint(self.request, 'opened')
                 thread.last_post.set_checkpoint(self.request, 'opened')
+                last_posts.append(thread.last_post.pk)
         if opened:
         if opened:
+            Post.objects.filter(id__in=last_posts).update(checkpoints=True)
             Thread.objects.filter(id__in=opened).update(closed=False)
             Thread.objects.filter(id__in=opened).update(closed=False)
             self.request.messages.set_flash(Message(_('Selected threads have been opened.')), 'success', 'threads')
             self.request.messages.set_flash(Message(_('Selected threads have been opened.')), 'success', 'threads')
 
 
     def action_close(self, ids):
     def action_close(self, ids):
         closed = []
         closed = []
+        last_posts = []
         for thread in self.threads:
         for thread in self.threads:
             if thread.pk in ids and not thread.closed:
             if thread.pk in ids and not thread.closed:
                 closed.append(thread.pk)
                 closed.append(thread.pk)
                 thread.last_post.set_checkpoint(self.request, 'closed')
                 thread.last_post.set_checkpoint(self.request, 'closed')
+                last_posts.append(thread.last_post.pk)
         if closed:
         if closed:
+            Post.objects.filter(id__in=last_posts).update(checkpoints=True)
             Thread.objects.filter(id__in=closed).update(closed=True)
             Thread.objects.filter(id__in=closed).update(closed=True)
             self.request.messages.set_flash(Message(_('Selected threads have been closed.')), 'success', 'threads')
             self.request.messages.set_flash(Message(_('Selected threads have been closed.')), 'success', 'threads')
 
 
     def action_undelete(self, ids):
     def action_undelete(self, ids):
         undeleted = []
         undeleted = []
+        last_posts = []
         posts = 0
         posts = 0
         for thread in self.threads:
         for thread in self.threads:
             if thread.pk in ids and thread.deleted:
             if thread.pk in ids and thread.deleted:
@@ -305,11 +315,13 @@ class ThreadsView(BaseView):
             self.request.monitor['posts'] = int(self.request.monitor['posts']) + posts
             self.request.monitor['posts'] = int(self.request.monitor['posts']) + posts
             self.forum.sync()
             self.forum.sync()
             self.forum.save(force_update=True)
             self.forum.save(force_update=True)
+            Post.objects.filter(id__in=last_posts).update(checkpoints=True)
             Thread.objects.filter(id__in=undeleted).update(deleted=False)
             Thread.objects.filter(id__in=undeleted).update(deleted=False)
             self.request.messages.set_flash(Message(_('Selected threads have been undeleted.')), 'success', 'threads')
             self.request.messages.set_flash(Message(_('Selected threads have been undeleted.')), 'success', 'threads')
 
 
     def action_soft(self, ids):
     def action_soft(self, ids):
         deleted = []
         deleted = []
+        last_posts = []
         posts = 0
         posts = 0
         for thread in self.threads:
         for thread in self.threads:
             if thread.pk in ids and not thread.deleted:
             if thread.pk in ids and not thread.deleted:
@@ -318,11 +330,13 @@ class ThreadsView(BaseView):
                 thread.start_post.deleted = True
                 thread.start_post.deleted = True
                 thread.start_post.save(force_update=True)
                 thread.start_post.save(force_update=True)
                 thread.last_post.set_checkpoint(self.request, 'deleted')
                 thread.last_post.set_checkpoint(self.request, 'deleted')
+                last_posts.append(thread.last_post.pk)
         if deleted:
         if deleted:
             self.request.monitor['threads'] = int(self.request.monitor['threads']) - len(deleted)
             self.request.monitor['threads'] = int(self.request.monitor['threads']) - len(deleted)
             self.request.monitor['posts'] = int(self.request.monitor['posts']) - posts
             self.request.monitor['posts'] = int(self.request.monitor['posts']) - posts
             self.forum.sync()
             self.forum.sync()
             self.forum.save(force_update=True)
             self.forum.save(force_update=True)
+            Post.objects.filter(id__in=last_posts).update(checkpoints=True)
             Thread.objects.filter(id__in=deleted).update(deleted=True)
             Thread.objects.filter(id__in=deleted).update(deleted=True)
             self.request.messages.set_flash(Message(_('Selected threads have been softly deleted.')), 'success', 'threads')
             self.request.messages.set_flash(Message(_('Selected threads have been softly deleted.')), 'success', 'threads')
 
 

+ 3 - 12
misago/users/models.py

@@ -16,6 +16,7 @@ from misago.acl.builder import build_acl
 from misago.monitor.monitor import Monitor
 from misago.monitor.monitor import Monitor
 from misago.roles.models import Role
 from misago.roles.models import Role
 from misago.settings.settings import Settings as DBSettings
 from misago.settings.settings import Settings as DBSettings
+from misago.users.signals import delete_user_content, rename_user
 from misago.users.validators import validate_username, validate_password, validate_email
 from misago.users.validators import validate_username, validate_password, validate_email
 from misago.utils import get_random_string, slugify
 from misago.utils import get_random_string, slugify
 from misago.utils.avatars import avatar_size
 from misago.utils.avatars import avatar_size
@@ -298,12 +299,7 @@ class User(models.Model):
         self.delete_avatar_image()
         self.delete_avatar_image()
 
 
     def delete_content(self):
     def delete_content(self):
-        if self.pk:
-            for model_obj in models.get_models():
-                try:
-                    model_obj.objects.delete_user_content(self)
-                except AttributeError:
-                    pass
+        delete_user_content.send(sender=self)
 
 
     def delete(self, *args, **kwargs):
     def delete(self, *args, **kwargs):
         self.delete_avatar()
         self.delete_avatar()
@@ -312,13 +308,8 @@ class User(models.Model):
     def set_username(self, username):
     def set_username(self, username):
         self.username = username.strip()
         self.username = username.strip()
         self.username_slug = slugify(username)
         self.username_slug = slugify(username)
-
         if self.pk:
         if self.pk:
-            for model_obj in models.get_models():
-                try:
-                    model_obj.objects.update_username(self)
-                except AttributeError:
-                    pass
+            rename_user.send(sender=self)
 
 
     def is_username_valid(self, e):
     def is_username_valid(self, e):
         try:
         try:

+ 4 - 0
misago/users/signals.py

@@ -0,0 +1,4 @@
+import django.dispatch
+
+delete_user_content = django.dispatch.Signal()
+rename_user = django.dispatch.Signal()

+ 22 - 0
misago/users/views.py

@@ -3,6 +3,7 @@ from django.db.models import Q
 from django.utils.translation import ugettext as _
 from django.utils.translation import ugettext as _
 from misago.admin import site
 from misago.admin import site
 from misago.admin.widgets import *
 from misago.admin.widgets import *
+from misago.forums.models import Forum
 from misago.markdown import signature_markdown
 from misago.markdown import signature_markdown
 from misago.users.forms import UserForm, NewUserForm, SearchUsersForm
 from misago.users.forms import UserForm, NewUserForm, SearchUsersForm
 from misago.users.models import User
 from misago.users.models import User
@@ -38,6 +39,7 @@ class List(ListWidget):
                ('remove_sig', _("Remove and lock signatures"), _("Are you sure you want to remove selected members signatures and their ability to edit them?")),
                ('remove_sig', _("Remove and lock signatures"), _("Are you sure you want to remove selected members signatures and their ability to edit them?")),
                ('remove_locks', _("Remove locks from avatars and signatures"), _("Are you sure you want to remove locks from selected members avatars and signatures?")),
                ('remove_locks', _("Remove locks from avatars and signatures"), _("Are you sure you want to remove locks from selected members avatars and signatures?")),
                ('reset', _("Reset passwords"), _("Are you sure you want to reset selected members passwords?")),
                ('reset', _("Reset passwords"), _("Are you sure you want to reset selected members passwords?")),
+               ('delete_content', _("Delete users with content"), _("Are you sure you want to delete selected users and their content?")),
                ('delete', _("Delete users"), _("Are you sure you want to delete selected users?")),
                ('delete', _("Delete users"), _("Are you sure you want to delete selected users?")),
                )
                )
 
 
@@ -188,6 +190,26 @@ class List(ListWidget):
 
 
         return Message(_('Selected users passwords have been reset successfully.'), 'success'), reverse('admin_users')
         return Message(_('Selected users passwords have been reset successfully.'), 'success'), reverse('admin_users')
 
 
+    def action_delete_content(self, items, checked):
+        for user in items:
+            if unicode(user.pk) in checked:
+                if user.pk == self.request.user.id:
+                    return Message(_('You cannot delete yourself.'), 'error'), reverse('admin_users')
+                if user.is_protected():
+                    return Message(_('You cannot delete protected members.'), 'error'), reverse('admin_users')
+
+        for user in items:
+            if unicode(user.pk) in checked:
+                user.delete_content()
+                user.delete()
+
+        for forum in Forum.objects.all():
+            forum.sync()
+            forum.save(force_update=True)
+        
+        User.objects.resync_monitor(self.request.monitor)
+        return Message(_('Selected users and their content have been deleted successfully.'), 'success'), reverse('admin_users')
+
     def action_delete(self, items, checked):
     def action_delete(self, items, checked):
         for user in items:
         for user in items:
             if unicode(user.pk) in checked:
             if unicode(user.pk) in checked: