##// END OF EJS Templates
Allow linked tags to have cyclic dependancies
Pavel Ryapolov -
r286:ad629f57 default
parent child Browse files
Show More
@@ -1,352 +1,354 b''
1 import os
1 import os
2 from random import random
2 from random import random
3 import time
3 import time
4 import math
4 import math
5
5
6 from django.db import models
6 from django.db import models
7 from django.db.models import Count
7 from django.db.models import Count
8 from django.http import Http404
8 from django.http import Http404
9 from django.utils import timezone
9 from django.utils import timezone
10 from markupfield.fields import MarkupField
10 from markupfield.fields import MarkupField
11
11
12 from neboard import settings
12 from neboard import settings
13 import thumbs
13 import thumbs
14
14
15 IMAGE_THUMB_SIZE = (200, 150)
15 IMAGE_THUMB_SIZE = (200, 150)
16
16
17 TITLE_MAX_LENGTH = 50
17 TITLE_MAX_LENGTH = 50
18
18
19 DEFAULT_MARKUP_TYPE = 'markdown'
19 DEFAULT_MARKUP_TYPE = 'markdown'
20
20
21 NO_PARENT = -1
21 NO_PARENT = -1
22 NO_IP = '0.0.0.0'
22 NO_IP = '0.0.0.0'
23 UNKNOWN_UA = ''
23 UNKNOWN_UA = ''
24 ALL_PAGES = -1
24 ALL_PAGES = -1
25 OPENING_POST_POPULARITY_WEIGHT = 2
25 OPENING_POST_POPULARITY_WEIGHT = 2
26 IMAGES_DIRECTORY = 'images/'
26 IMAGES_DIRECTORY = 'images/'
27 FILE_EXTENSION_DELIMITER = '.'
27 FILE_EXTENSION_DELIMITER = '.'
28
28
29 RANK_ADMIN = 0
29 RANK_ADMIN = 0
30 RANK_MODERATOR = 10
30 RANK_MODERATOR = 10
31 RANK_USER = 100 \
31 RANK_USER = 100 \
32
32
33 SETTING_MODERATE = "moderate"
33 SETTING_MODERATE = "moderate"
34
34
35
35
36 class PostManager(models.Manager):
36 class PostManager(models.Manager):
37
37
38 def create_post(self, title, text, image=None, thread=None,
38 def create_post(self, title, text, image=None, thread=None,
39 ip=NO_IP, tags=None, user=None):
39 ip=NO_IP, tags=None, user=None):
40 post = self.create(title=title,
40 post = self.create(title=title,
41 text=text,
41 text=text,
42 pub_time=timezone.now(),
42 pub_time=timezone.now(),
43 thread=thread,
43 thread=thread,
44 image=image,
44 image=image,
45 poster_ip=ip,
45 poster_ip=ip,
46 poster_user_agent=UNKNOWN_UA,
46 poster_user_agent=UNKNOWN_UA,
47 last_edit_time=timezone.now(),
47 last_edit_time=timezone.now(),
48 bump_time=timezone.now(),
48 bump_time=timezone.now(),
49 user=user)
49 user=user)
50
50
51 if tags:
51 if tags:
52 map(post.tags.add, tags)
52 map(post.tags.add, tags)
53 for tag in tags:
53 for tag in tags:
54 tag.threads.add(post)
54 tag.threads.add(post)
55
55
56 if thread:
56 if thread:
57 thread.replies.add(post)
57 thread.replies.add(post)
58 thread.bump()
58 thread.bump()
59 thread.last_edit_time = timezone.now()
59 thread.last_edit_time = timezone.now()
60 thread.save()
60 thread.save()
61 else:
61 else:
62 self._delete_old_threads()
62 self._delete_old_threads()
63
63
64 return post
64 return post
65
65
66 def delete_post(self, post):
66 def delete_post(self, post):
67 if post.replies.count() > 0:
67 if post.replies.count() > 0:
68 map(self.delete_post, post.replies.all())
68 map(self.delete_post, post.replies.all())
69
69
70 # Update thread's last edit time (used as cache key)
70 # Update thread's last edit time (used as cache key)
71 thread = post.thread
71 thread = post.thread
72 if thread:
72 if thread:
73 thread.last_edit_time = timezone.now()
73 thread.last_edit_time = timezone.now()
74 thread.save()
74 thread.save()
75
75
76 post.delete()
76 post.delete()
77
77
78 def delete_posts_by_ip(self, ip):
78 def delete_posts_by_ip(self, ip):
79 posts = self.filter(poster_ip=ip)
79 posts = self.filter(poster_ip=ip)
80 map(self.delete_post, posts)
80 map(self.delete_post, posts)
81
81
82 def get_threads(self, tag=None, page=ALL_PAGES,
82 def get_threads(self, tag=None, page=ALL_PAGES,
83 order_by='-bump_time'):
83 order_by='-bump_time'):
84 if tag:
84 if tag:
85 threads = tag.threads
85 threads = tag.threads
86
86
87 if threads.count() == 0:
87 if threads.count() == 0:
88 raise Http404
88 raise Http404
89 else:
89 else:
90 threads = self.filter(thread=None)
90 threads = self.filter(thread=None)
91
91
92 threads = threads.order_by(order_by)
92 threads = threads.order_by(order_by)
93
93
94 if page != ALL_PAGES:
94 if page != ALL_PAGES:
95 thread_count = threads.count()
95 thread_count = threads.count()
96
96
97 if page < self.get_thread_page_count(tag=tag):
97 if page < self.get_thread_page_count(tag=tag):
98 start_thread = page * settings.THREADS_PER_PAGE
98 start_thread = page * settings.THREADS_PER_PAGE
99 end_thread = min(start_thread + settings.THREADS_PER_PAGE,
99 end_thread = min(start_thread + settings.THREADS_PER_PAGE,
100 thread_count)
100 thread_count)
101 threads = threads[start_thread:end_thread]
101 threads = threads[start_thread:end_thread]
102
102
103 return threads
103 return threads
104
104
105 def get_thread(self, opening_post_id):
105 def get_thread(self, opening_post_id):
106 try:
106 try:
107 opening_post = self.get(id=opening_post_id, thread=None)
107 opening_post = self.get(id=opening_post_id, thread=None)
108 except Post.DoesNotExist:
108 except Post.DoesNotExist:
109 raise Http404
109 raise Http404
110
110
111 if opening_post.replies:
111 if opening_post.replies:
112 thread = [opening_post]
112 thread = [opening_post]
113 thread.extend(opening_post.replies.all().order_by('pub_time'))
113 thread.extend(opening_post.replies.all().order_by('pub_time'))
114
114
115 return thread
115 return thread
116
116
117 def exists(self, post_id):
117 def exists(self, post_id):
118 posts = self.filter(id=post_id)
118 posts = self.filter(id=post_id)
119
119
120 return posts.count() > 0
120 return posts.count() > 0
121
121
122 def get_thread_page_count(self, tag=None):
122 def get_thread_page_count(self, tag=None):
123 if tag:
123 if tag:
124 threads = self.filter(thread=None, tags=tag)
124 threads = self.filter(thread=None, tags=tag)
125 else:
125 else:
126 threads = self.filter(thread=None)
126 threads = self.filter(thread=None)
127
127
128 return int(math.ceil(threads.count() / float(
128 return int(math.ceil(threads.count() / float(
129 settings.THREADS_PER_PAGE)))
129 settings.THREADS_PER_PAGE)))
130
130
131 def _delete_old_threads(self):
131 def _delete_old_threads(self):
132 """
132 """
133 Preserves maximum thread count. If there are too many threads,
133 Preserves maximum thread count. If there are too many threads,
134 delete the old ones.
134 delete the old ones.
135 """
135 """
136
136
137 # TODO Move old threads to the archive instead of deleting them.
137 # TODO Move old threads to the archive instead of deleting them.
138 # Maybe make some 'old' field in the model to indicate the thread
138 # Maybe make some 'old' field in the model to indicate the thread
139 # must not be shown and be able for replying.
139 # must not be shown and be able for replying.
140
140
141 threads = self.get_threads()
141 threads = self.get_threads()
142 thread_count = threads.count()
142 thread_count = threads.count()
143
143
144 if thread_count > settings.MAX_THREAD_COUNT:
144 if thread_count > settings.MAX_THREAD_COUNT:
145 num_threads_to_delete = thread_count - settings.MAX_THREAD_COUNT
145 num_threads_to_delete = thread_count - settings.MAX_THREAD_COUNT
146 old_threads = threads[thread_count - num_threads_to_delete:]
146 old_threads = threads[thread_count - num_threads_to_delete:]
147
147
148 map(self.delete_post, old_threads)
148 map(self.delete_post, old_threads)
149
149
150
150
151 class TagManager(models.Manager):
151 class TagManager(models.Manager):
152
152
153 def get_not_empty_tags(self):
153 def get_not_empty_tags(self):
154 tags = self.annotate(Count('threads')) \
154 tags = self.annotate(Count('threads')) \
155 .filter(threads__count__gt=0).order_by('name')
155 .filter(threads__count__gt=0).order_by('name')
156
156
157 return tags
157 return tags
158
158
159
159
160 class Tag(models.Model):
160 class Tag(models.Model):
161 """
161 """
162 A tag is a text node assigned to the post. The tag serves as a board
162 A tag is a text node assigned to the post. The tag serves as a board
163 section. There can be multiple tags for each message
163 section. There can be multiple tags for each message
164 """
164 """
165
165
166 objects = TagManager()
166 objects = TagManager()
167
167
168 name = models.CharField(max_length=100)
168 name = models.CharField(max_length=100)
169 threads = models.ManyToManyField('Post', null=True,
169 threads = models.ManyToManyField('Post', null=True,
170 blank=True, related_name='tag+')
170 blank=True, related_name='tag+')
171 linked = models.ForeignKey('Tag', null=True, blank=True)
171 linked = models.ForeignKey('Tag', null=True, blank=True)
172
172
173 def __unicode__(self):
173 def __unicode__(self):
174 return self.name
174 return self.name
175
175
176 def is_empty(self):
176 def is_empty(self):
177 return self.get_post_count() == 0
177 return self.get_post_count() == 0
178
178
179 def get_post_count(self):
179 def get_post_count(self):
180 return self.threads.count()
180 return self.threads.count()
181
181
182 def get_popularity(self):
182 def get_popularity(self):
183 posts_with_tag = Post.objects.get_threads(tag=self)
183 posts_with_tag = Post.objects.get_threads(tag=self)
184 reply_count = 0
184 reply_count = 0
185 for post in posts_with_tag:
185 for post in posts_with_tag:
186 reply_count += post.get_reply_count()
186 reply_count += post.get_reply_count()
187 reply_count += OPENING_POST_POPULARITY_WEIGHT
187 reply_count += OPENING_POST_POPULARITY_WEIGHT
188
188
189 return reply_count
189 return reply_count
190
190
191 def get_linked_tags(self):
191 def get_linked_tags(self, tag_list=[]):
192 linked_tags = []
192 """
193 Returns the list of tags linked to current. The list can be got
194 through returned value or tag_list parameter
195 """
193
196
194 linked_tag = self.linked
197 linked_tag = self.linked
195 if linked_tag:
198
196 linked_tags.append(linked_tag)
199 if linked_tag and not (linked_tag in tag_list):
200 tag_list.append(linked_tag)
197
201
198 far_tags = linked_tag.get_linked_tags()
202 linked_tag.get_linked_tags(tag_list)
199 if len(far_tags) > 0:
200 linked_tags.extend(far_tags)
201
203
202 return linked_tags
204 return tag_list
203
205
204
206
205 class Post(models.Model):
207 class Post(models.Model):
206 """A post is a message."""
208 """A post is a message."""
207
209
208 objects = PostManager()
210 objects = PostManager()
209
211
210 def _update_image_filename(self, filename):
212 def _update_image_filename(self, filename):
211 """Get unique image filename"""
213 """Get unique image filename"""
212
214
213 path = IMAGES_DIRECTORY
215 path = IMAGES_DIRECTORY
214 new_name = str(int(time.mktime(time.gmtime())))
216 new_name = str(int(time.mktime(time.gmtime())))
215 new_name += str(int(random() * 1000))
217 new_name += str(int(random() * 1000))
216 new_name += FILE_EXTENSION_DELIMITER
218 new_name += FILE_EXTENSION_DELIMITER
217 new_name += filename.split(FILE_EXTENSION_DELIMITER)[-1:][0]
219 new_name += filename.split(FILE_EXTENSION_DELIMITER)[-1:][0]
218
220
219 return os.path.join(path, new_name)
221 return os.path.join(path, new_name)
220
222
221 title = models.CharField(max_length=TITLE_MAX_LENGTH)
223 title = models.CharField(max_length=TITLE_MAX_LENGTH)
222 pub_time = models.DateTimeField()
224 pub_time = models.DateTimeField()
223 text = MarkupField(default_markup_type=DEFAULT_MARKUP_TYPE,
225 text = MarkupField(default_markup_type=DEFAULT_MARKUP_TYPE,
224 escape_html=False)
226 escape_html=False)
225
227
226 image_width = models.IntegerField(default=0)
228 image_width = models.IntegerField(default=0)
227 image_height = models.IntegerField(default=0)
229 image_height = models.IntegerField(default=0)
228
230
229 image = thumbs.ImageWithThumbsField(upload_to=_update_image_filename,
231 image = thumbs.ImageWithThumbsField(upload_to=_update_image_filename,
230 blank=True, sizes=(IMAGE_THUMB_SIZE,),
232 blank=True, sizes=(IMAGE_THUMB_SIZE,),
231 width_field='image_width',
233 width_field='image_width',
232 height_field='image_height')
234 height_field='image_height')
233
235
234 poster_ip = models.GenericIPAddressField()
236 poster_ip = models.GenericIPAddressField()
235 poster_user_agent = models.TextField()
237 poster_user_agent = models.TextField()
236
238
237 thread = models.ForeignKey('Post', null=True, default=None)
239 thread = models.ForeignKey('Post', null=True, default=None)
238 tags = models.ManyToManyField(Tag)
240 tags = models.ManyToManyField(Tag)
239 last_edit_time = models.DateTimeField()
241 last_edit_time = models.DateTimeField()
240 bump_time = models.DateTimeField()
242 bump_time = models.DateTimeField()
241 user = models.ForeignKey('User', null=True, default=None)
243 user = models.ForeignKey('User', null=True, default=None)
242
244
243 replies = models.ManyToManyField('Post', symmetrical=False, null=True,
245 replies = models.ManyToManyField('Post', symmetrical=False, null=True,
244 blank=True, related_name='re+')
246 blank=True, related_name='re+')
245
247
246 def __unicode__(self):
248 def __unicode__(self):
247 return '#' + str(self.id) + ' ' + self.title + ' (' + \
249 return '#' + str(self.id) + ' ' + self.title + ' (' + \
248 self.text.raw[:50] + ')'
250 self.text.raw[:50] + ')'
249
251
250 def get_title(self):
252 def get_title(self):
251 title = self.title
253 title = self.title
252 if len(title) == 0:
254 if len(title) == 0:
253 title = self.text.raw[:20]
255 title = self.text.raw[:20]
254
256
255 return title
257 return title
256
258
257 def get_reply_count(self):
259 def get_reply_count(self):
258 return self.replies.count()
260 return self.replies.count()
259
261
260 def get_images_count(self):
262 def get_images_count(self):
261 images_count = 1 if self.image else 0
263 images_count = 1 if self.image else 0
262 images_count += self.replies.filter(image_width__gt=0).count()
264 images_count += self.replies.filter(image_width__gt=0).count()
263
265
264 return images_count
266 return images_count
265
267
266 def can_bump(self):
268 def can_bump(self):
267 """Check if the thread can be bumped by replying"""
269 """Check if the thread can be bumped by replying"""
268
270
269 post_count = self.get_reply_count() + 1
271 post_count = self.get_reply_count() + 1
270
272
271 return post_count <= settings.MAX_POSTS_PER_THREAD
273 return post_count <= settings.MAX_POSTS_PER_THREAD
272
274
273 def bump(self):
275 def bump(self):
274 """Bump (move to up) thread"""
276 """Bump (move to up) thread"""
275
277
276 if self.can_bump():
278 if self.can_bump():
277 self.bump_time = timezone.now()
279 self.bump_time = timezone.now()
278
280
279 def get_last_replies(self):
281 def get_last_replies(self):
280 if settings.LAST_REPLIES_COUNT > 0:
282 if settings.LAST_REPLIES_COUNT > 0:
281 reply_count = self.get_reply_count()
283 reply_count = self.get_reply_count()
282
284
283 if reply_count > 0:
285 if reply_count > 0:
284 reply_count_to_show = min(settings.LAST_REPLIES_COUNT,
286 reply_count_to_show = min(settings.LAST_REPLIES_COUNT,
285 reply_count)
287 reply_count)
286 last_replies = self.replies.all().order_by('pub_time')[reply_count -
288 last_replies = self.replies.all().order_by('pub_time')[reply_count -
287 reply_count_to_show:]
289 reply_count_to_show:]
288
290
289 return last_replies
291 return last_replies
290
292
291
293
292 class User(models.Model):
294 class User(models.Model):
293
295
294 user_id = models.CharField(max_length=50)
296 user_id = models.CharField(max_length=50)
295 rank = models.IntegerField()
297 rank = models.IntegerField()
296
298
297 registration_time = models.DateTimeField()
299 registration_time = models.DateTimeField()
298
300
299 fav_tags = models.ManyToManyField(Tag, null=True, blank=True)
301 fav_tags = models.ManyToManyField(Tag, null=True, blank=True)
300 fav_threads = models.ManyToManyField(Post, related_name='+', null=True,
302 fav_threads = models.ManyToManyField(Post, related_name='+', null=True,
301 blank=True)
303 blank=True)
302
304
303 def save_setting(self, name, value):
305 def save_setting(self, name, value):
304 setting, created = Setting.objects.get_or_create(name=name, user=self)
306 setting, created = Setting.objects.get_or_create(name=name, user=self)
305 setting.value = str(value)
307 setting.value = str(value)
306 setting.save()
308 setting.save()
307
309
308 return setting
310 return setting
309
311
310 def get_setting(self, name):
312 def get_setting(self, name):
311 if Setting.objects.filter(name=name, user=self).exists():
313 if Setting.objects.filter(name=name, user=self).exists():
312 setting = Setting.objects.get(name=name, user=self)
314 setting = Setting.objects.get(name=name, user=self)
313 setting_value = setting.value
315 setting_value = setting.value
314 else:
316 else:
315 setting_value = None
317 setting_value = None
316
318
317 return setting_value
319 return setting_value
318
320
319 def is_moderator(self):
321 def is_moderator(self):
320 return RANK_MODERATOR >= self.rank
322 return RANK_MODERATOR >= self.rank
321
323
322 def get_sorted_fav_tags(self):
324 def get_sorted_fav_tags(self):
323 tags = self.fav_tags.annotate(Count('threads'))\
325 tags = self.fav_tags.annotate(Count('threads'))\
324 .filter(threads__count__gt=0).order_by('name')
326 .filter(threads__count__gt=0).order_by('name')
325
327
326 return tags
328 return tags
327
329
328 def get_post_count(self):
330 def get_post_count(self):
329 return Post.objects.filter(user=self).count()
331 return Post.objects.filter(user=self).count()
330
332
331 def __unicode__(self):
333 def __unicode__(self):
332 return self.user_id + '(' + str(self.rank) + ')'
334 return self.user_id + '(' + str(self.rank) + ')'
333
335
334 def get_last_access_time(self):
336 def get_last_access_time(self):
335 posts = Post.objects.filter(user=self)
337 posts = Post.objects.filter(user=self)
336 if posts.count() > 0:
338 if posts.count() > 0:
337 return posts.latest('pub_time').pub_time
339 return posts.latest('pub_time').pub_time
338
340
339
341
340 class Setting(models.Model):
342 class Setting(models.Model):
341
343
342 name = models.CharField(max_length=50)
344 name = models.CharField(max_length=50)
343 value = models.CharField(max_length=50)
345 value = models.CharField(max_length=50)
344 user = models.ForeignKey(User)
346 user = models.ForeignKey(User)
345
347
346
348
347 class Ban(models.Model):
349 class Ban(models.Model):
348
350
349 ip = models.GenericIPAddressField()
351 ip = models.GenericIPAddressField()
350
352
351 def __unicode__(self):
353 def __unicode__(self):
352 return self.ip
354 return self.ip
General Comments 0
You need to be logged in to leave comments. Login now