##// END OF EJS Templates
Added tag search. Refactored search to show any model results in a list.
neko259 -
r692:d0013853 1.8-dev
parent child Browse files
Show More
@@ -0,0 +1,10 b''
1 __author__ = 'neko259'
2
3
4 class Viewable():
5 def __init__(self):
6 pass
7
8 def get_view(self, *args, **kwargs):
9 """Get an HTML view for a model"""
10 pass No newline at end of file
@@ -1,360 +1,396 b''
1 from datetime import datetime, timedelta, date
1 from datetime import datetime, timedelta, date
2 from datetime import time as dtime
2 from datetime import time as dtime
3 import logging
3 import logging
4 import os
4 import os
5 from random import random
5 from random import random
6 import time
6 import time
7 import re
7 import re
8 import hashlib
8 import hashlib
9
9
10 from django.core.cache import cache
10 from django.core.cache import cache
11 from django.core.urlresolvers import reverse
11 from django.core.urlresolvers import reverse
12 from django.db import models, transaction
12 from django.db import models, transaction
13 from django.template.loader import render_to_string
13 from django.utils import timezone
14 from django.utils import timezone
14 from markupfield.fields import MarkupField
15 from markupfield.fields import MarkupField
16 from boards.models.base import Viewable
17
15 from boards.models.thread import Thread
18 from boards.models.thread import Thread
16
17 from neboard import settings
19 from neboard import settings
18 from boards import thumbs
20 from boards import thumbs
19
21
20
22
21 APP_LABEL_BOARDS = 'boards'
23 APP_LABEL_BOARDS = 'boards'
22
24
23 CACHE_KEY_PPD = 'ppd'
25 CACHE_KEY_PPD = 'ppd'
24 CACHE_KEY_POST_URL = 'post_url'
26 CACHE_KEY_POST_URL = 'post_url'
25
27
26 POSTS_PER_DAY_RANGE = range(7)
28 POSTS_PER_DAY_RANGE = range(7)
27
29
28 BAN_REASON_AUTO = 'Auto'
30 BAN_REASON_AUTO = 'Auto'
29
31
30 IMAGE_THUMB_SIZE = (200, 150)
32 IMAGE_THUMB_SIZE = (200, 150)
31
33
32 TITLE_MAX_LENGTH = 200
34 TITLE_MAX_LENGTH = 200
33
35
34 DEFAULT_MARKUP_TYPE = 'markdown'
36 DEFAULT_MARKUP_TYPE = 'markdown'
35
37
36 NO_PARENT = -1
38 NO_PARENT = -1
37 NO_IP = '0.0.0.0'
39 NO_IP = '0.0.0.0'
38 UNKNOWN_UA = ''
40 UNKNOWN_UA = ''
39 ALL_PAGES = -1
41 ALL_PAGES = -1
40 IMAGES_DIRECTORY = 'images/'
42 IMAGES_DIRECTORY = 'images/'
41 FILE_EXTENSION_DELIMITER = '.'
43 FILE_EXTENSION_DELIMITER = '.'
42
44
43 SETTING_MODERATE = "moderate"
45 SETTING_MODERATE = "moderate"
44
46
45 REGEX_REPLY = re.compile('>>(\d+)')
47 REGEX_REPLY = re.compile('>>(\d+)')
46
48
47 logger = logging.getLogger(__name__)
49 logger = logging.getLogger(__name__)
48
50
49
51
50 class PostManager(models.Manager):
52 class PostManager(models.Manager):
51
53
52 def create_post(self, title, text, image=None, thread=None,
54 def create_post(self, title, text, image=None, thread=None,
53 ip=NO_IP, tags=None, user=None):
55 ip=NO_IP, tags=None, user=None):
54 """
56 """
55 Creates new post
57 Creates new post
56 """
58 """
57
59
58 posting_time = timezone.now()
60 posting_time = timezone.now()
59 if not thread:
61 if not thread:
60 thread = Thread.objects.create(bump_time=posting_time,
62 thread = Thread.objects.create(bump_time=posting_time,
61 last_edit_time=posting_time)
63 last_edit_time=posting_time)
62 new_thread = True
64 new_thread = True
63 else:
65 else:
64 thread.bump()
66 thread.bump()
65 thread.last_edit_time = posting_time
67 thread.last_edit_time = posting_time
66 thread.save()
68 thread.save()
67 new_thread = False
69 new_thread = False
68
70
69 post = self.create(title=title,
71 post = self.create(title=title,
70 text=text,
72 text=text,
71 pub_time=posting_time,
73 pub_time=posting_time,
72 thread_new=thread,
74 thread_new=thread,
73 image=image,
75 image=image,
74 poster_ip=ip,
76 poster_ip=ip,
75 poster_user_agent=UNKNOWN_UA, # TODO Get UA at
77 poster_user_agent=UNKNOWN_UA, # TODO Get UA at
76 # last!
78 # last!
77 last_edit_time=posting_time,
79 last_edit_time=posting_time,
78 user=user)
80 user=user)
79
81
80 thread.replies.add(post)
82 thread.replies.add(post)
81 if tags:
83 if tags:
82 linked_tags = []
84 linked_tags = []
83 for tag in tags:
85 for tag in tags:
84 tag_linked_tags = tag.get_linked_tags()
86 tag_linked_tags = tag.get_linked_tags()
85 if len(tag_linked_tags) > 0:
87 if len(tag_linked_tags) > 0:
86 linked_tags.extend(tag_linked_tags)
88 linked_tags.extend(tag_linked_tags)
87
89
88 tags.extend(linked_tags)
90 tags.extend(linked_tags)
89 map(thread.add_tag, tags)
91 map(thread.add_tag, tags)
90
92
91 if new_thread:
93 if new_thread:
92 self._delete_old_threads()
94 self._delete_old_threads()
93 self.connect_replies(post)
95 self.connect_replies(post)
94
96
95 logger.info('Created post #%d' % post.id)
97 logger.info('Created post #%d' % post.id)
96
98
97 return post
99 return post
98
100
99 def delete_post(self, post):
101 def delete_post(self, post):
100 """
102 """
101 Deletes post and update or delete its thread
103 Deletes post and update or delete its thread
102 """
104 """
103
105
104 post_id = post.id
106 post_id = post.id
105
107
106 thread = post.get_thread()
108 thread = post.get_thread()
107
109
108 if post.is_opening():
110 if post.is_opening():
109 thread.delete_with_posts()
111 thread.delete_with_posts()
110 else:
112 else:
111 thread.last_edit_time = timezone.now()
113 thread.last_edit_time = timezone.now()
112 thread.save()
114 thread.save()
113
115
114 post.delete()
116 post.delete()
115
117
116 logger.info('Deleted post #%d' % post_id)
118 logger.info('Deleted post #%d' % post_id)
117
119
118 def delete_posts_by_ip(self, ip):
120 def delete_posts_by_ip(self, ip):
119 """
121 """
120 Deletes all posts of the author with same IP
122 Deletes all posts of the author with same IP
121 """
123 """
122
124
123 posts = self.filter(poster_ip=ip)
125 posts = self.filter(poster_ip=ip)
124 map(self.delete_post, posts)
126 map(self.delete_post, posts)
125
127
126 # TODO Move this method to thread manager
128 # TODO Move this method to thread manager
127 def _delete_old_threads(self):
129 def _delete_old_threads(self):
128 """
130 """
129 Preserves maximum thread count. If there are too many threads,
131 Preserves maximum thread count. If there are too many threads,
130 archive the old ones.
132 archive the old ones.
131 """
133 """
132
134
133 threads = Thread.objects.filter(archived=False).order_by('-bump_time')
135 threads = Thread.objects.filter(archived=False).order_by('-bump_time')
134 thread_count = threads.count()
136 thread_count = threads.count()
135
137
136 if thread_count > settings.MAX_THREAD_COUNT:
138 if thread_count > settings.MAX_THREAD_COUNT:
137 num_threads_to_delete = thread_count - settings.MAX_THREAD_COUNT
139 num_threads_to_delete = thread_count - settings.MAX_THREAD_COUNT
138 old_threads = threads[thread_count - num_threads_to_delete:]
140 old_threads = threads[thread_count - num_threads_to_delete:]
139
141
140 for thread in old_threads:
142 for thread in old_threads:
141 thread.archived = True
143 thread.archived = True
142 thread.last_edit_time = timezone.now()
144 thread.last_edit_time = timezone.now()
143 thread.save()
145 thread.save()
144
146
145 logger.info('Archived %d old threads' % num_threads_to_delete)
147 logger.info('Archived %d old threads' % num_threads_to_delete)
146
148
147 def connect_replies(self, post):
149 def connect_replies(self, post):
148 """
150 """
149 Connects replies to a post to show them as a reflink map
151 Connects replies to a post to show them as a reflink map
150 """
152 """
151
153
152 for reply_number in re.finditer(REGEX_REPLY, post.text.raw):
154 for reply_number in re.finditer(REGEX_REPLY, post.text.raw):
153 post_id = reply_number.group(1)
155 post_id = reply_number.group(1)
154 ref_post = self.filter(id=post_id)
156 ref_post = self.filter(id=post_id)
155 if ref_post.count() > 0:
157 if ref_post.count() > 0:
156 referenced_post = ref_post[0]
158 referenced_post = ref_post[0]
157 referenced_post.referenced_posts.add(post)
159 referenced_post.referenced_posts.add(post)
158 referenced_post.last_edit_time = post.pub_time
160 referenced_post.last_edit_time = post.pub_time
159 referenced_post.build_refmap()
161 referenced_post.build_refmap()
160 referenced_post.save()
162 referenced_post.save()
161
163
162 referenced_thread = referenced_post.get_thread()
164 referenced_thread = referenced_post.get_thread()
163 referenced_thread.last_edit_time = post.pub_time
165 referenced_thread.last_edit_time = post.pub_time
164 referenced_thread.save()
166 referenced_thread.save()
165
167
166 def get_posts_per_day(self):
168 def get_posts_per_day(self):
167 """
169 """
168 Gets average count of posts per day for the last 7 days
170 Gets average count of posts per day for the last 7 days
169 """
171 """
170
172
171 today = date.today()
173 today = date.today()
172 ppd = cache.get(CACHE_KEY_PPD + str(today))
174 ppd = cache.get(CACHE_KEY_PPD + str(today))
173 if ppd:
175 if ppd:
174 return ppd
176 return ppd
175
177
176 posts_per_days = []
178 posts_per_days = []
177 for i in POSTS_PER_DAY_RANGE:
179 for i in POSTS_PER_DAY_RANGE:
178 day_end = today - timedelta(i + 1)
180 day_end = today - timedelta(i + 1)
179 day_start = today - timedelta(i + 2)
181 day_start = today - timedelta(i + 2)
180
182
181 day_time_start = timezone.make_aware(datetime.combine(
183 day_time_start = timezone.make_aware(datetime.combine(
182 day_start, dtime()), timezone.get_current_timezone())
184 day_start, dtime()), timezone.get_current_timezone())
183 day_time_end = timezone.make_aware(datetime.combine(
185 day_time_end = timezone.make_aware(datetime.combine(
184 day_end, dtime()), timezone.get_current_timezone())
186 day_end, dtime()), timezone.get_current_timezone())
185
187
186 posts_per_days.append(float(self.filter(
188 posts_per_days.append(float(self.filter(
187 pub_time__lte=day_time_end,
189 pub_time__lte=day_time_end,
188 pub_time__gte=day_time_start).count()))
190 pub_time__gte=day_time_start).count()))
189
191
190 ppd = (sum(posts_per_day for posts_per_day in posts_per_days) /
192 ppd = (sum(posts_per_day for posts_per_day in posts_per_days) /
191 len(posts_per_days))
193 len(posts_per_days))
192 cache.set(CACHE_KEY_PPD + str(today), ppd)
194 cache.set(CACHE_KEY_PPD + str(today), ppd)
193 return ppd
195 return ppd
194
196
195
197
196 class Post(models.Model):
198 class Post(models.Model, Viewable):
197 """A post is a message."""
199 """A post is a message."""
198
200
199 objects = PostManager()
201 objects = PostManager()
200
202
201 class Meta:
203 class Meta:
202 app_label = APP_LABEL_BOARDS
204 app_label = APP_LABEL_BOARDS
203 ordering = ('id',)
205 ordering = ('id',)
204
206
205 # TODO Save original file name to some field
207 # TODO Save original file name to some field
206 def _update_image_filename(self, filename):
208 def _update_image_filename(self, filename):
207 """
209 """
208 Gets unique image filename
210 Gets unique image filename
209 """
211 """
210
212
211 path = IMAGES_DIRECTORY
213 path = IMAGES_DIRECTORY
212 new_name = str(int(time.mktime(time.gmtime())))
214 new_name = str(int(time.mktime(time.gmtime())))
213 new_name += str(int(random() * 1000))
215 new_name += str(int(random() * 1000))
214 new_name += FILE_EXTENSION_DELIMITER
216 new_name += FILE_EXTENSION_DELIMITER
215 new_name += filename.split(FILE_EXTENSION_DELIMITER)[-1:][0]
217 new_name += filename.split(FILE_EXTENSION_DELIMITER)[-1:][0]
216
218
217 return os.path.join(path, new_name)
219 return os.path.join(path, new_name)
218
220
219 title = models.CharField(max_length=TITLE_MAX_LENGTH)
221 title = models.CharField(max_length=TITLE_MAX_LENGTH)
220 pub_time = models.DateTimeField()
222 pub_time = models.DateTimeField()
221 text = MarkupField(default_markup_type=DEFAULT_MARKUP_TYPE,
223 text = MarkupField(default_markup_type=DEFAULT_MARKUP_TYPE,
222 escape_html=False)
224 escape_html=False)
223
225
224 image_width = models.IntegerField(default=0)
226 image_width = models.IntegerField(default=0)
225 image_height = models.IntegerField(default=0)
227 image_height = models.IntegerField(default=0)
226
228
227 image_pre_width = models.IntegerField(default=0)
229 image_pre_width = models.IntegerField(default=0)
228 image_pre_height = models.IntegerField(default=0)
230 image_pre_height = models.IntegerField(default=0)
229
231
230 image = thumbs.ImageWithThumbsField(upload_to=_update_image_filename,
232 image = thumbs.ImageWithThumbsField(upload_to=_update_image_filename,
231 blank=True, sizes=(IMAGE_THUMB_SIZE,),
233 blank=True, sizes=(IMAGE_THUMB_SIZE,),
232 width_field='image_width',
234 width_field='image_width',
233 height_field='image_height',
235 height_field='image_height',
234 preview_width_field='image_pre_width',
236 preview_width_field='image_pre_width',
235 preview_height_field='image_pre_height')
237 preview_height_field='image_pre_height')
236 image_hash = models.CharField(max_length=36)
238 image_hash = models.CharField(max_length=36)
237
239
238 poster_ip = models.GenericIPAddressField()
240 poster_ip = models.GenericIPAddressField()
239 poster_user_agent = models.TextField()
241 poster_user_agent = models.TextField()
240
242
241 thread_new = models.ForeignKey('Thread', null=True, default=None,
243 thread_new = models.ForeignKey('Thread', null=True, default=None,
242 db_index=True)
244 db_index=True)
243 last_edit_time = models.DateTimeField()
245 last_edit_time = models.DateTimeField()
244 user = models.ForeignKey('User', null=True, default=None, db_index=True)
246 user = models.ForeignKey('User', null=True, default=None, db_index=True)
245
247
246 referenced_posts = models.ManyToManyField('Post', symmetrical=False,
248 referenced_posts = models.ManyToManyField('Post', symmetrical=False,
247 null=True,
249 null=True,
248 blank=True, related_name='rfp+',
250 blank=True, related_name='rfp+',
249 db_index=True)
251 db_index=True)
250 refmap = models.TextField(null=True, blank=True)
252 refmap = models.TextField(null=True, blank=True)
251
253
252 def __unicode__(self):
254 def __unicode__(self):
253 return '#' + str(self.id) + ' ' + self.title + ' (' + \
255 return '#' + str(self.id) + ' ' + self.title + ' (' + \
254 self.text.raw[:50] + ')'
256 self.text.raw[:50] + ')'
255
257
256 def get_title(self):
258 def get_title(self):
257 """
259 """
258 Gets original post title or part of its text.
260 Gets original post title or part of its text.
259 """
261 """
260
262
261 title = self.title
263 title = self.title
262 if not title:
264 if not title:
263 title = self.text.rendered
265 title = self.text.rendered
264
266
265 return title
267 return title
266
268
267 def build_refmap(self):
269 def build_refmap(self):
268 map_string = ''
270 map_string = ''
269
271
270 first = True
272 first = True
271 for refpost in self.referenced_posts.all():
273 for refpost in self.referenced_posts.all():
272 if not first:
274 if not first:
273 map_string += ', '
275 map_string += ', '
274 map_string += '<a href="%s">&gt;&gt;%s</a>' % (refpost.get_url(), refpost.id)
276 map_string += '<a href="%s">&gt;&gt;%s</a>' % (refpost.get_url(), refpost.id)
275 first = False
277 first = False
276
278
277 self.refmap = map_string
279 self.refmap = map_string
278
280
279 def get_sorted_referenced_posts(self):
281 def get_sorted_referenced_posts(self):
280 return self.refmap
282 return self.refmap
281
283
282 def is_referenced(self):
284 def is_referenced(self):
283 return len(self.refmap) > 0
285 return len(self.refmap) > 0
284
286
285 def is_opening(self):
287 def is_opening(self):
286 """
288 """
287 Checks if this is an opening post or just a reply.
289 Checks if this is an opening post or just a reply.
288 """
290 """
289
291
290 return self.get_thread().get_opening_post_id() == self.id
292 return self.get_thread().get_opening_post_id() == self.id
291
293
292 def save(self, *args, **kwargs):
294 def save(self, *args, **kwargs):
293 """
295 """
294 Saves the model and computes the image hash for deduplication purposes.
296 Saves the model and computes the image hash for deduplication purposes.
295 """
297 """
296
298
297 if not self.pk and self.image:
299 if not self.pk and self.image:
298 md5 = hashlib.md5()
300 md5 = hashlib.md5()
299 for chunk in self.image.chunks():
301 for chunk in self.image.chunks():
300 md5.update(chunk)
302 md5.update(chunk)
301 self.image_hash = md5.hexdigest()
303 self.image_hash = md5.hexdigest()
302 super(Post, self).save(*args, **kwargs)
304 super(Post, self).save(*args, **kwargs)
303
305
304 @transaction.atomic
306 @transaction.atomic
305 def add_tag(self, tag):
307 def add_tag(self, tag):
306 edit_time = timezone.now()
308 edit_time = timezone.now()
307
309
308 thread = self.get_thread()
310 thread = self.get_thread()
309 thread.add_tag(tag)
311 thread.add_tag(tag)
310 self.last_edit_time = edit_time
312 self.last_edit_time = edit_time
311 self.save()
313 self.save()
312
314
313 thread.last_edit_time = edit_time
315 thread.last_edit_time = edit_time
314 thread.save()
316 thread.save()
315
317
316 @transaction.atomic
318 @transaction.atomic
317 def remove_tag(self, tag):
319 def remove_tag(self, tag):
318 edit_time = timezone.now()
320 edit_time = timezone.now()
319
321
320 thread = self.get_thread()
322 thread = self.get_thread()
321 thread.remove_tag(tag)
323 thread.remove_tag(tag)
322 self.last_edit_time = edit_time
324 self.last_edit_time = edit_time
323 self.save()
325 self.save()
324
326
325 thread.last_edit_time = edit_time
327 thread.last_edit_time = edit_time
326 thread.save()
328 thread.save()
327
329
328 def get_url(self, thread=None):
330 def get_url(self, thread=None):
329 """
331 """
330 Gets full url to the post.
332 Gets full url to the post.
331 """
333 """
332
334
333 cache_key = CACHE_KEY_POST_URL + str(self.id)
335 cache_key = CACHE_KEY_POST_URL + str(self.id)
334 link = cache.get(cache_key)
336 link = cache.get(cache_key)
335
337
336 if not link:
338 if not link:
337 if not thread:
339 if not thread:
338 thread = self.get_thread()
340 thread = self.get_thread()
339
341
340 opening_id = thread.get_opening_post_id()
342 opening_id = thread.get_opening_post_id()
341
343
342 if self.id != opening_id:
344 if self.id != opening_id:
343 link = reverse('thread', kwargs={
345 link = reverse('thread', kwargs={
344 'post_id': opening_id}) + '#' + str(self.id)
346 'post_id': opening_id}) + '#' + str(self.id)
345 else:
347 else:
346 link = reverse('thread', kwargs={'post_id': self.id})
348 link = reverse('thread', kwargs={'post_id': self.id})
347
349
348 cache.set(cache_key, link)
350 cache.set(cache_key, link)
349
351
350 return link
352 return link
351
353
352 def get_thread(self):
354 def get_thread(self):
353 """
355 """
354 Gets post's thread.
356 Gets post's thread.
355 """
357 """
356
358
357 return self.thread_new
359 return self.thread_new
358
360
359 def get_referenced_posts(self):
361 def get_referenced_posts(self):
360 return self.referenced_posts.only('id', 'thread_new') No newline at end of file
362 return self.referenced_posts.only('id', 'thread_new')
363
364 def get_text(self):
365 return self.text
366
367 def get_view(self, moderator=False, need_open_link=False,
368 truncated=False, *args, **kwargs):
369 if 'is_opening' in kwargs:
370 is_opening = kwargs['is_opening']
371 else:
372 is_opening = self.is_opening()
373
374 if 'thread' in kwargs:
375 thread = kwargs['thread']
376 else:
377 thread = self.get_thread()
378
379 if 'can_bump' in kwargs:
380 can_bump = kwargs['can_bump']
381 else:
382 can_bump = thread.can_bump()
383
384 opening_post_id = thread.get_opening_post_id()
385
386 return render_to_string('boards/post.html', {
387 'post': self,
388 'moderator': moderator,
389 'is_opening': is_opening,
390 'thread': thread,
391 'bumpable': can_bump,
392 'need_open_link': need_open_link,
393 'truncated': truncated,
394 'opening_post_id': opening_post_id,
395 })
396
@@ -1,128 +1,139 b''
1 from django.template.loader import render_to_string
1 from boards.models import Thread, Post
2 from boards.models import Thread, Post
2 from django.db import models
3 from django.db import models
3 from django.db.models import Count, Sum
4 from django.db.models import Count, Sum
5 from django.core.urlresolvers import reverse
6 from boards.models.base import Viewable
4
7
5 __author__ = 'neko259'
8 __author__ = 'neko259'
6
9
7 MAX_TAG_FONT = 1
10 MAX_TAG_FONT = 1
8 MIN_TAG_FONT = 0.2
11 MIN_TAG_FONT = 0.2
9
12
10 TAG_POPULARITY_MULTIPLIER = 20
13 TAG_POPULARITY_MULTIPLIER = 20
11
14
12 ARCHIVE_POPULARITY_MODIFIER = 0.5
15 ARCHIVE_POPULARITY_MODIFIER = 0.5
13
16
14
17
15 class TagManager(models.Manager):
18 class TagManager(models.Manager):
16
19
17 def get_not_empty_tags(self):
20 def get_not_empty_tags(self):
18 """
21 """
19 Gets tags that have non-archived threads.
22 Gets tags that have non-archived threads.
20 """
23 """
21
24
22 tags = self.annotate(Count('threads')) \
25 tags = self.annotate(Count('threads')) \
23 .filter(threads__count__gt=0).filter(threads__archived=False) \
26 .filter(threads__count__gt=0).filter(threads__archived=False) \
24 .order_by('name')
27 .order_by('name')
25
28
26 return tags
29 return tags
27
30
28
31
29 class Tag(models.Model):
32 class Tag(models.Model, Viewable):
30 """
33 """
31 A tag is a text node assigned to the thread. The tag serves as a board
34 A tag is a text node assigned to the thread. The tag serves as a board
32 section. There can be multiple tags for each thread
35 section. There can be multiple tags for each thread
33 """
36 """
34
37
35 objects = TagManager()
38 objects = TagManager()
36
39
37 class Meta:
40 class Meta:
38 app_label = 'boards'
41 app_label = 'boards'
39 ordering = ('name',)
42 ordering = ('name',)
40
43
41 name = models.CharField(max_length=100, db_index=True)
44 name = models.CharField(max_length=100, db_index=True)
42 threads = models.ManyToManyField(Thread, null=True,
45 threads = models.ManyToManyField(Thread, null=True,
43 blank=True, related_name='tag+')
46 blank=True, related_name='tag+')
44 linked = models.ForeignKey('Tag', null=True, blank=True)
47 linked = models.ForeignKey('Tag', null=True, blank=True)
45
48
46 def __unicode__(self):
49 def __unicode__(self):
47 return self.name
50 return self.name
48
51
49 def is_empty(self):
52 def is_empty(self):
50 """
53 """
51 Checks if the tag has some threads.
54 Checks if the tag has some threads.
52 """
55 """
53
56
54 return self.get_thread_count() == 0
57 return self.get_thread_count() == 0
55
58
56 def get_thread_count(self):
59 def get_thread_count(self):
57 return self.threads.count()
60 return self.threads.count()
58
61
59 def get_popularity(self):
62 def get_popularity(self):
60 """
63 """
61 Gets tag's popularity value as a percentage of overall board post
64 Gets tag's popularity value as a percentage of overall board post
62 count.
65 count.
63 """
66 """
64
67
65 all_post_count = Post.objects.count()
68 all_post_count = Post.objects.count()
66
69
67 tag_reply_count = 0.0
70 tag_reply_count = 0.0
68
71
69 tag_reply_count += self.get_post_count()
72 tag_reply_count += self.get_post_count()
70 tag_reply_count +=\
73 tag_reply_count +=\
71 self.get_post_count(archived=True) * ARCHIVE_POPULARITY_MODIFIER
74 self.get_post_count(archived=True) * ARCHIVE_POPULARITY_MODIFIER
72
75
73 popularity = tag_reply_count / all_post_count
76 popularity = tag_reply_count / all_post_count
74
77
75 return popularity
78 return popularity
76
79
77 def get_linked_tags(self):
80 def get_linked_tags(self):
78 """
81 """
79 Gets tags linked to the current one.
82 Gets tags linked to the current one.
80 """
83 """
81
84
82 tag_list = []
85 tag_list = []
83 self.get_linked_tags_list(tag_list)
86 self.get_linked_tags_list(tag_list)
84
87
85 return tag_list
88 return tag_list
86
89
87 def get_linked_tags_list(self, tag_list=[]):
90 def get_linked_tags_list(self, tag_list=[]):
88 """
91 """
89 Returns the list of tags linked to current. The list can be got
92 Returns the list of tags linked to current. The list can be got
90 through returned value or tag_list parameter
93 through returned value or tag_list parameter
91 """
94 """
92
95
93 linked_tag = self.linked
96 linked_tag = self.linked
94
97
95 if linked_tag and not (linked_tag in tag_list):
98 if linked_tag and not (linked_tag in tag_list):
96 tag_list.append(linked_tag)
99 tag_list.append(linked_tag)
97
100
98 linked_tag.get_linked_tags_list(tag_list)
101 linked_tag.get_linked_tags_list(tag_list)
99
102
100 def get_font_value(self):
103 def get_font_value(self):
101 """
104 """
102 Gets tag font value to differ most popular tags in the list
105 Gets tag font value to differ most popular tags in the list
103 """
106 """
104
107
105 popularity = self.get_popularity()
108 popularity = self.get_popularity()
106
109
107 font_value = popularity * Tag.objects.get_not_empty_tags().count()
110 font_value = popularity * Tag.objects.get_not_empty_tags().count()
108 font_value = max(font_value, MIN_TAG_FONT)
111 font_value = max(font_value, MIN_TAG_FONT)
109 font_value = min(font_value, MAX_TAG_FONT)
112 font_value = min(font_value, MAX_TAG_FONT)
110
113
111 return str(font_value)
114 return str(font_value)
112
115
113 def get_post_count(self, archived=False):
116 def get_post_count(self, archived=False):
114 """
117 """
115 Gets posts count for the tag's threads.
118 Gets posts count for the tag's threads.
116 """
119 """
117
120
118 posts_count = 0
121 posts_count = 0
119
122
120 threads = self.threads.filter(archived=archived)
123 threads = self.threads.filter(archived=archived)
121 if threads.exists():
124 if threads.exists():
122 posts_count = threads.annotate(posts_count=Count('replies')).aggregate(
125 posts_count = threads.annotate(posts_count=Count('replies')).aggregate(
123 posts_sum=Sum('posts_count'))['posts_sum']
126 posts_sum=Sum('posts_count'))['posts_sum']
124
127
125 if not posts_count:
128 if not posts_count:
126 posts_count = 0
129 posts_count = 0
127
130
128 return posts_count
131 return posts_count
132
133 def get_url(self):
134 return reverse('tag', kwargs={'tag_name': self.name})
135
136 def get_view(self, *args, **kwargs):
137 return render_to_string('boards/tag.html', {
138 'tag': self,
139 }) No newline at end of file
@@ -1,14 +1,24 b''
1 from haystack import indexes
1 from haystack import indexes
2 from boards.models import Post
2 from boards.models import Post, Tag
3
3
4 __author__ = 'neko259'
4 __author__ = 'neko259'
5
5
6
6
7 class PostIndex(indexes.SearchIndex, indexes.Indexable):
7 class PostIndex(indexes.SearchIndex, indexes.Indexable):
8 text = indexes.CharField(document=True, use_template=True)
8 text = indexes.CharField(document=True, use_template=True)
9
9
10 def get_model(self):
10 def get_model(self):
11 return Post
11 return Post
12
12
13 def index_queryset(self, using=None):
13 def index_queryset(self, using=None):
14 return self.get_model().objects.all() No newline at end of file
14 return self.get_model().objects.all()
15
16
17 class TagIndex(indexes.SearchIndex, indexes.Indexable):
18 text = indexes.CharField(document=True, use_template=True)
19
20 def get_model(self):
21 return Tag
22
23 def index_queryset(self, using=None):
24 return self.get_model().objects.get_not_empty_tags()
@@ -1,96 +1,3 b''
1 {% load i18n %}
1 <div class="post">
2 {% load board %}
2 <a class="tag" href="{% url 'tag' tag_name=tag.name %}">#{{ tag.name }}</a>
3 {% load cache %}
3 </div> No newline at end of file
4
5 {% get_current_language as LANGUAGE_CODE %}
6
7 {% spaceless %}
8 {% cache 600 post post.id post.last_edit_time thread.archived bumpable truncated moderator LANGUAGE_CODE need_open_link %}
9 {% if thread.archived %}
10 <div class="post archive_post" id="{{ post.id }}">
11 {% elif bumpable %}
12 <div class="post" id="{{ post.id }}">
13 {% else %}
14 <div class="post dead_post" id="{{ post.id }}">
15 {% endif %}
16
17 <div class="post-info">
18 <a class="post_id" href="{% post_object_url post thread=thread %}"
19 {% if not truncated and not thread.archived %}
20 onclick="javascript:addQuickReply('{{ post.id }}'); return false;"
21 title="{% trans 'Quote' %}"
22 {% endif %}
23 >({{ post.id }}) </a>
24 <span class="title">{{ post.title }} </span>
25 <span class="pub_time">{{ post.pub_time }}</span>
26 {% if thread.archived %}
27 β€” {{ thread.bump_time }}
28 {% endif %}
29 {% if is_opening and need_open_link %}
30 {% if thread.archived %}
31 [<a class="link" href="{% url 'thread' post.id %}">{% trans "Open" %}</a>]
32 {% else %}
33 [<a class="link" href="{% url 'thread' post.id %}#form">{% trans "Reply" %}</a>]
34 {% endif %}
35 {% endif %}
36
37 {% if moderator %}
38 <span class="moderator_info">
39 [<a href="{% url 'post_admin' post_id=post.id %}"
40 >{% trans 'Edit' %}</a>]
41 [<a href="{% url 'delete' post_id=post.id %}"
42 >{% trans 'Delete' %}</a>]
43 ({{ post.poster_ip }})
44 [<a href="{% url 'ban' post_id=post.id %}?next={{ request.path }}"
45 >{% trans 'Ban IP' %}</a>]
46 </span>
47 {% endif %}
48 </div>
49 {% if post.image %}
50 <div class="image">
51 <a
52 class="thumb"
53 href="{{ post.image.url }}"><img
54 src="{{ post.image.url_200x150 }}"
55 alt="{{ post.id }}"
56 width="{{ post.image_pre_width }}"
57 height="{{ post.image_pre_height }}"
58 data-width="{{ post.image_width }}"
59 data-height="{{ post.image_height }}"/>
60 </a>
61 </div>
62 {% endif %}
63 <div class="message">
64 {% autoescape off %}
65 {% if truncated %}
66 {{ post.text.rendered|truncatewords_html:50 }}
67 {% else %}
68 {{ post.text.rendered }}
69 {% endif %}
70 {% endautoescape %}
71 {% if post.is_referenced %}
72 <div class="refmap">
73 {% autoescape off %}
74 {% trans "Replies" %}: {{ post.refmap }}
75 {% endautoescape %}
76 </div>
77 {% endif %}
78 </div>
79 {% endcache %}
80 {% if is_opening %}
81 {% cache 600 post_thread thread.id thread.last_edit_time LANGUAGE_CODE need_open_link %}
82 <div class="metadata">
83 {% if is_opening and need_open_link %}
84 {{ thread.get_images_count }} {% trans 'images' %}.
85 {% endif %}
86 <span class="tags">
87 {% for tag in thread.get_tags %}
88 <a class="tag" href="{% url 'tag' tag.name %}">
89 #{{ tag.name }}</a>{% if not forloop.last %},{% endif %}
90 {% endfor %}
91 </span>
92 </div>
93 {% endcache %}
94 {% endif %}
95 </div>
96 {% endspaceless %}
@@ -1,89 +1,89 b''
1 from django.shortcuts import get_object_or_404
1 from django.shortcuts import get_object_or_404
2 from boards.models import Post
3 from boards.views import thread, api
4 from django import template
2 from django import template
5
3
4
6 register = template.Library()
5 register = template.Library()
7
6
8 actions = [
7 actions = [
9 {
8 {
10 'name': 'google',
9 'name': 'google',
11 'link': 'http://google.com/searchbyimage?image_url=%s',
10 'link': 'http://google.com/searchbyimage?image_url=%s',
12 },
11 },
13 {
12 {
14 'name': 'iqdb',
13 'name': 'iqdb',
15 'link': 'http://iqdb.org/?url=%s',
14 'link': 'http://iqdb.org/?url=%s',
16 },
15 },
17 ]
16 ]
18
17
19
18
20 @register.simple_tag(name='post_url')
19 @register.simple_tag(name='post_url')
21 def post_url(*args, **kwargs):
20 def post_url(*args, **kwargs):
22 post_id = args[0]
21 post_id = args[0]
23
22
24 post = get_object_or_404(Post, id=post_id)
23 post = get_object_or_404('Post', id=post_id)
25
24
26 return post.get_url()
25 return post.get_url()
27
26
28
27
29 @register.simple_tag(name='post_object_url')
28 @register.simple_tag(name='post_object_url')
30 def post_object_url(*args, **kwargs):
29 def post_object_url(*args, **kwargs):
31 post = args[0]
30 post = args[0]
32
31
33 if 'thread' in kwargs:
32 if 'thread' in kwargs:
34 post_thread = kwargs['thread']
33 post_thread = kwargs['thread']
35 else:
34 else:
36 post_thread = None
35 post_thread = None
37
36
38 return post.get_url(thread=post_thread)
37 return post.get_url(thread=post_thread)
39
38
40
39
41 @register.simple_tag(name='image_actions')
40 @register.simple_tag(name='image_actions')
42 def image_actions(*args, **kwargs):
41 def image_actions(*args, **kwargs):
43 image_link = args[0]
42 image_link = args[0]
44 if len(args) > 1:
43 if len(args) > 1:
45 image_link = 'http://' + args[1] + image_link # TODO https?
44 image_link = 'http://' + args[1] + image_link # TODO https?
46
45
47 result = ''
46 result = ''
48
47
49 for action in actions:
48 for action in actions:
50 result += '[<a href="' + action['link'] % image_link + '">' + \
49 result += '[<a href="' + action['link'] % image_link + '">' + \
51 action['name'] + '</a>]'
50 action['name'] + '</a>]'
52
51
53 return result
52 return result
54
53
55
54
55 # TODO Use get_view of a post instead of this
56 @register.inclusion_tag('boards/post.html', name='post_view')
56 @register.inclusion_tag('boards/post.html', name='post_view')
57 def post_view(post, moderator=False, need_open_link=False, truncated=False,
57 def post_view(post, moderator=False, need_open_link=False, truncated=False,
58 **kwargs):
58 **kwargs):
59 """
59 """
60 Get post
60 Get post
61 """
61 """
62
62
63 if 'is_opening' in kwargs:
63 if 'is_opening' in kwargs:
64 is_opening = kwargs['is_opening']
64 is_opening = kwargs['is_opening']
65 else:
65 else:
66 is_opening = post.is_opening()
66 is_opening = post.is_opening()
67
67
68 if 'thread' in kwargs:
68 if 'thread' in kwargs:
69 thread = kwargs['thread']
69 thread = kwargs['thread']
70 else:
70 else:
71 thread = post.get_thread()
71 thread = post.get_thread()
72
72
73 if 'can_bump' in kwargs:
73 if 'can_bump' in kwargs:
74 can_bump = kwargs['can_bump']
74 can_bump = kwargs['can_bump']
75 else:
75 else:
76 can_bump = thread.can_bump()
76 can_bump = thread.can_bump()
77
77
78 opening_post_id = thread.get_opening_post_id()
78 opening_post_id = thread.get_opening_post_id()
79
79
80 return {
80 return {
81 'post': post,
81 'post': post,
82 'moderator': moderator,
82 'moderator': moderator,
83 'is_opening': is_opening,
83 'is_opening': is_opening,
84 'thread': thread,
84 'thread': thread,
85 'bumpable': can_bump,
85 'bumpable': can_bump,
86 'need_open_link': need_open_link,
86 'need_open_link': need_open_link,
87 'truncated': truncated,
87 'truncated': truncated,
88 'opening_post_id': opening_post_id,
88 'opening_post_id': opening_post_id,
89 }
89 }
@@ -1,8 +1,9 b''
1 haystack
1 pillow
2 pillow
2 django>=1.6
3 django>=1.6
3 django_cleanup
4 django_cleanup
4 django-markupfield
5 django-markupfield
5 markdown
6 markdown
6 python-markdown
7 python-markdown
7 django-simple-captcha
8 django-simple-captcha
8 line-profiler
9 line-profiler
@@ -1,2 +1,1 b''
1 {{ object.title }}
1 {{ object.name }} No newline at end of file
2 {{ object.text }} No newline at end of file
@@ -1,29 +1,29 b''
1 {% extends 'boards/base.html' %}
1 {% extends 'boards/base.html' %}
2
2
3 {% load board %}
3 {% load board %}
4 {% load i18n %}
4 {% load i18n %}
5
5
6 {% block content %}
6 {% block content %}
7 <div class="post-form-w">
7 <div class="post-form-w">
8 <h3>{% trans 'Search' %}</h3>
8 <h3>{% trans 'Search' %}</h3>
9 <form method="get" action=".">
9 <form method="get" action=".">
10 {{ form.as_p }}
10 {{ form.as_p }}
11 <input type="submit" value="{% trans 'Search' %}">
11 <input type="submit" value="{% trans 'Search' %}">
12 </form>
12 </form>
13 </div>
13 </div>
14
14
15 {% if query %}
15 {% if query %}
16 {% for result in page.object_list %}
16 {% for result in page.object_list %}
17 {% post_view result.object %}
17 {{ result.object.get_view }}
18 {% empty %}
18 {% empty %}
19 <p>{% trans 'No results found.' %}</p>
19 <div class="post">{% trans 'No results found.' %}</div>
20 {% endfor %}
20 {% endfor %}
21
21
22 {% if page.has_previous or page.has_next %}
22 {% if page.has_previous or page.has_next %}
23 <div>
23 <div>
24 {% if page.has_previous %}<a href="?q={{ query }}&amp;page={{ page.previous_page_number }}">{% endif %}&laquo; {% trans 'Previous' %}{% if page.has_previous %}</a>{% endif %}
24 {% if page.has_previous %}<a href="?q={{ query }}&amp;page={{ page.previous_page_number }}">{% endif %}&laquo; {% trans 'Previous' %}{% if page.has_previous %}</a>{% endif %}
25 {% if page.has_next %}<a href="?q={{ query }}&amp;page= {{ page.next_page_number }}">{% endif %}{% trans 'Next' %} &raquo; {% if page.has_next %}</a>{% endif %}
25 {% if page.has_next %}<a href="?q={{ query }}&amp;page= {{ page.next_page_number }}">{% endif %}{% trans 'Next' %} &raquo; {% if page.has_next %}</a>{% endif %}
26 </div>
26 </div>
27 {% endif %}
27 {% endif %}
28 {% endif %}
28 {% endif %}
29 {% endblock %} No newline at end of file
29 {% endblock %}
General Comments 0
You need to be logged in to leave comments. Login now