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