Browse Source

#477: count unread private threads for UI

Rafał Pitoń 10 years ago
parent
commit
b93e3431cd

+ 8 - 0
misago/readtracker/signals.py

@@ -38,6 +38,14 @@ def decrease_unread_count(sender, **kwargs):
     user.unread_threads.decrease()
     user.unread_threads.decrease()
 
 
 
 
+@receiver(thread_read)
+def decrease_unread_private_count(sender, **kwargs):
+    user = sender
+    if user.unread_private_threads:
+        user.unread_private_threads -= 1
+        user.save(update_fields=['unread_private_threads'])
+
+
 @receiver(all_read)
 @receiver(all_read)
 def zero_unread_counters(sender, **kwargs):
 def zero_unread_counters(sender, **kwargs):
     sender.new_threads.set(0)
     sender.new_threads.set(0)

+ 7 - 3
misago/templates/misago/user_nav.html

@@ -64,7 +64,11 @@
   </li>
   </li>
   {% if user.acl.moderated_forums %}
   {% if user.acl.moderated_forums %}
   <li>
   <li>
-    <a href="{% url 'misago:moderated_content' %}" class="tooltip-bottom" title="{% trans "Moderated content" %}">
+    <a href="{% url 'misago:moderated_content' %}" class="tooltip-bottom" {% if user.moderated_content %}
+        title="{% blocktrans with moderated=user.moderated_content count counter=user.moderated_content %}{{ moderated }} item in moderation{% plural %}{{ moderated }} items in moderation{% endblocktrans %}"
+        {% else %}
+        title="{% trans "Moderated content" %}"
+        {% endif %}>
       <span class="fa fa-eye-slash fa-fw"></span>
       <span class="fa fa-eye-slash fa-fw"></span>
       <span class="badge fade {{ user.moderated_content|iftrue:"in" }}" data-misago-badge="moderated_content">{{ user.moderated_content }}</span>
       <span class="badge fade {{ user.moderated_content|iftrue:"in" }}" data-misago-badge="moderated_content">{{ user.moderated_content }}</span>
     </a>
     </a>
@@ -72,7 +76,7 @@
   {% endif %}
   {% endif %}
   <li>
   <li>
     <a href="{% url 'misago:private_threads' %}" class="tooltip-bottom" {% if user.unread_private_threads %}
     <a href="{% url 'misago:private_threads' %}" class="tooltip-bottom" {% if user.unread_private_threads %}
-        title="{% blocktrans with unread=user.unread_private_threads count counter=user.unread_private_threads %}{{ unread }} unread private threads{% plural %}{{ unread }} unread private thread{% endblocktrans %}"
+        title="{% blocktrans with unread=user.unread_private_threads count counter=user.unread_private_threads %}{{ unread }} unread private thread{% plural %}{{ unread }} unread private threads{% endblocktrans %}"
         {% else %}
         {% else %}
         title="{% trans "Private threads" %}"
         title="{% trans "Private threads" %}"
         {% endif %}>
         {% endif %}>
@@ -85,7 +89,7 @@
         {% if user.new_notifications %}
         {% if user.new_notifications %}
         title="{% blocktrans with notifications=user.new_notifications count counter=user.new_notifications %}{{ notifications }} new notification{% plural %}{{ notifications }} new notifications{% endblocktrans %}"
         title="{% blocktrans with notifications=user.new_notifications count counter=user.new_notifications %}{{ notifications }} new notification{% plural %}{{ notifications }} new notifications{% endblocktrans %}"
         {% else %}
         {% else %}
-        title="{% trans "Your notifications" %}"
+        title="{% trans "Notifications" %}"
         {% endif %}
         {% endif %}
         data-toggle="dropdown">
         data-toggle="dropdown">
       <span class="fa fa-bell-o fa-fw"></span>
       <span class="fa fa-bell-o fa-fw"></span>

+ 31 - 3
misago/threads/counts.py

@@ -1,11 +1,13 @@
 from time import time
 from time import time
 
 
 from django.conf import settings
 from django.conf import settings
+from django.db.models import F
 from django.dispatch import receiver
 from django.dispatch import receiver
 
 
 from misago.threads.views.moderatedcontent import ModeratedContent
 from misago.threads.views.moderatedcontent import ModeratedContent
 from misago.threads.views.newthreads import NewThreads
 from misago.threads.views.newthreads import NewThreads
 from misago.threads.views.unreadthreads import UnreadThreads
 from misago.threads.views.unreadthreads import UnreadThreads
+from misago.threads.views.privatethreads import PrivateThreads
 
 
 
 
 class BaseCounter(object):
 class BaseCounter(object):
@@ -30,7 +32,7 @@ class BaseCounter(object):
         count = self.session.get(self.name, None)
         count = self.session.get(self.name, None)
 
 
         if not count or not self.is_cache_valid(count):
         if not count or not self.is_cache_valid(count):
-            count = self.get_real_count()
+            count = self.get_current_count_dict()
             self.session[self.name] = count
             self.session[self.name] = count
         return count['threads']
         return count['threads']
 
 
@@ -43,13 +45,16 @@ class BaseCounter(object):
     def get_expiration_timestamp(self):
     def get_expiration_timestamp(self):
         return time() + settings.MISAGO_CONTENT_COUNTING_FREQUENCY * 60
         return time() + settings.MISAGO_CONTENT_COUNTING_FREQUENCY * 60
 
 
-    def get_real_count(self):
+    def get_current_count_dict(self):
         return {
         return {
             'user': self.user.pk,
             'user': self.user.pk,
-            'threads': self.Threads(self.user).get_queryset().count(),
+            'threads': self.count_threads(),
             'expires': self.get_expiration_timestamp()
             'expires': self.get_expiration_timestamp()
         }
         }
 
 
+    def count_threads(self):
+        return self.Threads(self.user).get_queryset().count()
+
     def set(self, count):
     def set(self, count):
         self.count = count
         self.count = count
         self.session[self.name] = {
         self.session[self.name] = {
@@ -81,3 +86,26 @@ class NewThreadsCount(BaseCounter):
 class UnreadThreadsCount(BaseCounter):
 class UnreadThreadsCount(BaseCounter):
     Threads = UnreadThreads
     Threads = UnreadThreads
     name = 'unread_threads'
     name = 'unread_threads'
+
+
+def sync_user_unread_private_threads_count(user):
+    if not user.sync_unread_private_threads:
+        return
+
+    threads_qs = PrivateThreads(user).get_queryset()
+
+    all_threads_count = threads_qs.count()
+
+    read_qs = threads_qs.filter(threadread__user=user)
+    read_qs = read_qs.filter(threadread__last_read_on__gte=F('last_post_on'))
+    read_threads_count = read_qs.count()
+
+    user.unread_private_threads = all_threads_count - read_threads_count
+    if user.unread_private_threads < 0:
+        # we may end with negative count because of race conditions in counts
+        user.unread_private_threads = 0
+
+    user.sync_unread_private_threads = False
+    user.save(update_fields=[
+        'unread_private_threads', 'sync_unread_private_threads'
+    ])

+ 5 - 1
misago/threads/middleware.py

@@ -3,7 +3,8 @@ from time import time
 from django.conf import settings
 from django.conf import settings
 
 
 from misago.threads.counts import (ModeratedCount, NewThreadsCount,
 from misago.threads.counts import (ModeratedCount, NewThreadsCount,
-                                   UnreadThreadsCount)
+                                   UnreadThreadsCount,
+                                   sync_user_unread_private_threads_count)
 
 
 
 
 class UnreadThreadsCountMiddleware(object):
 class UnreadThreadsCountMiddleware(object):
@@ -17,3 +18,6 @@ class UnreadThreadsCountMiddleware(object):
             request.user.unread_threads = UnreadThreadsCount(
             request.user.unread_threads = UnreadThreadsCount(
                 request.user, request.session)
                 request.user, request.session)
 
 
+            if request.user.acl['can_use_private_threads']:
+                # special case: count unread threads
+                sync_user_unread_private_threads_count(request.user)

+ 62 - 6
misago/threads/tests/test_counters.py

@@ -1,12 +1,16 @@
+from datetime import timedelta
 from time import time
 from time import time
 
 
 from django.core.urlresolvers import reverse
 from django.core.urlresolvers import reverse
+from django.utils import timezone
 from django.utils.translation import ugettext as _
 from django.utils.translation import ugettext as _
 
 
 from misago.forums.models import Forum
 from misago.forums.models import Forum
+from misago.readtracker.models import ThreadRead
 from misago.users.testutils import AuthenticatedUserTestCase
 from misago.users.testutils import AuthenticatedUserTestCase
 
 
-from misago.threads.counts import NewThreadsCount, UnreadThreadsCount
+from misago.threads.counts import (NewThreadsCount,
+                                   sync_user_unread_private_threads_count)
 from misago.threads import testutils
 from misago.threads import testutils
 
 
 
 
@@ -58,20 +62,20 @@ class TestNewThreadsCount(AuthenticatedUserTestCase):
         counter = NewThreadsCount(self.user, {})
         counter = NewThreadsCount(self.user, {})
         self.assertTrue(counter.get_expiration_timestamp() > time())
         self.assertTrue(counter.get_expiration_timestamp() > time())
 
 
-    def test_get_real_count(self):
-        """get_real_count returns valid count of new threads"""
+    def test_get_current_count_dict(self):
+        """get_current_count_dict returns valid count of new threads"""
         counter = NewThreadsCount(self.user, {})
         counter = NewThreadsCount(self.user, {})
         self.assertEqual(counter.count, 0)
         self.assertEqual(counter.count, 0)
-        self.assertEqual(counter.get_real_count()['threads'], 0)
+        self.assertEqual(counter.get_current_count_dict()['threads'], 0)
 
 
         # create 10 new threads
         # create 10 new threads
         threads = [testutils.post_thread(self.forum) for t in xrange(10)]
         threads = [testutils.post_thread(self.forum) for t in xrange(10)]
-        self.assertEqual(counter.get_real_count()['threads'], 10)
+        self.assertEqual(counter.get_current_count_dict()['threads'], 10)
 
 
         # create new counter
         # create new counter
         counter = NewThreadsCount(self.user, {})
         counter = NewThreadsCount(self.user, {})
         self.assertEqual(counter.count, 10)
         self.assertEqual(counter.count, 10)
-        self.assertEqual(counter.get_real_count()['threads'], 10)
+        self.assertEqual(counter.get_current_count_dict()['threads'], 10)
 
 
     def test_set(self):
     def test_set(self):
         """set allows for changing count of threads"""
         """set allows for changing count of threads"""
@@ -100,3 +104,55 @@ class TestNewThreadsCount(AuthenticatedUserTestCase):
 
 
         self.assertEqual(int(counter), 0)
         self.assertEqual(int(counter), 0)
         self.assertEqual(session[counter.name]['threads'], 0)
         self.assertEqual(session[counter.name]['threads'], 0)
+
+
+class TestSyncUnreadPrivateThreadsCount(AuthenticatedUserTestCase):
+    def setUp(self):
+        super(TestSyncUnreadPrivateThreadsCount, self).setUp()
+
+        self.forum = Forum.objects.private_threads()
+        self.user.sync_unread_private_threads = True
+
+    def test_user_with_no_threads(self):
+        """user with no private threads has 0 unread threads"""
+        for i in range(5):
+            # post 5 invisible threads
+            testutils.post_thread(
+                self.forum, started_on=timezone.now() - timedelta(days=2))
+
+        sync_user_unread_private_threads_count(self.user)
+        self.assertEqual(self.user.unread_private_threads, 0)
+
+    def test_user_with_new_thread(self):
+        """user has one new private thred"""
+        for i in range(5):
+            # post 5 invisible threads
+            testutils.post_thread(
+                self.forum, started_on=timezone.now() - timedelta(days=2))
+
+        thread = testutils.post_thread(
+            self.forum, started_on=timezone.now() - timedelta(days=2))
+        thread.threadparticipant_set.create(user=self.user)
+
+        sync_user_unread_private_threads_count(self.user)
+        self.assertEqual(self.user.unread_private_threads, 1)
+
+    def test_user_with_new_thread(self):
+        """user has one unread private thred"""
+        for i in range(5):
+            # post 5 invisible threads
+            testutils.post_thread(
+                self.forum, started_on=timezone.now() - timedelta(days=2))
+
+        thread = testutils.post_thread(
+            self.forum, started_on=timezone.now() - timedelta(days=2))
+        thread.threadparticipant_set.create(user=self.user)
+
+        ThreadRead.objects.create(
+            user=self.user,
+            forum=self.forum,
+            thread=thread,
+            last_read_on=timezone.now() - timedelta(days=3))
+
+        sync_user_unread_private_threads_count(self.user)
+        self.assertEqual(self.user.unread_private_threads, 1)