Browse Source

WIP #410: Read tracker

Rafał Pitoń 10 years ago
parent
commit
04c84415ee

+ 5 - 0
docs/developers/settings.rst

@@ -254,6 +254,11 @@ Some lists act as rankings, displaying users in order of certain scoring criteri
 This setting controls maximum age in days of items that should count to ranking.
 
 
+MISAGO_READ_RECORD_LENGTH
+-------------------------
+Controls amount of data used in resolving read/unread states of threads and forums. Any activity older than number of days specified in this setting is assumed to be read and not tracked anymore. Active forums can try lowering this value while less active ones may wish to increase this number.
+
+
 MISAGO_SENDFILE_HEADER
 ----------------------
 

+ 8 - 0
misago/conf/defaults.py

@@ -125,6 +125,7 @@ INSTALLED_APPS = (
     'misago.legal',
     'misago.forums',
     'misago.threads',
+    'misago.readtracker',
     'misago.faker',
 )
 
@@ -278,6 +279,13 @@ MISAGO_AVATAR_SERVER_PATH = '/user-avatar'
 MISAGO_RANKING_LENGTH = 30
 
 
+# Controls amount of data used in resolving read/unread states of threads and
+# forums. Any activity older than number of days below is assumed to be read
+# and not tracked anymore. Active forums can try lowering this value while
+# less active ones may wish to increase this number
+MISAGO_READ_RECORD_LENGTH = 28
+
+
 # X-Sendfile
 # X-Sendfile is feature provided by Http servers that allows web apps to
 # delegate serving files over to the better performing server instead of

+ 5 - 1
misago/forums/lists.py

@@ -1,4 +1,5 @@
 from misago.acl import add_acl
+from misago.readtracker import make_forums_read_aware
 
 from misago.forums.models import Forum
 
@@ -24,7 +25,6 @@ def get_forums_list(user, parent=None):
     parent_level = parent.level + 1 if parent else 1
 
     for forum in visible_forums:
-        forum.is_read = True
         forum.subforums = []
         forums_dict[forum.pk] = forum
         forums_list.append(forum)
@@ -33,6 +33,7 @@ def get_forums_list(user, parent=None):
             forums_dict[forum.parent_id].subforums.append(forum)
 
     add_acl(user, forums_list)
+    make_forums_read_aware(user, forums_list)
 
     for forum in reversed(visible_forums):
         if forum.acl['can_browse']:
@@ -58,6 +59,9 @@ def get_forums_list(user, parent=None):
                     forum_parent.last_poster_name = forum.last_poster_name
                     forum_parent.last_poster_slug = forum.last_poster_slug
 
+                if not forum.is_read:
+                    forum_parent.is_read = False
+
     flat_list = []
     for forum in forums_list:
         if forum.role != "category" or forum.subforums:

+ 6 - 0
misago/readtracker/__init__.py

@@ -0,0 +1,6 @@
+# flake8: noqa
+from misago.readtracker.forums import *
+from misago.readtracker.threads import *
+
+
+default_app_config = 'misago.readtracker.apps.MisagoReadTrackerConfig'

+ 7 - 0
misago/readtracker/apps.py

@@ -0,0 +1,7 @@
+from django.apps import AppConfig
+
+
+class MisagoReadTrackerConfig(AppConfig):
+    name = 'misago.readtracker'
+    label = 'misago_readtracker'
+    verbose_name = "Misago Read Tracker"

+ 15 - 0
misago/readtracker/dates.py

@@ -0,0 +1,15 @@
+from datetime import timedelta
+
+from django.conf import settings
+from django.utils import timezone
+
+
+def cutoff_date():
+    return timezone.now() - timedelta(days=settings.MISAGO_READ_RECORD_LENGTH)
+
+
+def is_date_tracked(date):
+    if date:
+        return date > cutoff_date()
+    else:
+        return False

+ 26 - 0
misago/readtracker/forums.py

@@ -0,0 +1,26 @@
+from misago.readtracker.dates import is_date_tracked
+
+
+__all__ = ['make_forums_read_aware', 'make_forums_read']
+
+
+def make_forums_read_aware(user, forums):
+    if user.is_anonymous():
+        make_forums_read(forums)
+        return None
+
+    forums_dict = {}
+    for forum in forums:
+        forum.is_read = not is_date_tracked(forum.last_post_on)
+        forums_dict[forum.pk] = forum
+
+    for record in user.forumread_set.filter(forum__in=forums_dict.keys()):
+        if record.forum_id in forums_dict:
+            forum = forums_dict[record.forum_id]
+            forum.is_read = record.last_cleared_on >= forum.last_post_on
+
+
+def make_forums_read(forums):
+    for forum in forums:
+        forum.is_read = True
+

+ 43 - 0
misago/readtracker/migrations/0001_initial.py

@@ -0,0 +1,43 @@
+# -*- coding: utf-8 -*-
+from __future__ import unicode_literals
+
+from django.db import models, migrations
+from django.conf import settings
+
+
+class Migration(migrations.Migration):
+
+    dependencies = [
+        migrations.swappable_dependency(settings.AUTH_USER_MODEL),
+        ('misago_threads', '0001_initial'),
+    ]
+
+    operations = [
+        migrations.CreateModel(
+            name='ForumRead',
+            fields=[
+                ('id', models.AutoField(verbose_name='ID', serialize=False, auto_created=True, primary_key=True)),
+                ('last_updated_on', models.DateTimeField()),
+                ('last_cleared_on', models.DateTimeField()),
+                ('forum', models.ForeignKey(to='misago_forums.Forum')),
+                ('user', models.ForeignKey(to=settings.AUTH_USER_MODEL)),
+            ],
+            options={
+            },
+            bases=(models.Model,),
+        ),
+        migrations.CreateModel(
+            name='ThreadRead',
+            fields=[
+                ('id', models.AutoField(verbose_name='ID', serialize=False, auto_created=True, primary_key=True)),
+                ('read_replies', models.PositiveIntegerField(default=0)),
+                ('last_read_on', models.DateTimeField()),
+                ('forum', models.ForeignKey(to='misago_forums.Forum')),
+                ('thread', models.ForeignKey(to='misago_threads.Thread')),
+                ('user', models.ForeignKey(to=settings.AUTH_USER_MODEL)),
+            ],
+            options={
+            },
+            bases=(models.Model,),
+        ),
+    ]

+ 0 - 0
misago/readtracker/migrations/__init__.py


+ 20 - 0
misago/readtracker/models.py

@@ -0,0 +1,20 @@
+from datetime import timedelta
+
+from django.conf import settings
+from django.db import models
+from django.utils import timezone
+
+
+class ForumRead(models.Model):
+    user = models.ForeignKey(settings.AUTH_USER_MODEL)
+    forum = models.ForeignKey('misago_forums.Forum')
+    last_updated_on = models.DateTimeField()
+    last_cleared_on = models.DateTimeField()
+
+
+class ThreadRead(models.Model):
+    user = models.ForeignKey(settings.AUTH_USER_MODEL)
+    forum = models.ForeignKey('misago_forums.Forum')
+    thread = models.ForeignKey('misago_threads.Thread')
+    read_replies =  models.PositiveIntegerField(default=0)
+    last_read_on = models.DateTimeField()

+ 35 - 0
misago/readtracker/threads.py

@@ -0,0 +1,35 @@
+from misago.readtracker.dates import is_date_tracked
+
+
+__all__ = ['make_threads_read_aware', 'make_threads_read']
+
+
+def make_threads_read_aware(user, threads):
+    if user.is_anonymous():
+        make_threads_read(threads)
+        return None
+
+    threads_dict = {}
+    for thread in threads:
+        thread.is_read = not is_date_tracked(thread.last_post_on)
+        if thread.is_read:
+            thread.unread_posts = 0
+        else:
+            thread.unread_posts = thread.replies
+        threads_dict[thread.pk] = thread
+
+    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
+            if thread.is_read:
+                thread.unread_posts = 0
+            else:
+                thread.unread_posts = thread.replies - record.read_replies
+
+
+def make_threads_read(threads):
+    for thread in threads:
+        thread.unread_posts = 0
+        thread.is_read = True
+

+ 2 - 2
misago/templates/misago/forums/forums.html

@@ -3,9 +3,9 @@
   {% for forum in forums %}
   <li class="list-group-item">
     {% if forum.role == 'redirect' %}
-    <span class="forum-icon fa fa-link fa-fw"></span>
+    <span class="forum-icon fa fa-link fa-fw tooltip-top" title="{% trans "Redirect forum" %}"></span>
     {% else %}
-    <span class="forum-icon fa fa-comment{% if forum.is_read %}-o{% else %} new{% endif %} fa-fw"></span>
+    <span class="forum-icon fa fa-comment{% if forum.is_read %}-o{% else %} new{% endif %} fa-fw tooltip-top" title="{% if forum.is_read %}{% trans "Forum has no new posts." %}{% else %}{% trans "Forum has new posts." %}{% endif %}"></span>
     {% endif %}
 
     <a href="{{ forum.get_absolute_url }}" class="item-title forum-name">{{ forum.name }}</a>

+ 10 - 10
misago/templates/misago/threads/base.html

@@ -9,27 +9,27 @@
     <div class="table-panel">
       <ul class="list-group">
         {% for thread in threads %}
-        <li class="list-group-item{% if thread.is_new %} new{% endif %}">
+        <li class="list-group-item{% if not thread.is_read %} new{% endif %}">
           <div class="row">
 
             <div class="col-md-8">
               {% if thread.is_announcement %}
-                {% if thread.is_new %}
-                <span class="thread-icon tooltip-top fa fa-star fa-lg fa-fw" title="{% trans "Announcement, unread" %}"></span>
+                {% if thread.is_read %}
+                <span class="thread-icon tooltip-top fa fa-star-o fa-lg fa-fw" title="{% trans "Announcement, has no unread posts" %}"></span>
                 {% else %}
-                <span class="thread-icon tooltip-top fa fa-star-o fa-lg fa-fw" title="{% trans "Announcement, read" %}"></span>
+                <span class="thread-icon tooltip-top fa fa-star fa-lg fa-fw" title="{% trans "Announcement, has unread posts" %}"></span>
                 {% endif %}
               {% elif thread.is_pinned %}
-                {% if thread.is_new %}
-                <span class="thread-icon tooltip-top fa fa-bookmark fa-lg fa-fw" title="{% trans "Pinned, unread" %}"></span>
+                {% if thread.is_read %}
+                <span class="thread-icon tooltip-top fa fa-bookmark-o fa-lg fa-fw" title="{% trans "Pinned, has no unread posts" %}"></span>
                 {% else %}
-                <span class="thread-icon tooltip-top fa fa-bookmark-o fa-lg fa-fw" title="{% trans "Pinned, read" %}"></span>
+                <span class="thread-icon tooltip-top fa fa-bookmark fa-lg fa-fw" title="{% trans "Pinned, has unread posts" %}"></span>
                 {% endif %}
               {% else %}
-                {% if thread.is_new %}
-                <span class="thread-icon tooltip-top fa fa-circle fa-lg fa-fw" title="{% trans "Unread posts" %}"></span>
+                {% if thread.is_read %}
+                <span class="thread-icon tooltip-top fa fa-circle-thin fa-lg fa-fw" title="{% trans "Thread has no unread posts" %}"></span>
                 {% else %}
-                <span class="thread-icon tooltip-top fa fa-circle-thin fa-lg fa-fw" title="{% trans "Read posts" %}"></span>
+                <span class="thread-icon tooltip-top fa fa-circle fa-lg fa-fw" title="{% trans "Thread has unread posts" %}"></span>
                 {% endif %}
               {% endif %}
 

+ 1 - 1
misago/threads/views/generic/forum.py

@@ -135,6 +135,7 @@ class ForumView(FilterThreadsMixin, OrderThreadsMixin, ThreadsView):
             thread.forum = forum
 
         self.label_threads(threads, forum.labels)
+        self.make_threads_read_aware(request.user, threads)
 
         return page, threads
 
@@ -243,7 +244,6 @@ class ForumView(FilterThreadsMixin, OrderThreadsMixin, ThreadsView):
 
         page, threads = self.get_threads(
             request, forum, kwargs, order_by, filter_by)
-        self.add_threads_reads(request, threads)
 
         return self.render(request, {
             'forum': forum,

+ 3 - 7
misago/threads/views/generic/threads.py

@@ -4,6 +4,7 @@ from django.shortcuts import redirect
 from django.utils.translation import ugettext_lazy, ugettext as _
 
 from misago.core.shortcuts import paginate
+from misago.readtracker import make_threads_read_aware
 
 from misago.threads.views.generic.base import ViewBase
 
@@ -67,10 +68,5 @@ class ThreadsView(ViewBase):
     def get_threads_queryset(self, request):
         return forum.thread_set.all().order_by('-last_post_id')
 
-    def add_threads_reads(self, request, threads):
-        for thread in threads:
-            thread.is_new = False
-
-        import random
-        for thread in threads:
-            thread.is_new = random.choice((True, False))
+    def make_threads_read_aware(self, user, threads):
+        make_threads_read_aware(user, threads)