annotate gpp/forums/models.py @ 222:a5fcf3d1b663

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