annotate gpp/forums/models.py @ 429:d0f0800eef0c

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