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