Mercurial > public > sg101
comparison forums/models.py @ 581:ee87ea74d46b
For Django 1.4, rearranged project structure for new manage.py.
author | Brian Neal <bgneal@gmail.com> |
---|---|
date | Sat, 05 May 2012 17:10:48 -0500 |
parents | gpp/forums/models.py@70722b7d10af |
children | 7d005ebd8deb |
comparison
equal
deleted
inserted
replaced
580:c525f3e0b5d0 | 581:ee87ea74d46b |
---|---|
1 """ | |
2 Models for the forums application. | |
3 """ | |
4 import datetime | |
5 | |
6 from django.db import models | |
7 from django.db.models import Q | |
8 from django.contrib.auth.models import User, Group | |
9 from django.core.cache import cache | |
10 | |
11 from core.markup import site_markup | |
12 from oembed.models import Oembed | |
13 | |
14 | |
15 class Category(models.Model): | |
16 """ | |
17 Forums belong to a category, whose access may be assigned to groups. | |
18 """ | |
19 name = models.CharField(max_length=80) | |
20 slug = models.SlugField(max_length=80) | |
21 position = models.IntegerField(blank=True, default=0) | |
22 groups = models.ManyToManyField(Group, blank=True, null=True, | |
23 help_text="If groups are assigned to this category, only members" \ | |
24 " of those groups can view this category.") | |
25 | |
26 class Meta: | |
27 ordering = ('position', ) | |
28 verbose_name_plural = 'Categories' | |
29 | |
30 def __unicode__(self): | |
31 return self.name | |
32 | |
33 | |
34 class ForumManager(models.Manager): | |
35 """ | |
36 The manager for the Forum model. Provides a centralized place to | |
37 put commonly used and useful queries. | |
38 """ | |
39 | |
40 def forums_for_user(self, user): | |
41 """ | |
42 Returns a queryset containing the forums that the given user can | |
43 "see" due to authenticated status, superuser status and group membership. | |
44 """ | |
45 qs = self._for_user(user) | |
46 return qs.select_related('category', 'last_post', 'last_post__user') | |
47 | |
48 def forum_ids_for_user(self, user): | |
49 """Returns a list of forum IDs that the given user can "see".""" | |
50 qs = self._for_user(user) | |
51 return qs.values_list('id', flat=True) | |
52 | |
53 def public_forums(self): | |
54 """Returns a queryset containing the public forums.""" | |
55 return self.filter(category__groups__isnull=True) | |
56 | |
57 def public_forum_ids(self): | |
58 """ | |
59 Returns a list of ids for the public forums; the list is cached for | |
60 performance. | |
61 """ | |
62 public_forums = cache.get('public_forum_ids') | |
63 if public_forums is None: | |
64 public_forums = list(self.filter( | |
65 category__groups__isnull=True).values_list('id', flat=True)) | |
66 cache.set('public_forum_ids', public_forums, 3600) | |
67 return public_forums | |
68 | |
69 def _for_user(self, user): | |
70 """Common code for the xxx_for_user() methods.""" | |
71 if user.is_superuser: | |
72 qs = self.all() | |
73 else: | |
74 user_groups = user.groups.all() if user.is_authenticated() else [] | |
75 qs = self.filter(Q(category__groups__isnull=True) | | |
76 Q(category__groups__in=user_groups)) | |
77 return qs | |
78 | |
79 | |
80 class Forum(models.Model): | |
81 """ | |
82 A forum is a collection of topics. | |
83 """ | |
84 category = models.ForeignKey(Category, related_name='forums') | |
85 name = models.CharField(max_length=80) | |
86 slug = models.SlugField(max_length=80) | |
87 description = models.TextField(blank=True, default='') | |
88 position = models.IntegerField(blank=True, default=0) | |
89 moderators = models.ManyToManyField(Group, blank=True, null=True) | |
90 | |
91 # denormalized fields to reduce database hits | |
92 topic_count = models.IntegerField(blank=True, default=0) | |
93 post_count = models.IntegerField(blank=True, default=0) | |
94 last_post = models.OneToOneField('Post', blank=True, null=True, | |
95 related_name='parent_forum') | |
96 | |
97 objects = ForumManager() | |
98 | |
99 class Meta: | |
100 ordering = ('position', ) | |
101 | |
102 def __unicode__(self): | |
103 return self.name | |
104 | |
105 @models.permalink | |
106 def get_absolute_url(self): | |
107 return ('forums-forum_index', [self.slug]) | |
108 | |
109 def topic_count_update(self): | |
110 """Call to notify the forum that its topic count has been updated.""" | |
111 self.topic_count = Topic.objects.filter(forum=self).count() | |
112 | |
113 def post_count_update(self): | |
114 """Call to notify the forum that its post count has been updated.""" | |
115 my_posts = Post.objects.filter(topic__forum=self) | |
116 self.post_count = my_posts.count() | |
117 if self.post_count > 0: | |
118 self.last_post = my_posts[self.post_count - 1] | |
119 else: | |
120 self.last_post = None | |
121 | |
122 def sync(self): | |
123 """ | |
124 Call to notify the forum that it needs to recompute its | |
125 denormalized fields. | |
126 """ | |
127 self.topic_count_update() | |
128 self.post_count_update() | |
129 | |
130 def last_post_pre_delete(self, deleting_topic=False): | |
131 """ | |
132 Call this function prior to deleting the last post in the forum. | |
133 A new last post will be found, if one exists. | |
134 This is to avoid the Django cascading delete issue. | |
135 If deleting_topic is True, then the whole topic the last post is | |
136 part of is being deleted, so we can't pick a new last post from that | |
137 topic. | |
138 """ | |
139 try: | |
140 qs = Post.objects.filter(topic__forum=self) | |
141 if deleting_topic: | |
142 qs = qs.exclude(topic=self.last_post.topic) | |
143 else: | |
144 qs = qs.exclude(pk=self.last_post.pk) | |
145 | |
146 self.last_post = qs.latest() | |
147 | |
148 except Post.DoesNotExist: | |
149 self.last_post = None | |
150 | |
151 def catchup(self, user, flv=None): | |
152 """ | |
153 Call to mark this forum all caught up for the given user (i.e. mark all topics | |
154 read for this user). | |
155 """ | |
156 TopicLastVisit.objects.filter(user=user, topic__forum=self).delete() | |
157 if flv is None: | |
158 try: | |
159 flv = ForumLastVisit.objects.get(user=user, forum=self) | |
160 except ForumLastVisit.DoesNotExist: | |
161 flv = ForumLastVisit(user=user, forum=self) | |
162 | |
163 now = datetime.datetime.now() | |
164 flv.begin_date = now | |
165 flv.end_date = now | |
166 flv.save() | |
167 | |
168 | |
169 class Topic(models.Model): | |
170 """ | |
171 A topic is a thread of discussion, consisting of a series of posts. | |
172 """ | |
173 forum = models.ForeignKey(Forum, related_name='topics') | |
174 name = models.CharField(max_length=255) | |
175 creation_date = models.DateTimeField(db_index=True) | |
176 user = models.ForeignKey(User) | |
177 view_count = models.IntegerField(blank=True, default=0) | |
178 sticky = models.BooleanField(blank=True, default=False) | |
179 locked = models.BooleanField(blank=True, default=False) | |
180 subscribers = models.ManyToManyField(User, related_name='subscriptions', | |
181 verbose_name='subscribers', blank=True) | |
182 bookmarkers = models.ManyToManyField(User, related_name='favorite_topics', | |
183 verbose_name='bookmarkers', blank=True) | |
184 | |
185 # denormalized fields to reduce database hits | |
186 post_count = models.IntegerField(blank=True, default=0) | |
187 update_date = models.DateTimeField(db_index=True) | |
188 last_post = models.OneToOneField('Post', blank=True, null=True, | |
189 related_name='parent_topic') | |
190 | |
191 class Meta: | |
192 ordering = ('-sticky', '-update_date', ) | |
193 | |
194 def __unicode__(self): | |
195 return self.name | |
196 | |
197 @models.permalink | |
198 def get_absolute_url(self): | |
199 return ('forums-topic_index', [self.pk]) | |
200 | |
201 @models.permalink | |
202 def get_latest_post_url(self): | |
203 return ('forums-topic_latest', [self.pk]) | |
204 | |
205 def post_count_update(self): | |
206 """ | |
207 Call this function to notify the topic instance that its post count | |
208 has changed. | |
209 """ | |
210 my_posts = Post.objects.filter(topic=self) | |
211 self.post_count = my_posts.count() | |
212 if self.post_count > 0: | |
213 self.last_post = my_posts[self.post_count - 1] | |
214 self.update_date = self.last_post.creation_date | |
215 else: | |
216 self.last_post = None | |
217 self.update_date = self.creation_date | |
218 | |
219 def reply_count(self): | |
220 """ | |
221 Returns the number of replies to a topic. The first post | |
222 doesn't count as a reply. | |
223 """ | |
224 if self.post_count > 1: | |
225 return self.post_count - 1 | |
226 return 0 | |
227 | |
228 def save(self, *args, **kwargs): | |
229 if not self.id: | |
230 now = datetime.datetime.now() | |
231 self.creation_date = now | |
232 self.update_date = now | |
233 | |
234 super(Topic, self).save(*args, **kwargs) | |
235 | |
236 def last_post_pre_delete(self): | |
237 """ | |
238 Call this function prior to deleting the last post in the topic. | |
239 A new last post will be found, if one exists. | |
240 This is to avoid the Django cascading delete issue. | |
241 """ | |
242 try: | |
243 self.last_post = \ | |
244 Post.objects.filter(topic=self).exclude(pk=self.last_post.pk).latest() | |
245 except Post.DoesNotExist: | |
246 self.last_post = None | |
247 | |
248 def search_title(self): | |
249 if self.post_count == 1: | |
250 post_text = "(1 post)" | |
251 else: | |
252 post_text = "(%d posts)" % self.post_count | |
253 | |
254 return u"%s by %s; %s" % (self.name, self.user.username, post_text) | |
255 | |
256 def search_summary(self): | |
257 return u'' | |
258 | |
259 def ogp_tags(self): | |
260 """ | |
261 Returns a dict of Open Graph Protocol meta tags. | |
262 | |
263 """ | |
264 desc = 'Forum topic created by %s on %s.' % ( | |
265 self.user.username, | |
266 self.creation_date.strftime('%B %d, %Y')) | |
267 | |
268 return { | |
269 'og:title': self.name, | |
270 'og:type': 'article', | |
271 'og:url': self.get_absolute_url(), | |
272 'og:description': desc, | |
273 } | |
274 | |
275 | |
276 class Post(models.Model): | |
277 """ | |
278 A post is an instance of a user's single contribution to a topic. | |
279 """ | |
280 topic = models.ForeignKey(Topic, related_name='posts') | |
281 user = models.ForeignKey(User, related_name='posts') | |
282 creation_date = models.DateTimeField(db_index=True) | |
283 update_date = models.DateTimeField(db_index=True) | |
284 body = models.TextField() | |
285 html = models.TextField() | |
286 user_ip = models.IPAddressField(blank=True, default='', null=True) | |
287 attachments = models.ManyToManyField(Oembed, through='Attachment') | |
288 | |
289 class Meta: | |
290 ordering = ('creation_date', ) | |
291 get_latest_by = 'creation_date' | |
292 verbose_name = 'forum post' | |
293 verbose_name_plural = 'forum posts' | |
294 | |
295 @models.permalink | |
296 def get_absolute_url(self): | |
297 return ('forums-goto_post', [self.pk]) | |
298 | |
299 def summary(self): | |
300 limit = 65 | |
301 if len(self.body) < limit: | |
302 return self.body | |
303 return self.body[:limit] + '...' | |
304 | |
305 def __unicode__(self): | |
306 return self.summary() | |
307 | |
308 def save(self, *args, **kwargs): | |
309 if not self.id: | |
310 self.creation_date = datetime.datetime.now() | |
311 self.update_date = self.creation_date | |
312 | |
313 self.html = site_markup(self.body) | |
314 super(Post, self).save(*args, **kwargs) | |
315 | |
316 def delete(self, *args, **kwargs): | |
317 first_post_id = self.topic.posts.all()[0].id | |
318 super(Post, self).delete(*args, **kwargs) | |
319 if self.id == first_post_id: | |
320 self.topic.delete() | |
321 | |
322 def has_been_edited(self): | |
323 return self.update_date > self.creation_date | |
324 | |
325 def touch(self): | |
326 """Call this function to indicate the post has been edited.""" | |
327 self.update_date = datetime.datetime.now() | |
328 | |
329 def search_title(self): | |
330 return u"%s by %s" % (self.topic.name, self.user.username) | |
331 | |
332 def search_summary(self): | |
333 return self.body | |
334 | |
335 | |
336 class FlaggedPost(models.Model): | |
337 """This model represents a user flagging a post as inappropriate.""" | |
338 user = models.ForeignKey(User) | |
339 post = models.ForeignKey(Post) | |
340 flag_date = models.DateTimeField(auto_now_add=True) | |
341 | |
342 def __unicode__(self): | |
343 return u'Post ID %s flagged by %s' % (self.post.id, self.user.username) | |
344 | |
345 class Meta: | |
346 ordering = ('flag_date', ) | |
347 | |
348 def get_post_url(self): | |
349 return '<a href="%s">Post</a>' % self.post.get_absolute_url() | |
350 get_post_url.allow_tags = True | |
351 | |
352 | |
353 class ForumLastVisit(models.Model): | |
354 """ | |
355 This model records the last time a user visited a forum. | |
356 It is used to compute if a user has unread topics in a forum. | |
357 We keep track of a window of time, delimited by begin_date and end_date. | |
358 Topics updated within this window are tracked, and may have TopicLastVisit | |
359 objects. | |
360 Marking a forum as all read sets the begin_date equal to the end_date. | |
361 """ | |
362 user = models.ForeignKey(User) | |
363 forum = models.ForeignKey(Forum) | |
364 begin_date = models.DateTimeField() | |
365 end_date = models.DateTimeField() | |
366 | |
367 class Meta: | |
368 unique_together = ('user', 'forum') | |
369 ordering = ('-end_date', ) | |
370 | |
371 def __unicode__(self): | |
372 return u'Forum: %d User: %d Date: %s' % (self.forum.id, self.user.id, | |
373 self.end_date.strftime('%Y-%m-%d %H:%M:%S')) | |
374 | |
375 def is_caught_up(self): | |
376 return self.begin_date == self.end_date | |
377 | |
378 | |
379 class TopicLastVisit(models.Model): | |
380 """ | |
381 This model records the last time a user read a topic. | |
382 Objects of this class exist for the window specified in the | |
383 corresponding ForumLastVisit object. | |
384 """ | |
385 user = models.ForeignKey(User) | |
386 topic = models.ForeignKey(Topic) | |
387 last_visit = models.DateTimeField(db_index=True) | |
388 | |
389 class Meta: | |
390 unique_together = ('user', 'topic') | |
391 ordering = ('-last_visit', ) | |
392 | |
393 def __unicode__(self): | |
394 return u'Topic: %d User: %d Date: %s' % (self.topic.id, self.user.id, | |
395 self.last_visit.strftime('%Y-%m-%d %H:%M:%S')) | |
396 | |
397 def save(self, *args, **kwargs): | |
398 if self.last_visit is None: | |
399 self.touch() | |
400 super(TopicLastVisit, self).save(*args, **kwargs) | |
401 | |
402 def touch(self): | |
403 self.last_visit = datetime.datetime.now() | |
404 | |
405 | |
406 class Attachment(models.Model): | |
407 """ | |
408 This model is a "through" table for the M2M relationship between forum | |
409 posts and Oembed objects. | |
410 """ | |
411 post = models.ForeignKey(Post) | |
412 embed = models.ForeignKey(Oembed) | |
413 order = models.IntegerField() | |
414 | |
415 class Meta: | |
416 ordering = ('order', ) | |
417 | |
418 def __unicode__(self): | |
419 return u'Post %d, %s' % (self.post.pk, self.embed.title) | |
420 |