annotate gpp/forums/models.py @ 100:eb9f99382476

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