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

+ 9 - 0
misago/readtracker/categoriestracker.py

@@ -40,6 +40,9 @@ def make_read(categories):
 
 
 def start_record(user, category):
+    from misago.core import deprecations
+    deprecations.warn("categoriestracker.start_record has been deprecated")
+
     user.categoryread_set.create(
         category=category,
         last_read_on=user.joined_on,
@@ -47,6 +50,9 @@ def start_record(user, category):
 
 
 def sync_record(user, category):
+    from misago.core import deprecations
+    deprecations.warn("categoriestracker.sync_record has been deprecated")
+
     cutoff_date = get_cutoff_date()
     if user.joined_on > cutoff_date:
         cutoff_date = user.joined_on
@@ -90,6 +96,9 @@ def sync_record(user, category):
 
 
 def read_category(user, category):
+    from misago.core import deprecations
+    deprecations.warn("categoriestracker.read_category has been deprecated")
+
     categories = [category.pk]
     if not category.is_leaf_node():
         categories += category.get_descendants().filter(

+ 10 - 6
misago/readtracker/dates.py

@@ -5,18 +5,22 @@ from django.utils import timezone
 from misago.conf import settings
 
 
-def get_cutoff_date(*dates):
-    return timezone.now() - timedelta(days=settings.MISAGO_READTRACKER_CUTOFF)
+def get_cutoff_date(user=None):
+    cutoff_date = timezone.now() - timedelta(
+        days=settings.MISAGO_READTRACKER_CUTOFF,
+    )
 
+    if user and user.is_authenticated and user.joined_on > cutoff_date:
+        return user.joined_on
+    return cutoff_date
 
-def is_date_tracked(date, user, category_read_cutoff=None):
+
+def is_date_tracked(date, user):
     if date:
-        cutoff_date = timezone.now() - timedelta(days=settings.MISAGO_READTRACKER_CUTOFF)
+        cutoff_date = get_cutoff_date()
 
         if cutoff_date < user.joined_on:
             cutoff_date = user.joined_on
-        if category_read_cutoff and cutoff_date < category_read_cutoff:
-            cutoff_date = category_read_cutoff
 
         return date > cutoff_date
     else:

+ 55 - 0
misago/readtracker/poststracker.py

@@ -0,0 +1,55 @@
+from django.db import transaction
+from django.utils import timezone
+
+from .dates import get_cutoff_date
+from .models import PostRead
+
+
+def make_read_aware(user, target):
+    if not target:
+        return
+
+    if hasattr(target, '__iter__'):
+        make_posts_read_aware(user, target)
+    else:
+        make_posts_read_aware(user, [target])
+
+
+def make_posts_read_aware(user, posts):
+    make_read(posts)
+
+    if user.is_anonymous:
+        return
+
+    cutoff_date = get_cutoff_date(user)
+    unresolved_posts = {}
+
+    for post in posts:
+        if post.posted_on > cutoff_date:
+            post.is_read = False
+            post.is_new = True
+            unresolved_posts[post.pk] = post
+
+    if unresolved_posts:
+        queryset = user.postread_set.filter(post__in=unresolved_posts)
+        for post_id in queryset.values_list('post_id', flat=True):
+            unresolved_posts[post_id].is_read = True
+            unresolved_posts[post_id].is_new = False
+
+
+def make_read(posts):
+    for post in posts:
+        post.is_read = True
+        post.is_new = False
+
+
+def save_read(user, post):
+    user.postread_set.create(
+        category=post.category,
+        thread=post.thread,
+        post=post,
+    )
+
+
+def delete_reads(post):
+    post.postread_set.all().delete()

+ 34 - 12
misago/readtracker/tests/test_dates.py

@@ -3,15 +3,48 @@ from datetime import timedelta
 from django.test import TestCase
 from django.utils import timezone
 
-from misago.readtracker.dates import is_date_tracked
+from misago.conf import settings
+from misago.readtracker.dates import get_cutoff_date, is_date_tracked
 
 
 class MockUser(object):
+    is_authenticated = True
+
     def __init__(self):
         self.joined_on = timezone.now()
 
 
+class MockAnonymousUser(object):
+    is_authenticated = False
+
+
 class ReadTrackerDatesTests(TestCase):
+    def test_get_cutoff_date_no_user(self):
+        """get_cutoff_date utility works without user argument"""
+        valid_cutoff_date = timezone.now() - timedelta(days=settings.MISAGO_READTRACKER_CUTOFF)
+        returned_cutoff_date = get_cutoff_date()
+
+        self.assertTrue(returned_cutoff_date > valid_cutoff_date)
+
+    def test_get_cutoff_date_user(self):
+        """get_cutoff_date utility works with user argument"""
+        user = MockUser()
+
+        valid_cutoff_date = timezone.now() - timedelta(days=settings.MISAGO_READTRACKER_CUTOFF)
+        returned_cutoff_date = get_cutoff_date(user)
+
+        self.assertTrue(returned_cutoff_date > valid_cutoff_date)
+        self.assertEqual(returned_cutoff_date, user.joined_on)
+
+    def test_get_cutoff_date_user(self):
+        """passing anonymous user to get_cutoff_date has no effect"""
+        user = MockAnonymousUser()
+
+        valid_cutoff_date = timezone.now() - timedelta(days=settings.MISAGO_READTRACKER_CUTOFF)
+        returned_cutoff_date = get_cutoff_date(user)
+
+        self.assertTrue(returned_cutoff_date > valid_cutoff_date)
+
     def test_is_date_tracked(self):
         """is_date_tracked validates dates"""
         self.assertFalse(is_date_tracked(None, MockUser()))
@@ -21,14 +54,3 @@ class ReadTrackerDatesTests(TestCase):
 
         future_date = timezone.now() + timedelta(minutes=10)
         self.assertTrue(is_date_tracked(future_date, MockUser()))
-
-    def test_is_date_tracked_with_category_cutoff(self):
-        """is_date_tracked validates dates using category cutoff"""
-        self.assertFalse(is_date_tracked(None, MockUser()))
-        past_date = timezone.now() + timedelta(minutes=10)
-
-        category_cutoff = timezone.now() + timedelta(minutes=20)
-        self.assertFalse(is_date_tracked(past_date, MockUser(), category_cutoff))
-
-        category_cutoff = timezone.now() - timedelta(minutes=20)
-        self.assertTrue(is_date_tracked(past_date, MockUser(), category_cutoff))

+ 100 - 0
misago/readtracker/tests/test_poststracker.py

@@ -0,0 +1,100 @@
+from datetime import timedelta
+
+from django.contrib.auth import get_user_model
+from django.test import TestCase
+from django.utils import timezone
+
+from misago.categories.models import Category
+from misago.conf import settings
+from misago.readtracker import poststracker
+from misago.readtracker.models import PostRead
+from misago.threads import testutils
+
+
+UserModel = get_user_model()
+
+
+class AnonymousUser(object):
+    is_authenticated = False
+    is_anonymous = True
+
+
+class PostsTrackerTests(TestCase):
+    def setUp(self):
+        self.user = UserModel.objects.create_user("UserA", "testa@user.com", 'Pass.123')
+        self.category = Category.objects.get(slug='first-category')
+        self.thread = testutils.post_thread(self.category)
+
+    def test_falsy_value(self):
+        """passing falsy value to readtracker causes no errors"""
+        poststracker.make_read_aware(self.user, None)
+        poststracker.make_read_aware(self.user, False)
+        poststracker.make_read_aware(self.user, [])
+
+    def test_anon_post_behind_cutoff(self):
+        """non-tracked post is marked as read for anonymous users"""
+        posted_on = timezone.now() - timedelta(days=settings.MISAGO_READTRACKER_CUTOFF)
+        post = testutils.reply_thread(self.thread, posted_on=posted_on)
+
+        poststracker.make_read_aware(AnonymousUser(), post)
+        self.assertTrue(post.is_read)
+        self.assertFalse(post.is_new)
+
+    def test_anon_post_after_cutoff(self):
+        """tracked post is marked as read for anonymous users"""
+        post = testutils.reply_thread(self.thread, posted_on=timezone.now())
+
+        poststracker.make_read_aware(AnonymousUser(), post)
+        self.assertTrue(post.is_read)
+        self.assertFalse(post.is_new)
+
+    def test_user_post_behind_cutoff(self):
+        """untracked post is marked as read for authenticated users"""
+        posted_on = timezone.now() - timedelta(days=settings.MISAGO_READTRACKER_CUTOFF)
+        post = testutils.reply_thread(self.thread, posted_on=posted_on)
+
+        poststracker.make_read_aware(self.user, post)
+        self.assertTrue(post.is_read)
+        self.assertFalse(post.is_new)
+
+    def test_user_unread_post(self):
+        """tracked post is marked as unread for authenticated users"""
+        post = testutils.reply_thread(self.thread, posted_on=timezone.now())
+
+        poststracker.make_read_aware(self.user, post)
+        self.assertFalse(post.is_read)
+        self.assertTrue(post.is_new)
+
+    def test_user_created_after_post(self):
+        """tracked post older than user is marked as read"""
+        posted_on = timezone.now() - timedelta(days=1)
+        post = testutils.reply_thread(self.thread, posted_on=posted_on)
+
+        poststracker.make_read_aware(self.user, post)
+        self.assertTrue(post.is_read)
+        self.assertFalse(post.is_new)
+
+    def test_user_read_post(self):
+        """tracked post is marked as read for authenticated users with read entry"""
+        post = testutils.reply_thread(self.thread, posted_on=timezone.now())
+
+        poststracker.save_read(self.user, post)
+        poststracker.make_read_aware(self.user, post)
+
+        self.assertTrue(post.is_read)
+        self.assertFalse(post.is_new)
+
+    def test_delete_reads(self):
+        """delete_reads util removes post's reads"""
+        post = testutils.reply_thread(self.thread, posted_on=timezone.now())
+        other_post = testutils.reply_thread(self.thread, posted_on=timezone.now())
+
+        poststracker.save_read(self.user, post)
+        poststracker.save_read(self.user, other_post)
+
+        self.assertEqual(PostRead.objects.count(), 2)
+
+        poststracker.delete_reads(post)
+
+        self.assertEqual(PostRead.objects.count(), 1)
+

+ 12 - 0
misago/readtracker/threadstracker.py

@@ -7,6 +7,9 @@ from .models import CategoryRead, ThreadRead
 
 
 def make_read_aware(user, target):
+    if not target:
+        return
+
     if hasattr(target, '__iter__'):
         make_threads_read_aware(user, target)
     else:
@@ -53,6 +56,8 @@ def make_categories_threads_read_aware(user, threads):
 
 
 def fetch_categories_cutoffs_for_threads(user, threads):
+    from misago.core import deprecations
+    deprecations.warn("threadstracker.fetch_categories_cutoffs_for_threads has been deprecated")
     categories = []
     for thread in threads:
         if thread.category_id not in categories:
@@ -109,6 +114,8 @@ def make_thread_read_aware(user, thread):
 
 
 def make_posts_read_aware(user, thread, posts):
+    from misago.core import deprecations
+    deprecations.warn("threadstracker.make_posts_read_aware has been deprecated")
     try:
         is_thread_read = thread.is_read
     except AttributeError:
@@ -131,6 +138,8 @@ def make_posts_read_aware(user, thread, posts):
 
 
 def read_thread(user, thread, last_read_reply):
+    from misago.core import deprecations
+    deprecations.warn("threadstracker.read_thread has been deprecated")
     if not thread.is_read:
         if thread.last_read_on < last_read_reply.posted_on:
             sync_record(user, thread, last_read_reply)
@@ -138,6 +147,9 @@ def read_thread(user, thread, last_read_reply):
 
 @transaction.atomic
 def sync_record(user, thread, last_read_reply):
+    from misago.core import deprecations
+    deprecations.warn("threadstracker.sync_record has been deprecated")
+
     notification_triggers = ['read_thread_%s' % thread.pk]
 
     if thread.read_record:

+ 20 - 0
misago/threads/migrations/0007_auto_20171008_0131.py

@@ -0,0 +1,20 @@
+# -*- coding: utf-8 -*-
+# Generated by Django 1.11.5 on 2017-10-08 01:31
+from __future__ import unicode_literals
+
+from django.db import migrations, models
+
+
+class Migration(migrations.Migration):
+
+    dependencies = [
+        ('misago_threads', '0006_redo_partial_indexes'),
+    ]
+
+    operations = [
+        migrations.AlterField(
+            model_name='post',
+            name='posted_on',
+            field=models.DateTimeField(db_index=True),
+        ),
+    ]

+ 1 - 1
misago/threads/models/post.py

@@ -42,7 +42,7 @@ class Post(models.Model):
 
     attachments_cache = JSONField(null=True, blank=True)
 
-    posted_on = models.DateTimeField()
+    posted_on = models.DateTimeField(db_index=True)
     updated_on = models.DateTimeField()
     hidden_on = models.DateTimeField(default=timezone.now)