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

Another iteration on anonymize_content

Rafał Pitoń 7 лет назад
Родитель
Сommit
2d2f0f58a7

+ 5 - 0
docs/settings/Core.md

@@ -121,6 +121,11 @@ Dialy limit of posts that may be posted from single account. Fail-safe for situa
 Function used to create unique avatar for this user. Allows for customization of algorithm used to generate those.
 
 
+## `MISAGO_ENABLE_DELETE_OWN_ACCOUNT`
+
+Allow users to delete their own accounts? Providing such feature is required by EU law from entities that process europeans personal data.
+
+
 ## `MISAGO_EVENTS_PER_PAGE`
 
 Misago reads events to display in separate database query to avoid situation when thread with large number of eg. moderator actions displays pages consisting exclusively of events. Using this setting you may specify upper limit of events displayed on thread's single page. This setting is intented as fail safe, both to save threads from excessively long lists of events your users will have to scroll trough, as well as to keep memory usage within limts.

+ 7 - 1
misago/conf/defaults.py

@@ -27,7 +27,13 @@ MISAGO_ACL_EXTENSIONS = [
 
 # Anonymous name used to replace deleted user's name in places that are keeping it
 
-MISAGO_ANONYMOUS_USERNAME = 'Ghost'
+MISAGO_ANONYMOUS_USERNAME = "Ghost"
+
+
+# Allow users to delete their own accounts?
+# Providing such feature is required by EU law from entities that process europeans personal data.
+
+MISAGO_ENABLE_DELETE_OWN_ACCOUNT = False
 
 
 # Custom markup extensions

+ 6 - 0
misago/project_template/project_name/settings.py

@@ -156,6 +156,12 @@ EMAIL_HOST_PASSWORD = ''
 DEFAULT_FROM_EMAIL = 'Forums <%s>' % EMAIL_HOST_USER
 
 
+# Allow users to delete their own accounts?
+# Providing such feature is required by EU law from entities that process europeans personal data.
+
+MISAGO_ENABLE_DELETE_OWN_ACCOUNT = True
+
+
 # Application definition
 
 AUTH_USER_MODEL = 'misago_users.User'

+ 41 - 0
misago/threads/anonymize.py

@@ -0,0 +1,41 @@
+from django.urls import reverse
+
+
+ANONYMIZABLE_EVENTS = (
+    'added_participant',
+    'changed_owner',
+    'owner_left',
+    'removed_owner',
+    'participant_left',
+    'removed_participant',
+)
+
+
+def anonymize_event(user, event):
+    if event.event_type not in ANONYMIZABLE_EVENTS:
+        raise ValueError('event of type "{}" can\'t be ananymized'.format(event.event_type))
+
+    event.event_context = {
+        'user': {
+            'id': None,
+            'username': user.username,
+            'url': reverse('misago:index'),
+        },
+    }
+    event.save(update_fields=['event_context'])
+
+
+def anonymize_post_last_likes(user, post):
+    cleaned_likes = []
+    for like in post.last_likes:
+        if like['id'] == user.id:
+            cleaned_likes.append({
+                'id': None,
+                'username': user.username
+            })
+        else:
+            cleaned_likes.append(like)
+
+    if cleaned_likes != post.last_likes:
+        post.last_likes = cleaned_likes
+        post.save(update_fields=['last_likes'])

+ 10 - 39
misago/threads/signals.py

@@ -2,7 +2,6 @@ from django.contrib.auth import get_user_model
 from django.db import transaction
 from django.db.models.signals import pre_delete
 from django.dispatch import Signal, receiver
-from django.urls import reverse
 
 from misago.categories.models import Category
 from misago.categories.signals import delete_category_content, move_category_content
@@ -10,6 +9,7 @@ from misago.core.pgutils import batch_delete, batch_update
 from misago.core.utils import ANONYMOUS_IP
 from misago.users.signals import anonymize_user_content, delete_user_content, username_changed
 
+from .anonymize import ANONYMIZABLE_EVENTS, anonymize_event, anonymize_post_last_likes
 from .models import Attachment, Poll, PollVote, Post, PostEdit, PostLike, Thread
 
 
@@ -93,6 +93,12 @@ def delete_user_threads(sender, **kwargs):
     recount_categories = set()
     recount_threads = set()
 
+    for post in sender.liked_post_set.iterator():
+        cleaned_likes = list(filter(lambda i: i['id'] != sender.id, post.last_likes))
+        if cleaned_likes != post.last_likes:
+            post.last_likes = cleaned_likes
+            post.save(update_fields=['last_likes'])
+            
     for thread in batch_delete(sender.thread_set.all(), 50):
         recount_categories.add(thread.category_id)
         with transaction.atomic():
@@ -116,15 +122,6 @@ def delete_user_threads(sender, **kwargs):
             category.save()
 
 
-@receiver([delete_user_content])
-def delete_user_in_likes(sender, **kwargs):
-    for post in sender.liked_post_set.iterator():
-        cleaned_likes = list(filter(lambda i: i['id'] != sender.id, post.last_likes))
-        if cleaned_likes != post.last_likes:
-            post.last_likes = cleaned_likes
-            post.save(update_fields=['last_likes'])
-
-
 @receiver(anonymize_user_content)
 def anonymize_user(sender, **kwargs):
     Post.objects.filter(poster=sender).update(poster_ip=ANONYMOUS_IP)
@@ -141,44 +138,18 @@ def anonymize_user(sender, **kwargs):
 def anonymize_user_in_events(sender, **kwargs):
     queryset = Post.objects.filter(
         is_event=True,
-        event_type__in=[
-            'added_participant',
-            'changed_owner',
-            'owner_left',
-            'removed_owner',
-            'participant_left',
-            'removed_participant',
-        ],
+        event_type__in=ANONYMIZABLE_EVENTS,
         event_context__user__id=sender.id,
     ).iterator()
 
     for event in queryset:
-        event.event_context = {
-            'user': {
-                'id': None,
-                'username': sender.username,
-                'url': reverse('misago:users'),
-            },
-        }
-        event.save(update_fields=['event_context'])
+        anonymize_event(sender, event)
 
 
 @receiver([anonymize_user_content])
 def anonymize_user_in_likes(sender, **kwargs):
     for post in sender.liked_post_set.iterator():
-        cleaned_likes = []
-        for like in post.last_likes:
-            if like['id'] == sender.id:
-                cleaned_likes.append({
-                    'id': None,
-                    'username': sender.username
-                })
-            else:
-                cleaned_likes.append(like)
-
-        if cleaned_likes != post.last_likes:
-            post.last_likes = cleaned_likes
-            post.save(update_fields=['last_likes'])
+        anonymize_post_last_likes(sender, post)
 
 
 @receiver([anonymize_user_content, username_changed])

+ 7 - 8
misago/threads/tests/test_anonymize_json_fields.py → misago/threads/tests/test_anonymize_data.py

@@ -3,7 +3,6 @@ from django.test import RequestFactory
 from django.urls import reverse
 
 from misago.categories.models import Category
-from misago.conf import settings
 from misago.users.testutils import AuthenticatedUserTestCase
 
 from misago.threads import testutils
@@ -55,7 +54,7 @@ class AnonymizeEventsTests(AuthenticatedUserTestCase):
             'user': {
                 'id': None,
                 'username': user.username,
-                'url': reverse('misago:users'),
+                'url': reverse('misago:index'),
             },
         })
 
@@ -75,7 +74,7 @@ class AnonymizeEventsTests(AuthenticatedUserTestCase):
             'user': {
                 'id': None,
                 'username': user.username,
-                'url': reverse('misago:users'),
+                'url': reverse('misago:index'),
             },
         })
 
@@ -98,7 +97,7 @@ class AnonymizeEventsTests(AuthenticatedUserTestCase):
             'user': {
                 'id': None,
                 'username': user.username,
-                'url': reverse('misago:users'),
+                'url': reverse('misago:index'),
             },
         })
 
@@ -121,7 +120,7 @@ class AnonymizeEventsTests(AuthenticatedUserTestCase):
             'user': {
                 'id': None,
                 'username': user.username,
-                'url': reverse('misago:users'),
+                'url': reverse('misago:index'),
             },
         })
 
@@ -144,7 +143,7 @@ class AnonymizeEventsTests(AuthenticatedUserTestCase):
             'user': {
                 'id': None,
                 'username': user.username,
-                'url': reverse('misago:users'),
+                'url': reverse('misago:index'),
             },
         })
         
@@ -167,7 +166,7 @@ class AnonymizeEventsTests(AuthenticatedUserTestCase):
             'user': {
                 'id': None,
                 'username': user.username,
-                'url': reverse('misago:users'),
+                'url': reverse('misago:index'),
             },
         })
 
@@ -208,4 +207,4 @@ class AnonymizeLikesTests(AuthenticatedUserTestCase):
                 'id': self.user.id,
                 'username': self.user.username,
             },
-        ])
+        ])

+ 52 - 0
misago/threads/tests/test_delete_user_likes.py

@@ -0,0 +1,52 @@
+from django.contrib.auth import get_user_model
+from django.test import RequestFactory
+
+from misago.categories.models import Category
+from misago.users.testutils import AuthenticatedUserTestCase
+
+from misago.threads import testutils
+from misago.threads.api.postendpoints.patch_post import patch_is_liked
+from misago.threads.models import Post
+
+
+UserModel = get_user_model()
+
+
+def get_mock_user():
+    seed = UserModel.objects.count() + 1
+    return UserModel.objects.create_user('bob%s' % seed, 'user%s@test.com' % seed, 'Pass.123')
+
+
+class DeleteUserLikesTests(AuthenticatedUserTestCase):
+    def setUp(self):
+        super(DeleteUserLikesTests, self).setUp()
+        self.factory = RequestFactory()
+
+    def get_request(self, user=None):
+        request = self.factory.get('/customer/details')
+        request.user = user or self.user
+        request.user_ip = '127.0.0.1'
+
+        return request
+
+    def test_anonymize_user_likes(self):
+        """post's last like is anonymized by user.anonymize_content"""
+        category = Category.objects.get(slug='first-category')
+        thread = testutils.post_thread(category)
+        post = testutils.reply_thread(thread)
+        post.acl = {'can_like': True}
+
+        user = get_mock_user()
+
+        patch_is_liked(self.get_request(self.user), post, 1)
+        patch_is_liked(self.get_request(user), post, 1)
+
+        user.delete_content()
+
+        last_likes = Post.objects.get(pk=post.pk).last_likes
+        self.assertEqual(last_likes, [
+            {
+                'id': self.user.id,
+                'username': self.user.username,
+            },
+        ])