annotate gpp/forums/unread.py @ 407:42a4e66972d5

Trying to fix #200; found a bug in the get_forum_unread_status() function. Added some db indexes.
author Brian Neal <bgneal@gmail.com>
date Fri, 01 Apr 2011 01:18:44 +0000
parents e9a066db3f54
children 7e0997b08b50
rev   line source
bgneal@113 1 """
bgneal@307 2 This file contains routines for implementing the "has unread" feature.
bgneal@113 3 Forums, topics, and posts are displayed with a visual indication if they have
bgneal@113 4 been read or not.
bgneal@113 5 """
bgneal@113 6 import datetime
bgneal@370 7 import logging
bgneal@113 8
bgneal@370 9 from django.db import IntegrityError
bgneal@167 10 from django.db.models import Q
bgneal@167 11
bgneal@167 12 from forums.models import ForumLastVisit, TopicLastVisit, Topic, Forum
bgneal@113 13
bgneal@113 14
bgneal@307 15 THRESHOLD = datetime.timedelta(days=14)
bgneal@113 16
bgneal@114 17 #######################################################################
bgneal@113 18
bgneal@113 19 def get_forum_unread_status(qs, user):
bgneal@113 20 if not user.is_authenticated():
bgneal@113 21 for forum in qs:
bgneal@113 22 forum.has_unread = False
bgneal@113 23 return
bgneal@113 24
bgneal@113 25 now = datetime.datetime.now()
bgneal@113 26 min_date = now - THRESHOLD
bgneal@113 27
bgneal@113 28 # retrieve ForumLastVisit records in one SQL query
bgneal@113 29 forum_ids = [forum.id for forum in qs]
bgneal@307 30 flvs = ForumLastVisit.objects.filter(user=user,
bgneal@113 31 forum__in=forum_ids).select_related()
bgneal@113 32 flvs = dict([(flv.forum.id, flv) for flv in flvs])
bgneal@113 33
bgneal@113 34 for forum in qs:
bgneal@113 35 # Edge case: forum has no posts
bgneal@113 36 if forum.last_post is None:
bgneal@113 37 forum.has_unread = False
bgneal@113 38 continue
bgneal@113 39
bgneal@113 40 # Get the ForumLastVisit record
bgneal@113 41 if forum.id in flvs:
bgneal@113 42 flv = flvs[forum.id]
bgneal@113 43 else:
bgneal@113 44 # One doesn't exist, create a default one for next time,
bgneal@113 45 # mark it as having no unread topics, and bail.
bgneal@113 46 flv = ForumLastVisit(user=user, forum=forum)
bgneal@113 47 flv.begin_date = now
bgneal@113 48 flv.end_date = now
bgneal@370 49
bgneal@370 50 # There is a race condition and sometimes another thread
bgneal@370 51 # saves a record before we do; just log this if it happens.
bgneal@370 52 try:
bgneal@370 53 flv.save()
bgneal@370 54 except IntegrityError:
bgneal@370 55 logging.exception('get_forum_unread_status')
bgneal@370 56
bgneal@113 57 forum.has_unread = False
bgneal@113 58 continue
bgneal@113 59
bgneal@113 60 # If the last visit record was too far in the past,
bgneal@113 61 # catch that user up and mark as no unreads.
bgneal@113 62 if now - flv.end_date > THRESHOLD:
bgneal@113 63 forum.catchup(user, flv)
bgneal@113 64 forum.has_unread = False
bgneal@113 65 continue
bgneal@113 66
bgneal@113 67 # Check the easy cases first. Check the last_post in the
bgneal@113 68 # forum. If created after the end_date in our window, there
bgneal@113 69 # are new posts. Likewise, if before the begin_date in our window,
bgneal@113 70 # there are no new posts.
bgneal@113 71 if forum.last_post.creation_date > flv.end_date:
bgneal@113 72 forum.has_unread = True
bgneal@113 73 elif forum.last_post.creation_date < flv.begin_date:
bgneal@113 74 if not flv.is_caught_up():
bgneal@113 75 forum.catchup(user, flv)
bgneal@113 76 forum.has_unread = False
bgneal@113 77 else:
bgneal@113 78 # Going to have to examine the topics in our window.
bgneal@113 79 # First adjust our window if it is too old.
bgneal@113 80 if now - flv.begin_date > THRESHOLD:
bgneal@113 81 flv.begin_date = min_date
bgneal@113 82 flv.save()
bgneal@113 83 TopicLastVisit.objects.filter(user=user, topic__forum=forum,
bgneal@113 84 last_visit__lt=min_date).delete()
bgneal@113 85
bgneal@307 86 topics = Topic.objects.filter(forum=forum,
bgneal@148 87 creation_date__gt=flv.begin_date)
bgneal@113 88 tracked_topics = TopicLastVisit.objects.filter(user=user,
bgneal@113 89 topic__forum=forum, last_visit__gt=flv.begin_date)
bgneal@113 90
bgneal@113 91 # If the number of topics created since our window was started
bgneal@113 92 # is greater than the tracked topic records, then there are new
bgneal@113 93 # posts.
bgneal@113 94 if topics.count() > tracked_topics.count():
bgneal@113 95 forum.has_unread = True
bgneal@113 96 continue
bgneal@113 97
bgneal@113 98 tracked_dict = dict([(t.id, t) for t in tracked_topics])
bgneal@113 99
bgneal@113 100 for topic in topics:
bgneal@113 101 if topic.id in tracked_dict:
bgneal@113 102 if topic.update_date > tracked_dict[topic.id].last_visit:
bgneal@113 103 forum.has_unread = True
bgneal@407 104 break
bgneal@113 105 else:
bgneal@113 106 forum.has_unread = True
bgneal@407 107 break
bgneal@407 108 else:
bgneal@407 109 # If we made it through the above loop without breaking out,
bgneal@407 110 # then we are all caught up.
bgneal@407 111 forum.catchup(user, flv)
bgneal@407 112 forum.has_unread = False
bgneal@114 113
bgneal@114 114 #######################################################################
bgneal@114 115
bgneal@114 116 def get_topic_unread_status(forum, topics, user):
bgneal@114 117
bgneal@114 118 # Edge case: no topics
bgneal@114 119 if forum.last_post is None:
bgneal@114 120 return
bgneal@114 121
bgneal@114 122 # This service isn't provided to unauthenticated users
bgneal@114 123 if not user.is_authenticated():
bgneal@114 124 for topic in topics:
bgneal@114 125 topic.has_unread = False
bgneal@114 126 return
bgneal@114 127
bgneal@114 128 now = datetime.datetime.now()
bgneal@114 129
bgneal@114 130 # Get the ForumLastVisit record
bgneal@114 131 try:
bgneal@114 132 flv = ForumLastVisit.objects.get(forum=forum, user=user)
bgneal@114 133 except ForumLastVisit.DoesNotExist:
bgneal@114 134 # One doesn't exist, create a default one for next time,
bgneal@114 135 # mark it as having no unread topics, and bail.
bgneal@114 136 flv = ForumLastVisit(user=user, forum=forum)
bgneal@114 137 flv.begin_date = now
bgneal@114 138 flv.end_date = now
bgneal@370 139
bgneal@370 140 # There is a race condition and sometimes another thread
bgneal@370 141 # saves a record before we do; just log this if it happens.
bgneal@370 142 try:
bgneal@370 143 flv.save()
bgneal@370 144 except IntegrityError:
bgneal@370 145 logging.exception('get_topic_unread_status')
bgneal@370 146
bgneal@114 147 for topic in topics:
bgneal@114 148 topic.has_unread = False
bgneal@114 149 return
bgneal@114 150
bgneal@114 151 # Are all the posts before our window? If so, all have been read.
bgneal@114 152 if forum.last_post.creation_date < flv.begin_date:
bgneal@114 153 for topic in topics:
bgneal@114 154 topic.has_unread = False
bgneal@114 155 return
bgneal@114 156
bgneal@114 157 topic_ids = [topic.id for topic in topics]
bgneal@114 158 tlvs = TopicLastVisit.objects.filter(user=user, topic__id__in=topic_ids)
bgneal@114 159 tlvs = dict([(tlv.topic.id, tlv) for tlv in tlvs])
bgneal@114 160
bgneal@114 161 # Otherwise we have to go through the topics one by one:
bgneal@114 162 for topic in topics:
bgneal@114 163 if topic.update_date < flv.begin_date:
bgneal@114 164 topic.has_unread = False
bgneal@114 165 elif topic.update_date > flv.end_date:
bgneal@114 166 topic.has_unread = True
bgneal@114 167 elif topic.id in tlvs:
bgneal@114 168 topic.has_unread = topic.update_date > tlvs[topic.id].last_visit
bgneal@114 169 else:
bgneal@114 170 topic.has_unread = True
bgneal@114 171
bgneal@114 172 #######################################################################
bgneal@114 173
bgneal@114 174 def get_post_unread_status(topic, posts, user):
bgneal@114 175 # This service isn't provided to unauthenticated users
bgneal@114 176 if not user.is_authenticated():
bgneal@114 177 for post in posts:
bgneal@114 178 post.unread = False
bgneal@114 179 return
bgneal@114 180
bgneal@114 181 # Get the ForumLastVisit record
bgneal@114 182 try:
bgneal@114 183 flv = ForumLastVisit.objects.get(forum=topic.forum, user=user)
bgneal@114 184 except ForumLastVisit.DoesNotExist:
bgneal@114 185 # One doesn't exist, all posts are old.
bgneal@114 186 for post in posts:
bgneal@114 187 post.unread = False
bgneal@114 188 return
bgneal@114 189
bgneal@114 190 # Are all the posts before our window? If so, all have been read.
bgneal@114 191 if topic.last_post.creation_date < flv.begin_date:
bgneal@114 192 for post in posts:
bgneal@114 193 post.unread = False
bgneal@114 194 return
bgneal@114 195
bgneal@114 196 # Do we have a topic last visit record for this topic?
bgneal@114 197
bgneal@114 198 try:
bgneal@114 199 tlv = TopicLastVisit.objects.get(user=user, topic=topic)
bgneal@114 200 except TopicLastVisit.DoesNotExist:
bgneal@114 201 # No we don't, we could be all caught up, or all are new
bgneal@114 202 for post in posts:
bgneal@114 203 post.unread = post.creation_date > flv.end_date
bgneal@114 204 else:
bgneal@114 205 for post in posts:
bgneal@114 206 post.unread = post.creation_date > tlv.last_visit
bgneal@167 207
bgneal@167 208 #######################################################################
bgneal@167 209
bgneal@167 210 def get_unread_topics(user):
bgneal@167 211 """Returns a list of topics the user hasn't read yet."""
bgneal@167 212
bgneal@167 213 # This is only available to authenticated users
bgneal@167 214 if not user.is_authenticated():
bgneal@167 215 return []
bgneal@167 216
bgneal@167 217 now = datetime.datetime.now()
bgneal@167 218
bgneal@167 219 # Obtain list of forums the user can view
bgneal@167 220 forums = Forum.objects.forums_for_user(user)
bgneal@167 221
bgneal@167 222 # Get forum last visit records for the forum ids
bgneal@167 223 flvs = ForumLastVisit.objects.filter(user=user,
bgneal@167 224 forum__in=forums).select_related()
bgneal@167 225 flvs = dict([(flv.forum.id, flv) for flv in flvs])
bgneal@167 226
bgneal@167 227 unread_topics = []
bgneal@167 228 topics = Topic.objects.none()
bgneal@167 229 for forum in forums:
bgneal@167 230 # if the user hasn't visited the forum, create a last
bgneal@167 231 # visit record set to "now"
bgneal@167 232 if not forum.id in flvs:
bgneal@167 233 flv = ForumLastVisit(user=user, forum=forum, begin_date=now,
bgneal@167 234 end_date=now)
bgneal@167 235 flv.save()
bgneal@167 236 else:
bgneal@167 237 flv = flvs[forum.id]
bgneal@167 238 topics |= Topic.objects.filter(forum=forum,
bgneal@168 239 update_date__gt=flv.begin_date).order_by('-update_date').select_related(
bgneal@168 240 'forum', 'user', 'last_post', 'last_post__user')
bgneal@167 241
bgneal@167 242 if topics is not None:
bgneal@167 243 # get all topic last visit records for the topics of interest
bgneal@167 244
bgneal@167 245 tlvs = TopicLastVisit.objects.filter(user=user, topic__in=topics)
bgneal@167 246 tlvs = dict([(tlv.topic.id, tlv) for tlv in tlvs])
bgneal@167 247
bgneal@167 248 for topic in topics:
bgneal@167 249 if topic.id in tlvs:
bgneal@167 250 tlv = tlvs[topic.id]
bgneal@167 251 if topic.update_date > tlv.last_visit:
bgneal@167 252 unread_topics.append(topic)
bgneal@167 253 else:
bgneal@167 254 unread_topics.append(topic)
bgneal@167 255
bgneal@167 256 return unread_topics