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
|