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@148: creation_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