Browse Source

posts, threads and categories trackers

Rafał Pitoń 7 years ago
parent
commit
af545a57e0

+ 31 - 95
misago/readtracker/categoriestracker.py

@@ -1,126 +1,62 @@
-from django.db.models import F
+from django.db import transaction
 from django.utils import timezone
 from django.utils import timezone
 
 
-from misago.threads.permissions import exclude_invisible_threads
+from misago.threads.models import Post, Thread
+from misago.threads.permissions import exclude_invisible_posts, exclude_invisible_threads
 
 
-from . import signals
-from .dates import get_cutoff_date, is_date_tracked
-from .models import CategoryRead
+from .dates import get_cutoff_date
+from .models import CategoryRead, ThreadRead
 
 
 
 
 def make_read_aware(user, categories):
 def make_read_aware(user, categories):
+    if not categories:
+        return
+
     if not hasattr(categories, '__iter__'):
     if not hasattr(categories, '__iter__'):
         categories = [categories]
         categories = [categories]
 
 
+    make_read(categories)
+
     if user.is_anonymous:
     if user.is_anonymous:
-        make_read(categories)
-        return None
+        return
 
 
-    categories_dict = {}
-    for category in categories:
-        category.last_read_on = user.joined_on
-        category.is_read = not is_date_tracked(category.last_post_on, user)
-        if not category.is_read:
-            categories_dict[category.pk] = category
+    threads = Thread.objects.filter(category__in=categories)
+    threads = exclude_invisible_threads(user, categories, threads)
 
 
-    if categories_dict:
-        categories_records = user.categoryread_set.filter(category__in=categories_dict.keys())
+    queryset = Post.objects.filter(
+        category__in=categories,
+        thread__in=threads,
+        posted_on__gt=get_cutoff_date(user),
+    ).values_list('category', flat=True).distinct()
 
 
-        for record in categories_records:
-            category = categories_dict[record.category_id]
-            category.last_read_on = record.last_read_on
-            category.is_read = category.last_read_on >= category.last_post_on
+    queryset = queryset.exclude(id__in=user.postread_set.values('post'))
+    queryset = exclude_invisible_posts(user, categories, queryset)
 
 
+    unread_categories = list(queryset)
 
 
-def make_read(categories):
-    now = timezone.now()
     for category in categories:
     for category in categories:
-        category.last_read_on = now
-        category.is_read = True
+        if category.pk in unread_categories:
+            category.is_read = False
+            category.is_new = True
+
 
 
+def make_read(threads):
+    for thread in threads:
+        thread.is_read = True
+        thread.is_new = False
 
 
+
+# Deprecated stuff goes here
 def start_record(user, category):
 def start_record(user, category):
     from misago.core import deprecations
     from misago.core import deprecations
     deprecations.warn("categoriestracker.start_record has been deprecated")
     deprecations.warn("categoriestracker.start_record has been deprecated")
 
 
-    user.categoryread_set.create(
-        category=category,
-        last_read_on=user.joined_on,
-    )
-
 
 
 def sync_record(user, category):
 def sync_record(user, category):
     from misago.core import deprecations
     from misago.core import deprecations
     deprecations.warn("categoriestracker.sync_record has been deprecated")
     deprecations.warn("categoriestracker.sync_record has been deprecated")
 
 
-    cutoff_date = get_cutoff_date()
-    if user.joined_on > cutoff_date:
-        cutoff_date = user.joined_on
-
-    try:
-        category_record = user.categoryread_set.get(category=category)
-        if category_record.last_read_on > cutoff_date:
-            cutoff_date = category_record.last_read_on
-    except CategoryRead.DoesNotExist:
-        category_record = None
-
-    all_threads = category.thread_set.filter(last_post_on__gt=cutoff_date)
-    all_threads_count = exclude_invisible_threads(user, [category], all_threads).count()
-
-    read_threads_count = user.threadread_set.filter(
-        category=category,
-        thread__in=all_threads,
-        last_read_on__gt=cutoff_date,
-        thread__last_post_on__lte=F("last_read_on"),
-    ).count()
-
-    category_is_read = read_threads_count == all_threads_count
-
-    if category_is_read:
-        signals.category_read.send(sender=user, category=category)
-
-    if category_record:
-        if category_is_read:
-            category_record.last_read_on = timezone.now()
-        else:
-            category_record.last_read_on = cutoff_date
-        category_record.save(update_fields=['last_read_on'])
-    else:
-        if category_is_read:
-            last_read_on = timezone.now()
-        else:
-            last_read_on = cutoff_date
-        category_record = user.categoryread_set.create(
-            category=category, last_read_on=last_read_on
-        )
-
 
 
 def read_category(user, category):
 def read_category(user, category):
     from misago.core import deprecations
     from misago.core import deprecations
     deprecations.warn("categoriestracker.read_category has been deprecated")
     deprecations.warn("categoriestracker.read_category has been deprecated")
-
-    categories = [category.pk]
-    if not category.is_leaf_node():
-        categories += category.get_descendants().filter(
-            id__in=user.acl_cache['visible_categories'],
-        ).values_list(
-            'id',
-            flat=True,
-        )
-
-    user.categoryread_set.filter(category_id__in=categories).delete()
-    user.threadread_set.filter(category_id__in=categories).delete()
-
-    now = timezone.now()
-    new_reads = []
-    for category in categories:
-        new_reads.append(CategoryRead(
-            user=user,
-            category_id=category,
-            last_read_on=now,
-        ))
-
-    if new_reads:
-        CategoryRead.objects.bulk_create(new_reads)
-
-    signals.category_read.send(sender=user, category=category)

+ 234 - 0
misago/readtracker/tests/test_categoriestracker.py

@@ -0,0 +1,234 @@
+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.core import cache, threadstore
+from misago.readtracker import poststracker, categoriestracker
+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 CategoriesTrackerTests(TestCase):
+    def setUp(self):
+        cache.cache.clear()
+        threadstore.clear()
+
+        self.user = UserModel.objects.create_user("UserA", "testa@user.com", 'Pass.123')
+        self.category = Category.objects.get(slug='first-category')
+
+    def test_falsy_value(self):
+        """passing falsy value to readtracker causes no errors"""
+        categoriestracker.make_read_aware(self.user, None)
+        categoriestracker.make_read_aware(self.user, False)
+        categoriestracker.make_read_aware(self.user, [])
+
+    def test_anon_thread_before_cutoff(self):
+        """non-tracked thread is marked as read for anonymous users"""
+        started_on = timezone.now() - timedelta(days=settings.MISAGO_READTRACKER_CUTOFF)
+        testutils.post_thread(self.category, started_on=started_on)
+
+        categoriestracker.make_read_aware(AnonymousUser(), self.category)
+        self.assertTrue(self.category.is_read)
+        self.assertFalse(self.category.is_new)
+
+    def test_anon_thread_after_cutoff(self):
+        """tracked thread is marked as read for anonymous users"""
+        testutils.post_thread(self.category, started_on=timezone.now())
+
+        categoriestracker.make_read_aware(AnonymousUser(), self.category)
+        self.assertTrue(self.category.is_read)
+        self.assertFalse(self.category.is_new)
+
+    def test_user_thread_before_cutoff(self):
+        """non-tracked thread is marked as read for authenticated users"""
+        started_on = timezone.now() - timedelta(days=settings.MISAGO_READTRACKER_CUTOFF)
+        testutils.post_thread(self.category, started_on=started_on)
+
+        categoriestracker.make_read_aware(self.user, self.category)
+        self.assertTrue(self.category.is_read)
+        self.assertFalse(self.category.is_new)
+
+    def test_user_unread_thread(self):
+        """tracked thread is marked as unread for authenticated users"""
+        testutils.post_thread(self.category, started_on=timezone.now())
+
+        categoriestracker.make_read_aware(self.user, self.category)
+        self.assertFalse(self.category.is_read)
+        self.assertTrue(self.category.is_new)
+
+    def test_user_created_after_thread(self):
+        """tracked thread older than user is marked as read"""
+        started_on = timezone.now() - timedelta(days=1)
+        testutils.post_thread(self.category, started_on=started_on)
+
+        categoriestracker.make_read_aware(self.user, self.category)
+        self.assertTrue(self.category.is_read)
+        self.assertFalse(self.category.is_new)
+
+    def test_user_read_post(self):
+        """tracked thread with read post marked as read"""
+        thread = testutils.post_thread(self.category, started_on=timezone.now())
+
+        poststracker.save_read(self.user, thread.first_post)
+
+        categoriestracker.make_read_aware(self.user, self.category)
+        self.assertTrue(self.category.is_read)
+        self.assertFalse(self.category.is_new)
+
+    def test_user_first_unread_last_read_post(self):
+        """tracked thread with unread first and last read post marked as unread"""
+        thread = testutils.post_thread(self.category, started_on=timezone.now())
+
+        post = testutils.reply_thread(thread, posted_on=timezone.now())
+        poststracker.save_read(self.user, post)
+
+        categoriestracker.make_read_aware(self.user, self.category)
+        self.assertFalse(self.category.is_read)
+        self.assertTrue(self.category.is_new)
+
+    def test_user_first_read_post_unread_event(self):
+        """tracked thread with read first post and unread event"""
+        thread = testutils.post_thread(self.category, started_on=timezone.now())
+        poststracker.save_read(self.user, thread.first_post)
+
+        testutils.reply_thread(thread, posted_on=timezone.now(), is_event=True)
+
+        categoriestracker.make_read_aware(self.user, self.category)
+        self.assertFalse(self.category.is_read)
+        self.assertTrue(self.category.is_new)
+
+    def test_user_hidden_event(self):
+        """tracked thread with unread first post and hidden event"""
+        thread = testutils.post_thread(self.category, started_on=timezone.now())
+
+        testutils.reply_thread(
+            thread,
+            posted_on=timezone.now(),
+            is_event=True,
+            is_hidden=True,
+        )
+
+        categoriestracker.make_read_aware(self.user, self.category)
+        self.assertFalse(self.category.is_read)
+        self.assertTrue(self.category.is_new)
+
+    def test_user_first_read_post_hidden_event(self):
+        """tracked thread with read first post and hidden event"""
+        thread = testutils.post_thread(self.category, started_on=timezone.now())
+        poststracker.save_read(self.user, thread.first_post)
+
+        testutils.reply_thread(
+            thread,
+            posted_on=timezone.now(),
+            is_event=True,
+            is_hidden=True,
+        )
+
+        categoriestracker.make_read_aware(self.user, self.category)
+        self.assertTrue(self.category.is_read)
+        self.assertFalse(self.category.is_new)
+
+    def test_user_thread_before_cutoff_unread_post(self):
+        """non-tracked thread is marked as unread for anonymous users"""
+        started_on = timezone.now() - timedelta(days=settings.MISAGO_READTRACKER_CUTOFF)
+        testutils.post_thread(self.category, started_on=started_on)
+
+        categoriestracker.make_read_aware(self.user, self.category)
+        self.assertTrue(self.category.is_read)
+        self.assertFalse(self.category.is_new)
+
+    def test_user_first_read_post_unapproved_post(self):
+        """tracked thread with read first post and unapproved post"""
+        thread = testutils.post_thread(self.category, started_on=timezone.now())
+        poststracker.save_read(self.user, thread.first_post)
+
+        testutils.reply_thread(
+            thread,
+            posted_on=timezone.now(),
+            is_unapproved=True,
+        )
+
+        categoriestracker.make_read_aware(self.user, self.category)
+        self.assertTrue(self.category.is_read)
+        self.assertFalse(self.category.is_new)
+
+    def test_user_first_read_post_unapproved_own_post(self):
+        """tracked thread with read first post and unapproved own post"""
+        thread = testutils.post_thread(self.category, started_on=timezone.now())
+        poststracker.save_read(self.user, thread.first_post)
+
+        testutils.reply_thread(
+            thread,
+            posted_on=timezone.now(),
+            poster=self.user,
+            is_unapproved=True,
+        )
+
+        categoriestracker.make_read_aware(self.user, self.category)
+        self.assertFalse(self.category.is_read)
+        self.assertTrue(self.category.is_new)
+
+    def test_user_first_read_post_unapproved_own_post(self):
+        """tracked thread with read first post and unapproved own post"""
+        thread = testutils.post_thread(self.category, started_on=timezone.now())
+        poststracker.save_read(self.user, thread.first_post)
+
+        testutils.reply_thread(
+            thread,
+            posted_on=timezone.now(),
+            poster=self.user,
+            is_unapproved=True,
+        )
+
+        categoriestracker.make_read_aware(self.user, self.category)
+        self.assertFalse(self.category.is_read)
+        self.assertTrue(self.category.is_new)
+
+    def test_user_unapproved_thread_unread_post(self):
+        """tracked unapproved thread"""
+        thread = testutils.post_thread(
+            self.category,
+            started_on=timezone.now(),
+            is_unapproved=True,
+        )
+
+        categoriestracker.make_read_aware(self.user, self.category)
+        self.assertTrue(self.category.is_read)
+        self.assertFalse(self.category.is_new)
+
+    def test_user_unapproved_own_thread_unread_post(self):
+        """tracked unapproved but visible thread"""
+        thread = testutils.post_thread(
+            self.category,
+            poster=self.user,
+            started_on=timezone.now(),
+            is_unapproved=True,
+        )
+
+        categoriestracker.make_read_aware(self.user, self.category)
+        self.assertFalse(self.category.is_read)
+        self.assertTrue(self.category.is_new)
+
+    def test_user_hidden_thread_unread_post(self):
+        """tracked hidden thread"""
+        thread = testutils.post_thread(
+            self.category,
+            started_on=timezone.now(),
+            is_hidden=True,
+        )
+
+        categoriestracker.make_read_aware(self.user, self.category)
+        self.assertTrue(self.category.is_read)
+        self.assertFalse(self.category.is_new)

+ 2 - 2
misago/readtracker/tests/test_poststracker.py

@@ -31,7 +31,7 @@ class PostsTrackerTests(TestCase):
         poststracker.make_read_aware(self.user, False)
         poststracker.make_read_aware(self.user, False)
         poststracker.make_read_aware(self.user, [])
         poststracker.make_read_aware(self.user, [])
 
 
-    def test_anon_post_behind_cutoff(self):
+    def test_anon_post_before_cutoff(self):
         """non-tracked post is marked as read for anonymous users"""
         """non-tracked post is marked as read for anonymous users"""
         posted_on = timezone.now() - timedelta(days=settings.MISAGO_READTRACKER_CUTOFF)
         posted_on = timezone.now() - timedelta(days=settings.MISAGO_READTRACKER_CUTOFF)
         post = testutils.reply_thread(self.thread, posted_on=posted_on)
         post = testutils.reply_thread(self.thread, posted_on=posted_on)
@@ -48,7 +48,7 @@ class PostsTrackerTests(TestCase):
         self.assertTrue(post.is_read)
         self.assertTrue(post.is_read)
         self.assertFalse(post.is_new)
         self.assertFalse(post.is_new)
 
 
-    def test_user_post_behind_cutoff(self):
+    def test_user_post_before_cutoff(self):
         """untracked post is marked as read for authenticated users"""
         """untracked post is marked as read for authenticated users"""
         posted_on = timezone.now() - timedelta(days=settings.MISAGO_READTRACKER_CUTOFF)
         posted_on = timezone.now() - timedelta(days=settings.MISAGO_READTRACKER_CUTOFF)
         post = testutils.reply_thread(self.thread, posted_on=posted_on)
         post = testutils.reply_thread(self.thread, posted_on=posted_on)

+ 184 - 0
misago/readtracker/tests/test_threadstracker.py

@@ -0,0 +1,184 @@
+from datetime import timedelta
+
+from django.contrib.auth import get_user_model
+from django.test import TestCase
+from django.utils import timezone
+
+from misago.acl import add_acl
+from misago.categories.models import Category
+from misago.conf import settings
+from misago.core import cache, threadstore
+from misago.readtracker import poststracker, threadstracker
+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 ThreadsTrackerTests(TestCase):
+    def setUp(self):
+        cache.cache.clear()
+        threadstore.clear()
+
+        self.user = UserModel.objects.create_user("UserA", "testa@user.com", 'Pass.123')
+        self.category = Category.objects.get(slug='first-category')
+
+        add_acl(self.user, self.category)
+
+    def test_falsy_value(self):
+        """passing falsy value to readtracker causes no errors"""
+        threadstracker.make_read_aware(self.user, None)
+        threadstracker.make_read_aware(self.user, False)
+        threadstracker.make_read_aware(self.user, [])
+
+    def test_anon_thread_before_cutoff(self):
+        """non-tracked thread is marked as read for anonymous users"""
+        started_on = timezone.now() - timedelta(days=settings.MISAGO_READTRACKER_CUTOFF)
+        thread = testutils.post_thread(self.category, started_on=started_on)
+
+        threadstracker.make_read_aware(AnonymousUser(), thread)
+        self.assertTrue(thread.is_read)
+        self.assertFalse(thread.is_new)
+
+    def test_anon_thread_after_cutoff(self):
+        """tracked thread is marked as read for anonymous users"""
+        thread = testutils.post_thread(self.category, started_on=timezone.now())
+
+        threadstracker.make_read_aware(AnonymousUser(), thread)
+        self.assertTrue(thread.is_read)
+        self.assertFalse(thread.is_new)
+
+    def test_user_thread_before_cutoff(self):
+        """non-tracked thread is marked as read for authenticated users"""
+        started_on = timezone.now() - timedelta(days=settings.MISAGO_READTRACKER_CUTOFF)
+        thread = testutils.post_thread(self.category, started_on=started_on)
+
+        threadstracker.make_read_aware(self.user, thread)
+        self.assertTrue(thread.is_read)
+        self.assertFalse(thread.is_new)
+
+    def test_user_unread_thread(self):
+        """tracked thread is marked as unread for authenticated users"""
+        thread = testutils.post_thread(self.category, started_on=timezone.now())
+
+        threadstracker.make_read_aware(self.user, thread)
+        self.assertFalse(thread.is_read)
+        self.assertTrue(thread.is_new)
+
+    def test_user_created_after_thread(self):
+        """tracked thread older than user is marked as read"""
+        started_on = timezone.now() - timedelta(days=1)
+        thread = testutils.post_thread(self.category, started_on=started_on)
+
+        threadstracker.make_read_aware(self.user, thread)
+        self.assertTrue(thread.is_read)
+        self.assertFalse(thread.is_new)
+
+    def test_user_read_post(self):
+        """tracked thread with read post marked as read"""
+        thread = testutils.post_thread(self.category, started_on=timezone.now())
+
+        poststracker.save_read(self.user, thread.first_post)
+
+        threadstracker.make_read_aware(self.user, thread)
+        self.assertTrue(thread.is_read)
+        self.assertFalse(thread.is_new)
+
+    def test_user_first_unread_last_read_post(self):
+        """tracked thread with unread first and last read post marked as unread"""
+        thread = testutils.post_thread(self.category, started_on=timezone.now())
+
+        post = testutils.reply_thread(thread, posted_on=timezone.now())
+        poststracker.save_read(self.user, post)
+
+        threadstracker.make_read_aware(self.user, thread)
+        self.assertFalse(thread.is_read)
+        self.assertTrue(thread.is_new)
+
+    def test_user_first_read_post_unread_event(self):
+        """tracked thread with read first post and unread event"""
+        thread = testutils.post_thread(self.category, started_on=timezone.now())
+        poststracker.save_read(self.user, thread.first_post)
+
+        testutils.reply_thread(thread, posted_on=timezone.now(), is_event=True)
+
+        threadstracker.make_read_aware(self.user, thread)
+        self.assertFalse(thread.is_read)
+        self.assertTrue(thread.is_new)
+
+    def test_user_hidden_event(self):
+        """tracked thread with unread first post and hidden event"""
+        thread = testutils.post_thread(self.category, started_on=timezone.now())
+
+        testutils.reply_thread(
+            thread,
+            posted_on=timezone.now(),
+            is_event=True,
+            is_hidden=True,
+        )
+
+        threadstracker.make_read_aware(self.user, thread)
+        self.assertFalse(thread.is_read)
+        self.assertTrue(thread.is_new)
+
+    def test_user_first_read_post_hidden_event(self):
+        """tracked thread with read first post and hidden event"""
+        thread = testutils.post_thread(self.category, started_on=timezone.now())
+        poststracker.save_read(self.user, thread.first_post)
+
+        testutils.reply_thread(
+            thread,
+            posted_on=timezone.now(),
+            is_event=True,
+            is_hidden=True,
+        )
+
+        threadstracker.make_read_aware(self.user, thread)
+        self.assertTrue(thread.is_read)
+        self.assertFalse(thread.is_new)
+
+    def test_user_thread_before_cutoff_unread_post(self):
+        """non-tracked thread is marked as unread for anonymous users"""
+        started_on = timezone.now() - timedelta(days=settings.MISAGO_READTRACKER_CUTOFF)
+        thread = testutils.post_thread(self.category, started_on=started_on)
+
+        threadstracker.make_read_aware(self.user, thread)
+        self.assertTrue(thread.is_read)
+        self.assertFalse(thread.is_new)
+
+    def test_user_first_read_post_unapproved_post(self):
+        """tracked thread with read first post and unapproved post"""
+        thread = testutils.post_thread(self.category, started_on=timezone.now())
+        poststracker.save_read(self.user, thread.first_post)
+
+        testutils.reply_thread(
+            thread,
+            posted_on=timezone.now(),
+            is_unapproved=True,
+        )
+
+        threadstracker.make_read_aware(self.user, thread)
+        self.assertTrue(thread.is_read)
+        self.assertFalse(thread.is_new)
+
+    def test_user_first_read_post_unapproved_own_post(self):
+        """tracked thread with read first post and unapproved own post"""
+        thread = testutils.post_thread(self.category, started_on=timezone.now())
+        poststracker.save_read(self.user, thread.first_post)
+
+        testutils.reply_thread(
+            thread,
+            posted_on=timezone.now(),
+            poster=self.user,
+            is_unapproved=True,
+        )
+
+        threadstracker.make_read_aware(self.user, thread)
+        self.assertFalse(thread.is_read)
+        self.assertTrue(thread.is_new)

+ 28 - 142
misago/readtracker/threadstracker.py

@@ -1,169 +1,55 @@
 from django.db import transaction
 from django.db import transaction
 from django.utils import timezone
 from django.utils import timezone
 
 
-from . import categoriestracker, signals
-from .dates import is_date_tracked
-from .models import CategoryRead, ThreadRead
-
-
-def make_read_aware(user, target):
-    if not target:
-        return
+from misago.threads.models import Post
+from misago.threads.permissions import exclude_invisible_posts
 
 
-    if hasattr(target, '__iter__'):
-        make_threads_read_aware(user, target)
-    else:
-        make_thread_read_aware(user, target)
+from .dates import get_cutoff_date
+from .models import CategoryRead, ThreadRead
 
 
 
 
-def make_threads_read_aware(user, threads):
+def make_read_aware(user, threads):
     if not threads:
     if not threads:
         return
         return
 
 
-    if user.is_anonymous:
-        make_read(threads)
-    else:
-        make_categories_threads_read_aware(user, threads)
+    if not hasattr(threads, '__iter__'):
+        threads = [threads]
 
 
+    make_read(threads)
 
 
-def make_read(threads):
-    for thread in threads:
-        thread.is_read = True
-        thread.is_new = False
+    if user.is_anonymous:
+        return
 
 
+    categories = [t.category for t in threads]
 
 
-def make_unread(threads):
-    for thread in threads:
-        thread.is_read = False
-        thread.is_new = True
+    queryset = Post.objects.filter(
+        thread__in=threads,
+        posted_on__gt=get_cutoff_date(user),
+    ).values_list('thread', flat=True).distinct()
 
 
+    queryset = queryset.exclude(id__in=user.postread_set.values('post'))
+    queryset = exclude_invisible_posts(user, categories, queryset)
 
 
-def make_categories_threads_read_aware(user, threads):
-    categories_cutoffs = fetch_categories_cutoffs_for_threads(user, threads)
+    unread_threads = list(queryset)
 
 
-    threads_dict = {}
     for thread in threads:
     for thread in threads:
-        category_cutoff = categories_cutoffs.get(thread.category_id)
-        thread.is_read = not is_date_tracked(thread.last_post_on, user, category_cutoff)
-        thread.is_new = not thread.is_read
-        thread.last_read_on = user.joined_on
-
-        if not thread.is_read:
-            threads_dict[thread.pk] = thread
+        if thread.pk in unread_threads:
+            thread.is_read = False
+            thread.is_new = True
 
 
-    if threads_dict:
-        make_threads_dict_read_aware(user, threads_dict)
 
 
-
-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 = []
+def make_read(threads):
     for thread in threads:
     for thread in threads:
-        if thread.category_id not in categories:
-            categories.append(thread.category_id)
-
-    categories_dict = {}
-    for record in user.categoryread_set.filter(category__in=categories):
-        categories_dict[record.category_id] = record.last_read_on
-    return categories_dict
-
-
-def make_threads_dict_read_aware(user, threads_dict):
-    for record in user.threadread_set.filter(thread__in=threads_dict.keys()):
-        if record.thread_id in threads_dict:
-            thread = threads_dict[record.thread_id]
-            thread.is_read = record.last_read_on >= thread.last_post_on
-            thread.is_new = not thread.is_read
-            thread.last_read_on = record.last_read_on
-
+        thread.is_read = True
+        thread.is_new = False
 
 
-def make_thread_read_aware(user, thread):
-    thread.is_read = True
-    thread.is_new = False
-    thread.read_record = None
 
 
-    if user.is_anonymous:
-        thread.last_read_on = timezone.now()
-    else:
-        thread.last_read_on = user.joined_on
-
-    if user.is_authenticated and is_date_tracked(thread.last_post_on, user):
-        thread.is_read = False
-        thread.is_new = True
-
-        try:
-            category_record = user.categoryread_set.get(category_id=thread.category_id)
-            thread.last_read_on = category_record.last_read_on
-
-            if thread.last_post_on > category_record.last_read_on:
-                try:
-                    thread_record = user.threadread_set.get(thread=thread)
-                    thread.last_read_on = thread_record.last_read_on
-                    if thread.last_post_on <= thread_record.last_read_on:
-                        thread.is_new = False
-                        thread.is_read = True
-                    thread.read_record = thread_record
-                except ThreadRead.DoesNotExist:
-                    pass
-            else:
-                thread.is_read = True
-                thread.is_new = False
-        except CategoryRead.DoesNotExist:
-            categoriestracker.start_record(user, thread.category)
-
-
-def make_posts_read_aware(user, thread, posts):
+# Noop placeholders for exploding tests suite
+def make_posts_read_aware(*args, **kwargs):
     from misago.core import deprecations
     from misago.core import deprecations
     deprecations.warn("threadstracker.make_posts_read_aware has been deprecated")
     deprecations.warn("threadstracker.make_posts_read_aware has been deprecated")
-    try:
-        is_thread_read = thread.is_read
-    except AttributeError:
-        raise ValueError(
-            "thread passed make_posts_read_aware should be "
-            "made read aware via make_thread_read_aware"
-        )
-
-    if is_thread_read:
-        for post in posts:
-            post.is_read = True
-            post.is_new = False
-    else:
-        for post in posts:
-            if is_date_tracked(post.posted_on, user):
-                post.is_read = post.posted_on <= thread.last_read_on
-            else:
-                post.is_read = True
-            post.is_new = not post.is_read
-
-
-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)
 
 
 
 
-@transaction.atomic
-def sync_record(user, thread, last_read_reply):
+def read_thread(*args, **kwargs):
     from misago.core import deprecations
     from misago.core import deprecations
-    deprecations.warn("threadstracker.sync_record has been deprecated")
-
-    notification_triggers = ['read_thread_%s' % thread.pk]
-
-    if thread.read_record:
-        thread.read_record.last_read_on = last_read_reply.posted_on
-        thread.read_record.save(update_fields=['last_read_on'])
-    else:
-        user.threadread_set.create(
-            category=thread.category,
-            thread=thread,
-            last_read_on=last_read_reply.posted_on,
-        )
-        signals.thread_tracked.send(sender=user, thread=thread)
-        notification_triggers.append('see_thread_%s' % thread.pk)
-
-    if last_read_reply.posted_on == thread.last_post_on:
-        signals.thread_read.send(sender=user, thread=thread)
-        categoriestracker.sync_record(user, thread.category)
+    deprecations.warn("threadstracker.read_thread has been deprecated")

+ 66 - 1
misago/threads/permissions/threads.py

@@ -1348,7 +1348,72 @@ def exclude_invisible_threads(user, categories, queryset):
         return Thread.objects.none()
         return Thread.objects.none()
 
 
 
 
-def exclude_invisible_posts(user, category, queryset):
+def exclude_invisible_posts(user, categories, queryset):
+    if hasattr(categories, '__iter__'):
+        return exclude_invisible_posts_in_categories(user, categories, queryset)
+    else:
+        return exclude_invisible_posts_in_category(user, categories, queryset)
+
+
+def exclude_invisible_posts_in_categories(user, categories, queryset):
+    show_all = []
+    show_approved = []
+    show_approved_owned = []
+
+    hide_invisible_events = []
+
+    for category in categories:
+        if category.acl['can_approve_content']:
+            show_all.append(category.pk)
+        else:
+            if user.is_authenticated:
+                show_approved_owned.append(category.pk)
+            else:
+                show_approved.append(category.pk)
+
+        if not category.acl['can_hide_events']:
+            hide_invisible_events.append(category.pk)
+
+    conditions = None
+    if show_all:
+        conditions = Q(category__in=show_all)
+
+    if show_approved:
+        condition = Q(
+            category__in=show_approved,
+            is_unapproved=False,
+        )
+
+        if conditions:
+            conditions = conditions | condition
+        else:
+            conditions = condition
+
+    if show_approved_owned:
+        condition = Q(
+            Q(poster=user) | Q(is_unapproved=False),
+            category__in=show_approved_owned,
+        )
+
+        if conditions:
+            conditions = conditions | condition
+        else:
+            conditions = condition
+
+    if hide_invisible_events:
+        queryset = queryset.exclude(
+            category__in=hide_invisible_events,
+            is_event=True,
+            is_hidden=True,
+        )
+
+    if conditions:
+        return queryset.filter(conditions)
+    else:
+        return Post.objects.none()
+
+
+def exclude_invisible_posts_in_category(user, category, queryset):
     if not category.acl['can_approve_content']:
     if not category.acl['can_approve_content']:
         if user.is_authenticated:
         if user.is_authenticated:
             queryset = queryset.filter(Q(is_unapproved=False) | Q(poster=user))
             queryset = queryset.filter(Q(is_unapproved=False) | Q(poster=user))

+ 1 - 1
misago/threads/viewmodels/posts.py

@@ -1,7 +1,7 @@
 from misago.acl import add_acl
 from misago.acl import add_acl
 from misago.conf import settings
 from misago.conf import settings
 from misago.core.shortcuts import paginate, pagination_dict
 from misago.core.shortcuts import paginate, pagination_dict
-from misago.readtracker.threadstracker import make_posts_read_aware
+from misago.readtracker.poststracker import make_posts_read_aware
 from misago.threads.paginator import PostsPaginator
 from misago.threads.paginator import PostsPaginator
 from misago.threads.permissions import exclude_invisible_posts
 from misago.threads.permissions import exclude_invisible_posts
 from misago.threads.serializers import PostSerializer
 from misago.threads.serializers import PostSerializer