annotate gpp/forums/models.py @ 133:c515b7401078

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