view forums/unread.py @ 839:36d988b800c2

Issue #74, fix bug in 234726f.
author Brian Neal <bgneal@gmail.com>
date Sat, 04 Oct 2014 17:06:56 -0500
parents ee87ea74d46b
children
line wrap: on
line source
"""
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