diff forums/unread.py @ 581:ee87ea74d46b

For Django 1.4, rearranged project structure for new manage.py.
author Brian Neal <bgneal@gmail.com>
date Sat, 05 May 2012 17:10:48 -0500
parents gpp/forums/unread.py@c06d836c8b84
children
line wrap: on
line diff
--- /dev/null	Thu Jan 01 00:00:00 1970 +0000
+++ b/forums/unread.py	Sat May 05 17:10:48 2012 -0500
@@ -0,0 +1,257 @@
+"""
+This file contains routines for implementing the "has unread" feature.
+Forums, topics, and posts are displayed with a visual indication if they have
+been read or not.
+"""
+import datetime
+import logging
+
+from django.db import IntegrityError
+
+from forums.models import ForumLastVisit, TopicLastVisit, Topic, Forum
+
+
+THRESHOLD = datetime.timedelta(days=14)
+
+#######################################################################
+
+def get_forum_unread_status(qs, user):
+    if not user.is_authenticated():
+        for forum in qs:
+            forum.has_unread = False
+        return
+
+    now = datetime.datetime.now()
+    min_date = now - THRESHOLD
+
+    # retrieve ForumLastVisit records in one SQL query
+    forum_ids = [forum.id for forum in qs]
+    flvs = ForumLastVisit.objects.filter(user=user,
+            forum__in=forum_ids).select_related()
+    flvs = dict([(flv.forum.id, flv) for flv in flvs])
+
+    for forum in qs:
+        # Edge case: forum has no posts
+        if forum.last_post is None:
+            forum.has_unread = False
+            continue
+
+        # Get the ForumLastVisit record
+        if forum.id in flvs:
+            flv = flvs[forum.id]
+        else:
+            # One doesn't exist, create a default one for next time,
+            # mark it as having no unread topics, and bail.
+            flv = ForumLastVisit(user=user, forum=forum)
+            flv.begin_date = now
+            flv.end_date = now
+
+            # There is a race condition and sometimes another thread
+            # saves a record before we do; just log this if it happens.
+            try:
+                flv.save()
+            except IntegrityError:
+                logging.exception('get_forum_unread_status')
+
+            forum.has_unread = False
+            continue
+
+        # If the last visit record was too far in the past,
+        # catch that user up and mark as no unreads.
+        if now - flv.end_date > THRESHOLD:
+            forum.catchup(user, flv)
+            forum.has_unread = False
+            continue
+
+        # Check the easy cases first. Check the last_post in the
+        # forum. If created after the end_date in our window, there
+        # are new posts. Likewise, if before the begin_date in our window,
+        # there are no new posts.
+        if forum.last_post.creation_date > flv.end_date:
+            forum.has_unread = True
+        elif forum.last_post.creation_date < flv.begin_date:
+            if not flv.is_caught_up():
+                forum.catchup(user, flv)
+            forum.has_unread = False
+        else:
+            # Going to have to examine the topics in our window.
+            # First adjust our window if it is too old.
+            if now - flv.begin_date > THRESHOLD:
+                flv.begin_date = min_date
+                flv.save()
+                TopicLastVisit.objects.filter(user=user, topic__forum=forum,
+                        last_visit__lt=min_date).delete()
+
+            topics = Topic.objects.filter(forum=forum,
+                    update_date__gt=flv.begin_date)
+            tracked_topics = TopicLastVisit.objects.filter(
+                    user=user,
+                    topic__forum=forum,
+                    last_visit__gt=flv.begin_date).select_related('topic')
+
+            # If the number of topics created since our window was started 
+            # is greater than the tracked topic records, then there are new
+            # posts.
+            if topics.count() > tracked_topics.count():
+                forum.has_unread = True
+                continue
+
+            tracked_dict = dict((t.topic.id, t) for t in tracked_topics)
+
+            for topic in topics:
+                if topic.id in tracked_dict:
+                    if topic.update_date > tracked_dict[topic.id].last_visit:
+                        forum.has_unread = True
+                        break
+                else:
+                    forum.has_unread = True
+                    break
+            else:
+                # If we made it through the above loop without breaking out,
+                # then we are all caught up.
+                forum.catchup(user, flv)
+                forum.has_unread = False
+
+#######################################################################
+
+def get_topic_unread_status(forum, topics, user):
+
+    # Edge case: no topics 
+    if forum.last_post is None:
+        return
+
+    # This service isn't provided to unauthenticated users
+    if not user.is_authenticated():
+        for topic in topics:
+            topic.has_unread = False
+        return
+
+    now = datetime.datetime.now()
+
+    # Get the ForumLastVisit record
+    try:
+        flv = ForumLastVisit.objects.get(forum=forum, user=user)
+    except ForumLastVisit.DoesNotExist:
+        # One doesn't exist, create a default one for next time,
+        # mark it as having no unread topics, and bail.
+        flv = ForumLastVisit(user=user, forum=forum)
+        flv.begin_date = now
+        flv.end_date = now
+
+        # There is a race condition and sometimes another thread
+        # saves a record before we do; just log this if it happens.
+        try:
+            flv.save()
+        except IntegrityError:
+            logging.exception('get_topic_unread_status')
+
+        for topic in topics:
+            topic.has_unread = False
+        return
+
+    # Are all the posts before our window? If so, all have been read.
+    if forum.last_post.creation_date < flv.begin_date:
+        for topic in topics:
+            topic.has_unread = False
+        return
+
+    topic_ids = [topic.id for topic in topics]
+    tlvs = TopicLastVisit.objects.filter(user=user, topic__id__in=topic_ids)
+    tlvs = dict([(tlv.topic.id, tlv) for tlv in tlvs])
+
+    # Otherwise we have to go through the topics one by one:
+    for topic in topics:
+        if topic.update_date < flv.begin_date:
+            topic.has_unread = False
+        elif topic.update_date > flv.end_date:
+            topic.has_unread = True
+        elif topic.id in tlvs:
+            topic.has_unread = topic.update_date > tlvs[topic.id].last_visit
+        else:
+            topic.has_unread = True
+
+#######################################################################
+
+def get_post_unread_status(topic, posts, user):
+    # This service isn't provided to unauthenticated users
+    if not user.is_authenticated():
+        for post in posts:
+            post.unread = False
+        return
+
+    # Get the ForumLastVisit record
+    try:
+        flv = ForumLastVisit.objects.get(forum=topic.forum, user=user)
+    except ForumLastVisit.DoesNotExist:
+        # One doesn't exist, all posts are old.
+        for post in posts:
+            post.unread = False
+        return
+
+    # Are all the posts before our window? If so, all have been read.
+    if topic.last_post.creation_date < flv.begin_date:
+        for post in posts:
+            post.unread = False
+        return
+
+    # Do we have a topic last visit record for this topic?
+
+    try:
+        tlv = TopicLastVisit.objects.get(user=user, topic=topic)
+    except TopicLastVisit.DoesNotExist:
+        # No we don't, we could be all caught up, or all are new
+        for post in posts:
+            post.unread = post.creation_date > flv.end_date
+    else:
+        for post in posts:
+            post.unread = post.creation_date > tlv.last_visit
+
+#######################################################################
+
+def get_unread_topics(user):
+    """Returns a list of topics the user hasn't read yet."""
+
+    # This is only available to authenticated users
+    if not user.is_authenticated():
+        return []
+
+    now = datetime.datetime.now()
+
+    # Obtain list of forums the user can view
+    forums = Forum.objects.forums_for_user(user)
+
+    # Get forum last visit records for the forum ids
+    flvs = ForumLastVisit.objects.filter(user=user,
+            forum__in=forums).select_related()
+    flvs = dict([(flv.forum.id, flv) for flv in flvs])
+
+    unread_topics = []
+    topics = Topic.objects.none()
+    for forum in forums:
+        # if the user hasn't visited the forum, create a last
+        # visit record set to "now"
+        if not forum.id in flvs:
+            flv = ForumLastVisit(user=user, forum=forum, begin_date=now,
+                    end_date=now)
+            flv.save()
+        else:
+            flv = flvs[forum.id]
+            topics |= Topic.objects.filter(forum=forum,
+                update_date__gt=flv.begin_date).order_by('-update_date').select_related(
+                    'forum', 'user', 'last_post', 'last_post__user')
+
+    if topics is not None:
+        # get all topic last visit records for the topics of interest
+
+        tlvs = TopicLastVisit.objects.filter(user=user, topic__in=topics)
+        tlvs = dict([(tlv.topic.id, tlv) for tlv in tlvs])
+
+        for topic in topics:
+            if topic.id in tlvs:
+                tlv = tlvs[topic.id]
+                if topic.update_date > tlv.last_visit:
+                    unread_topics.append(topic)
+            else:
+                unread_topics.append(topic)
+
+    return unread_topics