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