annotate gpp/forums/models.py @ 112:d1b0b86441c0

Forums: added a sync() function on the forum model. Created javascript for the moderate forum function to drive the master topic select checkbox.
author Brian Neal <bgneal@gmail.com>
date Wed, 30 Sep 2009 00:42:13 +0000
parents e94398f5e027
children d97ceb95ce02
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@75 9 from django.template.loader import render_to_string
bgneal@75 10
bgneal@75 11
bgneal@75 12 class Category(models.Model):
bgneal@100 13 """
bgneal@100 14 Forums belong to a category, whose access may be assigned to groups.
bgneal@100 15 """
bgneal@75 16 name = models.CharField(max_length=80)
bgneal@75 17 slug = models.SlugField(max_length=80)
bgneal@75 18 position = models.IntegerField(blank=True, default=0)
bgneal@75 19 groups = models.ManyToManyField(Group, blank=True, null=True,
bgneal@75 20 help_text="If groups are assigned to this category, only members" \
bgneal@75 21 " of those groups can view this category.")
bgneal@75 22
bgneal@75 23 class Meta:
bgneal@75 24 ordering = ('position', )
bgneal@75 25 verbose_name_plural = 'Categories'
bgneal@75 26
bgneal@75 27 def __unicode__(self):
bgneal@75 28 return self.name
bgneal@75 29
bgneal@100 30 def can_access(self, user):
bgneal@100 31 """
bgneal@100 32 Checks to see if the given user has permission to access
bgneal@100 33 this category.
bgneal@100 34 If this category has no groups assigned to it, return true.
bgneal@100 35 Else, return true if the user belongs to a group that has been
bgneal@100 36 assigned to this category, and false otherwise.
bgneal@100 37 """
bgneal@100 38 if self.groups.count() == 0:
bgneal@100 39 return True
bgneal@100 40 if user.is_authenticated():
bgneal@100 41 return self.groups.filter(user__pk=user.id).count() > 0
bgneal@100 42 return False
bgneal@100 43
bgneal@100 44
bgneal@100 45 class ForumManager(models.Manager):
bgneal@100 46 """
bgneal@100 47 The manager for the Forum model. Provides a centralized place to
bgneal@100 48 put commonly used and useful queries.
bgneal@100 49 """
bgneal@100 50
bgneal@100 51 def forums_for_user(self, user):
bgneal@100 52 """
bgneal@100 53 Returns a queryset containing the forums that the given user can
bgneal@100 54 "see" due to authenticated status, superuser status and group membership.
bgneal@100 55 """
bgneal@100 56 if user.is_superuser:
bgneal@100 57 qs = self.all()
bgneal@100 58 else:
bgneal@100 59 user_groups = []
bgneal@100 60 if user.is_authenticated():
bgneal@100 61 user_groups = user.groups.all()
bgneal@100 62
bgneal@100 63 qs = self.filter(Q(category__groups__isnull=True) | \
bgneal@100 64 Q(category__groups__in=user_groups))
bgneal@100 65
bgneal@100 66 return qs.select_related('category', 'last_post', 'last_post__user')
bgneal@100 67
bgneal@75 68
bgneal@75 69 class Forum(models.Model):
bgneal@100 70 """
bgneal@100 71 A forum is a collection of topics.
bgneal@100 72 """
bgneal@75 73 category = models.ForeignKey(Category, related_name='forums')
bgneal@75 74 name = models.CharField(max_length=80)
bgneal@75 75 slug = models.SlugField(max_length=80)
bgneal@75 76 description = models.TextField(blank=True, default='')
bgneal@75 77 position = models.IntegerField(blank=True, default=0)
bgneal@75 78 moderators = models.ManyToManyField(Group, blank=True, null=True)
bgneal@75 79
bgneal@75 80 # denormalized fields to reduce database hits
bgneal@75 81 topic_count = models.IntegerField(blank=True, default=0)
bgneal@75 82 post_count = models.IntegerField(blank=True, default=0)
bgneal@75 83 last_post = models.OneToOneField('Post', blank=True, null=True,
bgneal@75 84 related_name='parent_forum')
bgneal@75 85
bgneal@100 86 objects = ForumManager()
bgneal@100 87
bgneal@75 88 class Meta:
bgneal@75 89 ordering = ('position', )
bgneal@75 90
bgneal@75 91 def __unicode__(self):
bgneal@75 92 return self.name
bgneal@75 93
bgneal@81 94 @models.permalink
bgneal@81 95 def get_absolute_url(self):
bgneal@81 96 return ('forums-forum_index', [self.slug])
bgneal@81 97
bgneal@75 98 def topic_count_update(self):
bgneal@75 99 """Call to notify the forum that its topic count has been updated."""
bgneal@75 100 self.topic_count = Topic.objects.filter(forum=self).count()
bgneal@75 101
bgneal@75 102 def post_count_update(self):
bgneal@75 103 """Call to notify the forum that its post count has been updated."""
bgneal@75 104 my_posts = Post.objects.filter(topic__forum=self)
bgneal@75 105 self.post_count = my_posts.count()
bgneal@75 106 if self.post_count > 0:
bgneal@75 107 self.last_post = my_posts[self.post_count - 1]
bgneal@75 108 else:
bgneal@75 109 self.last_post = None
bgneal@75 110
bgneal@112 111 def sync(self):
bgneal@112 112 """
bgneal@112 113 Call to notify the forum that it needs to recompute its
bgneal@112 114 denormalized fields.
bgneal@112 115 """
bgneal@112 116 self.topic_count_update()
bgneal@112 117 self.post_count_update()
bgneal@112 118
bgneal@107 119 def last_post_pre_delete(self):
bgneal@107 120 """
bgneal@107 121 Call this function prior to deleting the last post in the forum.
bgneal@107 122 A new last post will be found, if one exists.
bgneal@107 123 This is to avoid the Django cascading delete issue.
bgneal@107 124 """
bgneal@107 125 try:
bgneal@107 126 self.last_post = \
bgneal@107 127 Post.objects.filter(topic__forum=self).exclude(pk=self.last_post.pk).latest()
bgneal@107 128 except Post.DoesNotExist:
bgneal@107 129 self.last_post = None
bgneal@107 130
bgneal@75 131
bgneal@75 132 class Topic(models.Model):
bgneal@100 133 """
bgneal@100 134 A topic is a thread of discussion, consisting of a series of posts.
bgneal@100 135 """
bgneal@75 136 forum = models.ForeignKey(Forum, related_name='topics')
bgneal@75 137 name = models.CharField(max_length=255)
bgneal@75 138 creation_date = models.DateTimeField(auto_now_add=True)
bgneal@75 139 user = models.ForeignKey(User)
bgneal@75 140 view_count = models.IntegerField(blank=True, default=0)
bgneal@75 141 sticky = models.BooleanField(blank=True, default=False)
bgneal@75 142 locked = models.BooleanField(blank=True, default=False)
bgneal@75 143
bgneal@75 144 # denormalized fields to reduce database hits
bgneal@75 145 post_count = models.IntegerField(blank=True, default=0)
bgneal@102 146 update_date = models.DateTimeField()
bgneal@75 147 last_post = models.OneToOneField('Post', blank=True, null=True,
bgneal@75 148 related_name='parent_topic')
bgneal@75 149
bgneal@75 150 class Meta:
bgneal@75 151 ordering = ('-sticky', '-update_date', )
bgneal@75 152
bgneal@75 153 def __unicode__(self):
bgneal@75 154 return self.name
bgneal@75 155
bgneal@82 156 @models.permalink
bgneal@82 157 def get_absolute_url(self):
bgneal@82 158 return ('forums-topic_index', [self.pk])
bgneal@82 159
bgneal@75 160 def post_count_update(self):
bgneal@75 161 """
bgneal@75 162 Call this function to notify the topic instance that its post count
bgneal@75 163 has changed.
bgneal@75 164 """
bgneal@75 165 my_posts = Post.objects.filter(topic=self)
bgneal@75 166 self.post_count = my_posts.count()
bgneal@75 167 if self.post_count > 0:
bgneal@75 168 self.last_post = my_posts[self.post_count - 1]
bgneal@75 169 self.update_date = self.last_post.creation_date
bgneal@75 170 else:
bgneal@75 171 self.last_post = None
bgneal@75 172 self.update_date = self.creation_date
bgneal@75 173
bgneal@83 174 def reply_count(self):
bgneal@83 175 """
bgneal@83 176 Returns the number of replies to a topic. The first post
bgneal@83 177 doesn't count as a reply.
bgneal@83 178 """
bgneal@83 179 if self.post_count > 1:
bgneal@83 180 return self.post_count - 1
bgneal@83 181 return 0
bgneal@83 182
bgneal@102 183 def save(self, *args, **kwargs):
bgneal@102 184 if not self.id:
bgneal@102 185 now = datetime.datetime.now()
bgneal@102 186 self.creation_date = now
bgneal@102 187 self.update_date = now
bgneal@102 188
bgneal@102 189 super(Topic, self).save(*args, **kwargs)
bgneal@102 190
bgneal@107 191 def last_post_pre_delete(self):
bgneal@107 192 """
bgneal@107 193 Call this function prior to deleting the last post in the topic.
bgneal@107 194 A new last post will be found, if one exists.
bgneal@107 195 This is to avoid the Django cascading delete issue.
bgneal@107 196 """
bgneal@107 197 try:
bgneal@107 198 self.last_post = \
bgneal@107 199 Post.objects.filter(topic=self).exclude(pk=self.last_post.pk).latest()
bgneal@107 200 except Post.DoesNotExist:
bgneal@107 201 self.last_post = None
bgneal@107 202
bgneal@75 203
bgneal@75 204 class Post(models.Model):
bgneal@100 205 """
bgneal@100 206 A post is an instance of a user's single contribution to a topic.
bgneal@100 207 """
bgneal@75 208 topic = models.ForeignKey(Topic, related_name='posts')
bgneal@75 209 user = models.ForeignKey(User, related_name='posts')
bgneal@75 210 creation_date = models.DateTimeField(auto_now_add=True)
bgneal@75 211 update_date = models.DateTimeField(auto_now=True)
bgneal@75 212 body = models.TextField()
bgneal@75 213 html = models.TextField()
bgneal@83 214 user_ip = models.IPAddressField(blank=True, default='', null=True)
bgneal@75 215
bgneal@75 216 class Meta:
bgneal@97 217 ordering = ('creation_date', )
bgneal@107 218 get_latest_by = 'creation_date'
bgneal@75 219
bgneal@91 220 @models.permalink
bgneal@91 221 def get_absolute_url(self):
bgneal@91 222 return ('forums-goto_post', [self.pk])
bgneal@91 223
bgneal@75 224 def summary(self):
bgneal@75 225 LIMIT = 50
bgneal@75 226 if len(self.body) < LIMIT:
bgneal@75 227 return self.body
bgneal@75 228 return self.body[:LIMIT] + '...'
bgneal@75 229
bgneal@75 230 def __unicode__(self):
bgneal@75 231 return self.summary()
bgneal@75 232
bgneal@75 233 def save(self, *args, **kwargs):
bgneal@75 234 html = render_to_string('forums/post.html', {'data': self.body})
bgneal@75 235 self.html = html.strip()
bgneal@75 236 super(Post, self).save(*args, **kwargs)
bgneal@75 237
bgneal@75 238 def delete(self, *args, **kwargs):
bgneal@75 239 first_post_id = self.topic.posts.all()[0].id
bgneal@75 240 super(Post, self).delete(*args, **kwargs)
bgneal@75 241 if self.id == first_post_id:
bgneal@75 242 self.topic.delete()
bgneal@75 243
bgneal@98 244
bgneal@98 245 class FlaggedPost(models.Model):
bgneal@98 246 """This model represents a user flagging a post as inappropriate."""
bgneal@98 247 user = models.ForeignKey(User)
bgneal@98 248 post = models.ForeignKey(Post)
bgneal@98 249 flag_date = models.DateTimeField(auto_now_add=True)
bgneal@98 250
bgneal@98 251 def __unicode__(self):
bgneal@98 252 return u'Post ID %s flagged by %s' % (self.post.id, self.user.username)
bgneal@98 253
bgneal@98 254 class Meta:
bgneal@98 255 ordering = ('flag_date', )
bgneal@98 256
bgneal@98 257 def get_post_url(self):
bgneal@98 258 return '<a href="%s">Post</a>' % self.post.get_absolute_url()
bgneal@98 259 get_post_url.allow_tags = True
bgneal@98 260
bgneal@75 261 # TODO: A "read" table