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
|