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