##// END OF EJS Templates
Code cleanup. Update only edited fields while performing thread archiving or post editing. Remove image when post is removed
neko259 -
r715:056b308f default
parent child Browse files
Show More
@@ -1,75 +1,73 b''
1 1 # -*- coding: utf-8 -*-
2 import datetime
3 2 from south.db import db
4 3 from south.v2 import SchemaMigration
5 4 from django.db import models
6 from boards.models.post import Post, NO_PARENT
7 5
8 6
9 7 class Migration(SchemaMigration):
10 8
11 9 def forwards(self, orm):
12 10 # Adding M2M table for field replies on 'Post'
13 11 m2m_table_name = db.shorten_name(u'boards_post_replies')
14 12 db.create_table(m2m_table_name, (
15 13 ('id', models.AutoField(verbose_name='ID', primary_key=True, auto_created=True)),
16 14 ('from_post', models.ForeignKey(orm[u'boards.post'], null=False)),
17 15 ('to_post', models.ForeignKey(orm[u'boards.post'], null=False))
18 16 ))
19 17 db.create_unique(m2m_table_name, ['from_post_id', 'to_post_id'])
20 18
21 19 def backwards(self, orm):
22 20 # Removing M2M table for field replies on 'Post'
23 21 db.delete_table(db.shorten_name(u'boards_post_replies'))
24 22
25 23
26 24 models = {
27 25 u'boards.ban': {
28 26 'Meta': {'object_name': 'Ban'},
29 27 u'id': ('django.db.models.fields.AutoField', [], {'primary_key': 'True'}),
30 28 'ip': ('django.db.models.fields.GenericIPAddressField', [], {'max_length': '39'})
31 29 },
32 30 u'boards.post': {
33 31 'Meta': {'object_name': 'Post'},
34 32 '_text_rendered': ('django.db.models.fields.TextField', [], {}),
35 33 u'id': ('django.db.models.fields.AutoField', [], {'primary_key': 'True'}),
36 34 'image': ('boards.thumbs.ImageWithThumbsField', [], {'max_length': '100', 'blank': 'True'}),
37 35 'image_height': ('django.db.models.fields.IntegerField', [], {'default': '0'}),
38 36 'image_width': ('django.db.models.fields.IntegerField', [], {'default': '0'}),
39 37 'last_edit_time': ('django.db.models.fields.DateTimeField', [], {}),
40 38 'parent': ('django.db.models.fields.BigIntegerField', [], {}),
41 39 'poster_ip': ('django.db.models.fields.GenericIPAddressField', [], {'max_length': '39'}),
42 40 'poster_user_agent': ('django.db.models.fields.TextField', [], {}),
43 41 'pub_time': ('django.db.models.fields.DateTimeField', [], {}),
44 42 'replies': ('django.db.models.fields.related.ManyToManyField', [], {'blank': 'True', 'related_name': "'re+'", 'null': 'True', 'symmetrical': 'False', 'to': u"orm['boards.Post']"}),
45 43 'tags': ('django.db.models.fields.related.ManyToManyField', [], {'to': u"orm['boards.Tag']", 'symmetrical': 'False'}),
46 44 'text': ('markupfield.fields.MarkupField', [], {'rendered_field': 'True'}),
47 45 'text_markup_type': ('django.db.models.fields.CharField', [], {'default': "'markdown'", 'max_length': '30'}),
48 46 'title': ('django.db.models.fields.CharField', [], {'max_length': '50'}),
49 47 'user': ('django.db.models.fields.related.ForeignKey', [], {'default': 'None', 'to': u"orm['boards.User']", 'null': 'True'})
50 48 },
51 49 u'boards.setting': {
52 50 'Meta': {'object_name': 'Setting'},
53 51 u'id': ('django.db.models.fields.AutoField', [], {'primary_key': 'True'}),
54 52 'name': ('django.db.models.fields.CharField', [], {'max_length': '50'}),
55 53 'user': ('django.db.models.fields.related.ForeignKey', [], {'to': u"orm['boards.User']"}),
56 54 'value': ('django.db.models.fields.CharField', [], {'max_length': '50'})
57 55 },
58 56 u'boards.tag': {
59 57 'Meta': {'object_name': 'Tag'},
60 58 u'id': ('django.db.models.fields.AutoField', [], {'primary_key': 'True'}),
61 59 'name': ('django.db.models.fields.CharField', [], {'max_length': '100'})
62 60 },
63 61 u'boards.user': {
64 62 'Meta': {'object_name': 'User'},
65 63 'fav_tags': ('django.db.models.fields.related.ManyToManyField', [], {'symmetrical': 'False', 'to': u"orm['boards.Tag']", 'null': 'True', 'blank': 'True'}),
66 64 'fav_threads': ('django.db.models.fields.related.ManyToManyField', [], {'blank': 'True', 'related_name': "'+'", 'null': 'True', 'symmetrical': 'False', 'to': u"orm['boards.Post']"}),
67 65 u'id': ('django.db.models.fields.AutoField', [], {'primary_key': 'True'}),
68 66 'last_access_time': ('django.db.models.fields.DateTimeField', [], {}),
69 67 'rank': ('django.db.models.fields.IntegerField', [], {}),
70 68 'registration_time': ('django.db.models.fields.DateTimeField', [], {}),
71 69 'user_id': ('django.db.models.fields.CharField', [], {'max_length': '50'})
72 70 }
73 71 }
74 72
75 73 complete_apps = ['boards'] No newline at end of file
@@ -1,381 +1,354 b''
1 1 from datetime import datetime, timedelta, date
2 2 from datetime import time as dtime
3 3 import logging
4 4 import os
5 5 from random import random
6 6 import time
7 7 import re
8 8 import hashlib
9 9
10 10 from django.core.cache import cache
11 11 from django.core.urlresolvers import reverse
12 12 from django.db import models, transaction
13 13 from django.template.loader import render_to_string
14 14 from django.utils import timezone
15 15 from markupfield.fields import MarkupField
16 16 from boards.models import PostImage
17 17 from boards.models.base import Viewable
18 18
19 19 from boards.models.thread import Thread
20 20 from neboard import settings
21 21 from boards import thumbs
22 22
23 23
24 24 APP_LABEL_BOARDS = 'boards'
25 25
26 26 CACHE_KEY_PPD = 'ppd'
27 27 CACHE_KEY_POST_URL = 'post_url'
28 28
29 29 POSTS_PER_DAY_RANGE = range(7)
30 30
31 31 BAN_REASON_AUTO = 'Auto'
32 32
33 33 IMAGE_THUMB_SIZE = (200, 150)
34 34
35 35 TITLE_MAX_LENGTH = 200
36 36
37 37 DEFAULT_MARKUP_TYPE = 'markdown'
38 38
39 # TODO This should be removed when no code relies on it because thread id field
40 # was removed a long time ago
41 NO_PARENT = -1
42
43 39 # TODO This should be removed
44 40 NO_IP = '0.0.0.0'
45 41
46 42 # TODO Real user agent should be saved instead of this
47 43 UNKNOWN_UA = ''
48 44
49 # TODO This should be checked for usage and removed because a nativa
50 # paginator is used now
51 ALL_PAGES = -1
52
53 IMAGES_DIRECTORY = 'images/'
54 FILE_EXTENSION_DELIMITER = '.'
55
56 45 SETTING_MODERATE = "moderate"
57 46
58 47 REGEX_REPLY = re.compile('>>(\d+)')
59 48
60 49 logger = logging.getLogger(__name__)
61 50
62 51
63 52 class PostManager(models.Manager):
64 53
65 54 def create_post(self, title, text, image=None, thread=None,
66 55 ip=NO_IP, tags=None, user=None):
67 56 """
68 57 Creates new post
69 58 """
70 59
71 60 posting_time = timezone.now()
72 61 if not thread:
73 62 thread = Thread.objects.create(bump_time=posting_time,
74 63 last_edit_time=posting_time)
75 64 new_thread = True
76 65 else:
77 66 thread.bump()
78 67 thread.last_edit_time = posting_time
79 68 thread.save()
80 69 new_thread = False
81 70
82 71 post = self.create(title=title,
83 72 text=text,
84 73 pub_time=posting_time,
85 74 thread_new=thread,
86 75 poster_ip=ip,
87 76 poster_user_agent=UNKNOWN_UA, # TODO Get UA at
88 77 # last!
89 78 last_edit_time=posting_time,
90 79 user=user)
91 80
92 81 if image:
93 82 post_image = PostImage.objects.create(image=image)
94 83 post.images.add(post_image)
95 84 logger.info('Created image #%d for post #%d' % (post_image.id,
96 85 post.id))
97 86
98 87 thread.replies.add(post)
99 88 if tags:
100 89 linked_tags = []
101 90 for tag in tags:
102 91 tag_linked_tags = tag.get_linked_tags()
103 92 if len(tag_linked_tags) > 0:
104 93 linked_tags.extend(tag_linked_tags)
105 94
106 95 tags.extend(linked_tags)
107 96 map(thread.add_tag, tags)
108 97
109 98 if new_thread:
110 self._delete_old_threads()
99 Thread.objects.archive_oldest_threads()
111 100 self.connect_replies(post)
112 101
113 102 logger.info('Created post #%d' % post.id)
114 103
115 104 return post
116 105
117 106 def delete_post(self, post):
118 107 """
119 108 Deletes post and update or delete its thread
120 109 """
121 110
122 111 post_id = post.id
123 112
124 113 thread = post.get_thread()
125 114
126 115 if post.is_opening():
127 116 thread.delete_with_posts()
128 117 else:
129 118 thread.last_edit_time = timezone.now()
130 119 thread.save()
131 120
132 121 post.delete()
133 122
134 123 logger.info('Deleted post #%d' % post_id)
135 124
136 125 def delete_posts_by_ip(self, ip):
137 126 """
138 127 Deletes all posts of the author with same IP
139 128 """
140 129
141 130 posts = self.filter(poster_ip=ip)
142 131 map(self.delete_post, posts)
143 132
144 # TODO Move this method to thread manager
145 # TODO Rename it, because the threads are archived instead of plain
146 # removal. Split the delete and archive methods and make a setting to
147 # enable or disable archiving.
148 def _delete_old_threads(self):
149 """
150 Preserves maximum thread count. If there are too many threads,
151 archive the old ones.
152 """
153
154 threads = Thread.objects.filter(archived=False).order_by('-bump_time')
155 thread_count = threads.count()
156
157 if thread_count > settings.MAX_THREAD_COUNT:
158 num_threads_to_delete = thread_count - settings.MAX_THREAD_COUNT
159 old_threads = threads[thread_count - num_threads_to_delete:]
160
161 for thread in old_threads:
162 thread.archived = True
163 thread.last_edit_time = timezone.now()
164 thread.save()
165
166 logger.info('Archived %d old threads' % num_threads_to_delete)
167
168 133 def connect_replies(self, post):
169 134 """
170 135 Connects replies to a post to show them as a reflink map
171 136 """
172 137
173 138 for reply_number in re.finditer(REGEX_REPLY, post.text.raw):
174 139 post_id = reply_number.group(1)
175 140 ref_post = self.filter(id=post_id)
176 141 if ref_post.count() > 0:
177 142 referenced_post = ref_post[0]
178 143 referenced_post.referenced_posts.add(post)
179 144 referenced_post.last_edit_time = post.pub_time
180 145 referenced_post.build_refmap()
181 referenced_post.save()
146 referenced_post.save(update_fields=['refmap', 'last_edit_time'])
182 147
183 148 referenced_thread = referenced_post.get_thread()
184 149 referenced_thread.last_edit_time = post.pub_time
185 referenced_thread.save()
150 referenced_thread.save(update_fields=['last_edit_time'])
186 151
187 152 def get_posts_per_day(self):
188 153 """
189 154 Gets average count of posts per day for the last 7 days
190 155 """
191 156
192 157 today = date.today()
193 158 ppd = cache.get(CACHE_KEY_PPD + str(today))
194 159 if ppd:
195 160 return ppd
196 161
197 162 posts_per_days = []
198 163 for i in POSTS_PER_DAY_RANGE:
199 164 day_end = today - timedelta(i + 1)
200 165 day_start = today - timedelta(i + 2)
201 166
202 167 day_time_start = timezone.make_aware(datetime.combine(
203 168 day_start, dtime()), timezone.get_current_timezone())
204 169 day_time_end = timezone.make_aware(datetime.combine(
205 170 day_end, dtime()), timezone.get_current_timezone())
206 171
207 172 posts_per_days.append(float(self.filter(
208 173 pub_time__lte=day_time_end,
209 174 pub_time__gte=day_time_start).count()))
210 175
211 176 ppd = (sum(posts_per_day for posts_per_day in posts_per_days) /
212 177 len(posts_per_days))
213 178 cache.set(CACHE_KEY_PPD + str(today), ppd)
214 179 return ppd
215 180
216 181
217 182 class Post(models.Model, Viewable):
218 183 """A post is a message."""
219 184
220 185 objects = PostManager()
221 186
222 187 class Meta:
223 188 app_label = APP_LABEL_BOARDS
224 189 ordering = ('id',)
225 190
226 191 title = models.CharField(max_length=TITLE_MAX_LENGTH)
227 192 pub_time = models.DateTimeField()
228 193 text = MarkupField(default_markup_type=DEFAULT_MARKUP_TYPE,
229 194 escape_html=False)
230 195
231 196 images = models.ManyToManyField(PostImage, null=True, blank=True,
232 197 related_name='ip+', db_index=True)
233 198
234 199 poster_ip = models.GenericIPAddressField()
235 200 poster_user_agent = models.TextField()
236 201
237 202 thread_new = models.ForeignKey('Thread', null=True, default=None,
238 db_index=True)
203 db_index=True)
239 204 last_edit_time = models.DateTimeField()
240 205 user = models.ForeignKey('User', null=True, default=None, db_index=True)
241 206
242 207 referenced_posts = models.ManyToManyField('Post', symmetrical=False,
243 208 null=True,
244 209 blank=True, related_name='rfp+',
245 210 db_index=True)
246 211 refmap = models.TextField(null=True, blank=True)
247 212
248 213 def __unicode__(self):
249 214 return '#' + str(self.id) + ' ' + self.title + ' (' + \
250 215 self.text.raw[:50] + ')'
251 216
252 217 def get_title(self):
253 218 """
254 219 Gets original post title or part of its text.
255 220 """
256 221
257 222 title = self.title
258 223 if not title:
259 224 title = self.text.rendered
260 225
261 226 return title
262 227
263 228 def build_refmap(self):
264 229 map_string = ''
265 230
266 231 first = True
267 232 for refpost in self.referenced_posts.all():
268 233 if not first:
269 234 map_string += ', '
270 235 map_string += '<a href="%s">&gt;&gt;%s</a>' % (refpost.get_url(), refpost.id)
271 236 first = False
272 237
273 238 self.refmap = map_string
274 239
275 240 def get_sorted_referenced_posts(self):
276 241 return self.refmap
277 242
278 243 def is_referenced(self):
279 244 return len(self.refmap) > 0
280 245
281 246 def is_opening(self):
282 247 """
283 248 Checks if this is an opening post or just a reply.
284 249 """
285 250
286 251 return self.get_thread().get_opening_post_id() == self.id
287 252
288 253 @transaction.atomic
289 254 def add_tag(self, tag):
290 255 edit_time = timezone.now()
291 256
292 257 thread = self.get_thread()
293 258 thread.add_tag(tag)
294 259 self.last_edit_time = edit_time
295 260 self.save()
296 261
297 262 thread.last_edit_time = edit_time
298 263 thread.save()
299 264
300 265 @transaction.atomic
301 266 def remove_tag(self, tag):
302 267 edit_time = timezone.now()
303 268
304 269 thread = self.get_thread()
305 270 thread.remove_tag(tag)
306 271 self.last_edit_time = edit_time
307 272 self.save()
308 273
309 274 thread.last_edit_time = edit_time
310 275 thread.save()
311 276
312 277 def get_url(self, thread=None):
313 278 """
314 279 Gets full url to the post.
315 280 """
316 281
317 282 cache_key = CACHE_KEY_POST_URL + str(self.id)
318 283 link = cache.get(cache_key)
319 284
320 285 if not link:
321 286 if not thread:
322 287 thread = self.get_thread()
323 288
324 289 opening_id = thread.get_opening_post_id()
325 290
326 291 if self.id != opening_id:
327 292 link = reverse('thread', kwargs={
328 293 'post_id': opening_id}) + '#' + str(self.id)
329 294 else:
330 295 link = reverse('thread', kwargs={'post_id': self.id})
331 296
332 297 cache.set(cache_key, link)
333 298
334 299 return link
335 300
336 301 def get_thread(self):
337 302 """
338 303 Gets post's thread.
339 304 """
340 305
341 306 return self.thread_new
342 307
343 308 def get_referenced_posts(self):
344 309 return self.referenced_posts.only('id', 'thread_new')
345 310
346 311 def get_text(self):
347 312 return self.text
348 313
349 314 def get_view(self, moderator=False, need_open_link=False,
350 315 truncated=False, *args, **kwargs):
351 316 if 'is_opening' in kwargs:
352 317 is_opening = kwargs['is_opening']
353 318 else:
354 319 is_opening = self.is_opening()
355 320
356 321 if 'thread' in kwargs:
357 322 thread = kwargs['thread']
358 323 else:
359 324 thread = self.get_thread()
360 325
361 326 if 'can_bump' in kwargs:
362 327 can_bump = kwargs['can_bump']
363 328 else:
364 329 can_bump = thread.can_bump()
365 330
366 331 opening_post_id = thread.get_opening_post_id()
367 332
368 333 return render_to_string('boards/post.html', {
369 334 'post': self,
370 335 'moderator': moderator,
371 336 'is_opening': is_opening,
372 337 'thread': thread,
373 338 'bumpable': can_bump,
374 339 'need_open_link': need_open_link,
375 340 'truncated': truncated,
376 341 'opening_post_id': opening_post_id,
377 342 })
378 343
379 344 def get_first_image(self):
380 345 return self.images.earliest('id')
381 346
347 def delete(self, using=None):
348 """
349 Delete all post images and the post itself.
350 """
351
352 self.images.all().delete()
353
354 super(Post, self).delete(using) No newline at end of file
@@ -1,141 +1,100 b''
1 1 from django.template.loader import render_to_string
2 2 from boards.models import Thread, Post
3 3 from django.db import models
4 4 from django.db.models import Count, Sum
5 5 from django.core.urlresolvers import reverse
6 6 from boards.models.base import Viewable
7 7
8 8 __author__ = 'neko259'
9 9
10 # TODO Tag popularity ratings are not used any more, remove all of this
11 MAX_TAG_FONT = 1
12 MIN_TAG_FONT = 0.2
13
14 TAG_POPULARITY_MULTIPLIER = 20
15
16 ARCHIVE_POPULARITY_MODIFIER = 0.5
17
18 10
19 11 class TagManager(models.Manager):
20 12
21 13 def get_not_empty_tags(self):
22 14 """
23 15 Gets tags that have non-archived threads.
24 16 """
25 17
26 18 tags = self.annotate(Count('threads')) \
27 19 .filter(threads__count__gt=0).order_by('name')
28 20
29 21 return tags
30 22
31 23
32 24 class Tag(models.Model, Viewable):
33 25 """
34 26 A tag is a text node assigned to the thread. The tag serves as a board
35 27 section. There can be multiple tags for each thread
36 28 """
37 29
38 30 objects = TagManager()
39 31
40 32 class Meta:
41 33 app_label = 'boards'
42 34 ordering = ('name',)
43 35
44 36 name = models.CharField(max_length=100, db_index=True)
45 37 threads = models.ManyToManyField(Thread, null=True,
46 38 blank=True, related_name='tag+')
47 39 linked = models.ForeignKey('Tag', null=True, blank=True)
48 40
49 41 def __unicode__(self):
50 42 return self.name
51 43
52 44 def is_empty(self):
53 45 """
54 46 Checks if the tag has some threads.
55 47 """
56 48
57 49 return self.get_thread_count() == 0
58 50
59 51 def get_thread_count(self):
60 52 return self.threads.count()
61 53
62 # TODO Remove, not used any more
63 def get_popularity(self):
64 """
65 Gets tag's popularity value as a percentage of overall board post
66 count.
67 """
68
69 all_post_count = Post.objects.count()
70
71 tag_reply_count = 0.0
72
73 tag_reply_count += self.get_post_count()
74 tag_reply_count +=\
75 self.get_post_count(archived=True) * ARCHIVE_POPULARITY_MODIFIER
76
77 popularity = tag_reply_count / all_post_count
78
79 return popularity
80
81 54 def get_linked_tags(self):
82 55 """
83 56 Gets tags linked to the current one.
84 57 """
85 58
86 59 tag_list = []
87 60 self.get_linked_tags_list(tag_list)
88 61
89 62 return tag_list
90 63
91 64 def get_linked_tags_list(self, tag_list=[]):
92 65 """
93 66 Returns the list of tags linked to current. The list can be got
94 67 through returned value or tag_list parameter
95 68 """
96 69
97 70 linked_tag = self.linked
98 71
99 72 if linked_tag and not (linked_tag in tag_list):
100 73 tag_list.append(linked_tag)
101 74
102 75 linked_tag.get_linked_tags_list(tag_list)
103 76
104 # TODO Remove
105 def get_font_value(self):
106 """
107 Gets tag font value to differ most popular tags in the list
108 """
109
110 popularity = self.get_popularity()
111
112 font_value = popularity * Tag.objects.get_not_empty_tags().count()
113 font_value = max(font_value, MIN_TAG_FONT)
114 font_value = min(font_value, MAX_TAG_FONT)
115
116 return str(font_value)
117
118 77 def get_post_count(self, archived=False):
119 78 """
120 79 Gets posts count for the tag's threads.
121 80 """
122 81
123 82 posts_count = 0
124 83
125 84 threads = self.threads.filter(archived=archived)
126 85 if threads.exists():
127 posts_count = threads.annotate(posts_count=Count('replies')).aggregate(
128 posts_sum=Sum('posts_count'))['posts_sum']
86 posts_count = threads.annotate(posts_count=Count('replies')) \
87 .aggregate(posts_sum=Sum('posts_count'))['posts_sum']
129 88
130 89 if not posts_count:
131 90 posts_count = 0
132 91
133 92 return posts_count
134 93
135 94 def get_url(self):
136 95 return reverse('tag', kwargs={'tag_name': self.name})
137 96
138 97 def get_view(self, *args, **kwargs):
139 98 return render_to_string('boards/tag.html', {
140 99 'tag': self,
141 100 })
@@ -1,163 +1,186 b''
1 1 import logging
2 2 from django.db.models import Count
3 3 from django.utils import timezone
4 4 from django.core.cache import cache
5 5 from django.db import models
6 6 from neboard import settings
7 7
8 8 __author__ = 'neko259'
9 9
10 10
11 11 logger = logging.getLogger(__name__)
12 12
13 13
14 14 CACHE_KEY_OPENING_POST = 'opening_post_id'
15 15
16 16
17 class ThreadManager(models.Manager):
18 def archive_oldest_threads(self):
19 """
20 Preserves maximum thread count. If there are too many threads,
21 archive the old ones.
22 """
23
24 threads = Thread.objects.filter(archived=False).order_by('-bump_time')
25 thread_count = threads.count()
26
27 if thread_count > settings.MAX_THREAD_COUNT:
28 num_threads_to_delete = thread_count - settings.MAX_THREAD_COUNT
29 old_threads = threads[thread_count - num_threads_to_delete:]
30
31 for thread in old_threads:
32 thread.archived = True
33 thread.last_edit_time = timezone.now()
34 thread.save(update_fields=['archived', 'last_edit_time'])
35
36 logger.info('Archived %d old threads' % num_threads_to_delete)
37
38
17 39 class Thread(models.Model):
40 objects = ThreadManager()
18 41
19 42 class Meta:
20 43 app_label = 'boards'
21 44
22 45 tags = models.ManyToManyField('Tag')
23 46 bump_time = models.DateTimeField()
24 47 last_edit_time = models.DateTimeField()
25 48 replies = models.ManyToManyField('Post', symmetrical=False, null=True,
26 49 blank=True, related_name='tre+')
27 50 archived = models.BooleanField(default=False)
28 51
29 52 def get_tags(self):
30 53 """
31 54 Gets a sorted tag list.
32 55 """
33 56
34 57 return self.tags.order_by('name')
35 58
36 59 def bump(self):
37 60 """
38 61 Bumps (moves to up) thread if possible.
39 62 """
40 63
41 64 if self.can_bump():
42 65 self.bump_time = timezone.now()
43 66
44 67 logger.info('Bumped thread %d' % self.id)
45 68
46 69 def get_reply_count(self):
47 70 return self.replies.count()
48 71
49 72 def get_images_count(self):
50 73 # TODO Use sum
51 74 total_count = 0
52 75 for post_with_image in self.replies.annotate(images_count=Count(
53 76 'images')):
54 77 total_count += post_with_image.images_count
55 78 return total_count
56 79
57 80 def can_bump(self):
58 81 """
59 82 Checks if the thread can be bumped by replying to it.
60 83 """
61 84
62 85 if self.archived:
63 86 return False
64 87
65 88 post_count = self.get_reply_count()
66 89
67 90 return post_count < settings.MAX_POSTS_PER_THREAD
68 91
69 92 def delete_with_posts(self):
70 93 """
71 94 Completely deletes thread and all its posts
72 95 """
73 96
74 97 if self.replies.exists():
75 98 self.replies.all().delete()
76 99
77 100 self.delete()
78 101
79 102 def get_last_replies(self):
80 103 """
81 104 Gets several last replies, not including opening post
82 105 """
83 106
84 107 if settings.LAST_REPLIES_COUNT > 0:
85 108 reply_count = self.get_reply_count()
86 109
87 110 if reply_count > 0:
88 111 reply_count_to_show = min(settings.LAST_REPLIES_COUNT,
89 112 reply_count - 1)
90 113 replies = self.get_replies()
91 114 last_replies = replies[reply_count - reply_count_to_show:]
92 115
93 116 return last_replies
94 117
95 118 def get_skipped_replies_count(self):
96 119 """
97 120 Gets number of posts between opening post and last replies.
98 121 """
99 122 reply_count = self.get_reply_count()
100 123 last_replies_count = min(settings.LAST_REPLIES_COUNT,
101 124 reply_count - 1)
102 125 return reply_count - last_replies_count - 1
103 126
104 127 def get_replies(self, view_fields_only=False):
105 128 """
106 129 Gets sorted thread posts
107 130 """
108 131
109 132 query = self.replies.order_by('pub_time').prefetch_related('images')
110 133 if view_fields_only:
111 134 query = query.defer('poster_user_agent', 'text_markup_type')
112 135 return query.all()
113 136
114 137 def get_replies_with_images(self, view_fields_only=False):
115 138 return self.get_replies(view_fields_only).annotate(images_count=Count(
116 139 'images')).filter(images_count__gt=0)
117 140
118 141 def add_tag(self, tag):
119 142 """
120 143 Connects thread to a tag and tag to a thread
121 144 """
122 145
123 146 self.tags.add(tag)
124 147 tag.threads.add(self)
125 148
126 149 def remove_tag(self, tag):
127 150 self.tags.remove(tag)
128 151 tag.threads.remove(self)
129 152
130 153 def get_opening_post(self, only_id=False):
131 154 """
132 155 Gets the first post of the thread
133 156 """
134 157
135 158 query = self.replies.order_by('pub_time')
136 159 if only_id:
137 160 query = query.only('id')
138 161 opening_post = query.first()
139 162
140 163 return opening_post
141 164
142 165 def get_opening_post_id(self):
143 166 """
144 167 Gets ID of the first thread post.
145 168 """
146 169
147 170 cache_key = CACHE_KEY_OPENING_POST + str(self.id)
148 171 opening_post_id = cache.get(cache_key)
149 172 if not opening_post_id:
150 173 opening_post_id = self.get_opening_post(only_id=True).id
151 174 cache.set(cache_key, opening_post_id)
152 175
153 176 return opening_post_id
154 177
155 178 def __unicode__(self):
156 179 return str(self.id)
157 180
158 181 def get_pub_time(self):
159 182 """
160 183 Gets opening post's pub time because thread does not have its own one.
161 184 """
162 185
163 186 return self.get_opening_post().pub_time
@@ -1,84 +1,82 b''
1 1 from django.conf.urls import patterns, url, include
2 2 from boards import views
3 3 from boards.rss import AllThreadsFeed, TagThreadsFeed, ThreadPostsFeed
4 4 from boards.views import api, tag_threads, all_threads, \
5 5 login, settings, all_tags
6 6 from boards.views.authors import AuthorsView
7 7 from boards.views.delete_post import DeletePostView
8 8 from boards.views.ban import BanUserView
9 9 from boards.views.static import StaticPageView
10 10 from boards.views.post_admin import PostAdminView
11 11
12 12 js_info_dict = {
13 13 'packages': ('boards',),
14 14 }
15 15
16 16 urlpatterns = patterns('',
17 17
18 18 # /boards/
19 19 url(r'^$', all_threads.AllThreadsView.as_view(), name='index'),
20 20 # /boards/page/
21 21 url(r'^page/(?P<page>\w+)/$', all_threads.AllThreadsView.as_view(),
22 22 name='index'),
23 23
24 24 # login page
25 25 url(r'^login/$', login.LoginView.as_view(), name='login'),
26 26
27 27 # /boards/tag/tag_name/
28 28 url(r'^tag/(?P<tag_name>\w+)/$', tag_threads.TagView.as_view(),
29 29 name='tag'),
30 30 # /boards/tag/tag_id/page/
31 31 url(r'^tag/(?P<tag_name>\w+)/page/(?P<page>\w+)/$',
32 32 tag_threads.TagView.as_view(), name='tag'),
33 33
34 34 # /boards/thread/
35 35 url(r'^thread/(?P<post_id>\w+)/$', views.thread.ThreadView.as_view(),
36 36 name='thread'),
37 37 url(r'^thread/(?P<post_id>\w+)/mode/(?P<mode>\w+)/$', views.thread.ThreadView
38 38 .as_view(), name='thread_mode'),
39 39
40 40 # /boards/post_admin/
41 41 url(r'^post_admin/(?P<post_id>\w+)/$', PostAdminView.as_view(),
42 42 name='post_admin'),
43 43
44 44 url(r'^settings/$', settings.SettingsView.as_view(), name='settings'),
45 45 url(r'^tags/$', all_tags.AllTagsView.as_view(), name='tags'),
46 46 url(r'^captcha/', include('captcha.urls')),
47 47 url(r'^authors/$', AuthorsView.as_view(), name='authors'),
48 48 url(r'^delete/(?P<post_id>\w+)/$', DeletePostView.as_view(),
49 49 name='delete'),
50 50 url(r'^ban/(?P<post_id>\w+)/$', BanUserView.as_view(), name='ban'),
51 51
52 52 url(r'^banned/$', views.banned.BannedView.as_view(), name='banned'),
53 53 url(r'^staticpage/(?P<name>\w+)/$', StaticPageView.as_view(),
54 54 name='staticpage'),
55 55
56 56 # RSS feeds
57 57 url(r'^rss/$', AllThreadsFeed()),
58 58 url(r'^page/(?P<page>\w+)/rss/$', AllThreadsFeed()),
59 59 url(r'^tag/(?P<tag_name>\w+)/rss/$', TagThreadsFeed()),
60 60 url(r'^tag/(?P<tag_name>\w+)/page/(?P<page>\w+)/rss/$', TagThreadsFeed()),
61 61 url(r'^thread/(?P<post_id>\w+)/rss/$', ThreadPostsFeed()),
62 62
63 63 # i18n
64 64 url(r'^jsi18n/$', 'boards.views.cached_js_catalog', js_info_dict,
65 65 name='js_info_dict'),
66 66
67 67 # API
68 68 url(r'^api/post/(?P<post_id>\w+)/$', api.get_post, name="get_post"),
69 69 url(r'^api/diff_thread/(?P<thread_id>\w+)/(?P<last_update_time>\w+)/$',
70 70 api.api_get_threaddiff, name="get_thread_diff"),
71 71 url(r'^api/threads/(?P<count>\w+)/$', api.api_get_threads,
72 72 name='get_threads'),
73 73 url(r'^api/tags/$', api.api_get_tags, name='get_tags'),
74 74 url(r'^api/thread/(?P<opening_post_id>\w+)/$', api.api_get_thread_posts,
75 75 name='get_thread'),
76 76 url(r'^api/add_post/(?P<opening_post_id>\w+)/$', api.api_add_post,
77 77 name='add_post'),
78 url(r'^api/get_tag_popularity/(?P<tag_name>\w+)$', api.get_tag_popularity,
79 name='get_tag_popularity'),
80 78
81 79 # Search
82 80 url(r'^search/', include('haystack.urls')),
83 81
84 82 )
@@ -1,246 +1,237 b''
1 1 from datetime import datetime
2 2 import json
3 3 import logging
4 4 from django.db import transaction
5 5 from django.http import HttpResponse
6 6 from django.shortcuts import get_object_or_404, render
7 7 from django.template import RequestContext
8 8 from django.utils import timezone
9 9 from django.core import serializers
10 10
11 11 from boards.forms import PostForm, PlainErrorList
12 12 from boards.models import Post, Thread, Tag
13 13 from boards.utils import datetime_to_epoch
14 14 from boards.views.thread import ThreadView
15 15
16 16 __author__ = 'neko259'
17 17
18 18 PARAMETER_TRUNCATED = 'truncated'
19 19 PARAMETER_TAG = 'tag'
20 20 PARAMETER_OFFSET = 'offset'
21 21 PARAMETER_DIFF_TYPE = 'type'
22 22
23 23 DIFF_TYPE_HTML = 'html'
24 24 DIFF_TYPE_JSON = 'json'
25 25
26 26 STATUS_OK = 'ok'
27 27 STATUS_ERROR = 'error'
28 28
29 29 logger = logging.getLogger(__name__)
30 30
31 31
32 32 @transaction.atomic
33 33 def api_get_threaddiff(request, thread_id, last_update_time):
34 34 """
35 35 Gets posts that were changed or added since time
36 36 """
37 37
38 38 thread = get_object_or_404(Post, id=thread_id).get_thread()
39 39
40 40 filter_time = datetime.fromtimestamp(float(last_update_time) / 1000000,
41 41 timezone.get_current_timezone())
42 42
43 43 json_data = {
44 44 'added': [],
45 45 'updated': [],
46 46 'last_update': None,
47 47 }
48 48 added_posts = Post.objects.filter(thread_new=thread,
49 49 pub_time__gt=filter_time) \
50 50 .order_by('pub_time')
51 51 updated_posts = Post.objects.filter(thread_new=thread,
52 52 pub_time__lte=filter_time,
53 53 last_edit_time__gt=filter_time)
54 54
55 55 diff_type = DIFF_TYPE_HTML
56 56 if PARAMETER_DIFF_TYPE in request.GET:
57 57 diff_type = request.GET[PARAMETER_DIFF_TYPE]
58 58
59 59 for post in added_posts:
60 60 json_data['added'].append(_get_post_data(post.id, diff_type, request))
61 61 for post in updated_posts:
62 62 json_data['updated'].append(_get_post_data(post.id, diff_type, request))
63 63 json_data['last_update'] = datetime_to_epoch(thread.last_edit_time)
64 64
65 65 return HttpResponse(content=json.dumps(json_data))
66 66
67 67
68 68 def api_add_post(request, opening_post_id):
69 69 """
70 70 Adds a post and return the JSON response for it
71 71 """
72 72
73 73 opening_post = get_object_or_404(Post, id=opening_post_id)
74 74
75 75 logger.info('Adding post via api...')
76 76
77 77 status = STATUS_OK
78 78 errors = []
79 79
80 80 if request.method == 'POST':
81 81 form = PostForm(request.POST, request.FILES, error_class=PlainErrorList)
82 82 form.session = request.session
83 83
84 84 if form.need_to_ban:
85 85 # Ban user because he is suspected to be a bot
86 86 # _ban_current_user(request)
87 87 status = STATUS_ERROR
88 88 if form.is_valid():
89 89 post = ThreadView().new_post(request, form, opening_post,
90 90 html_response=False)
91 91 if not post:
92 92 status = STATUS_ERROR
93 93 else:
94 94 logger.info('Added post #%d via api.' % post.id)
95 95 else:
96 96 status = STATUS_ERROR
97 97 errors = form.as_json_errors()
98 98
99 99 response = {
100 100 'status': status,
101 101 'errors': errors,
102 102 }
103 103
104 104 return HttpResponse(content=json.dumps(response))
105 105
106 106
107 107 def get_post(request, post_id):
108 108 """
109 109 Gets the html of a post. Used for popups. Post can be truncated if used
110 110 in threads list with 'truncated' get parameter.
111 111 """
112 112
113 113 logger.info('Getting post #%s' % post_id)
114 114
115 115 post = get_object_or_404(Post, id=post_id)
116 116
117 117 context = RequestContext(request)
118 118 context['post'] = post
119 119 if PARAMETER_TRUNCATED in request.GET:
120 120 context[PARAMETER_TRUNCATED] = True
121 121
122 122 return render(request, 'boards/api_post.html', context)
123 123
124 124
125 125 # TODO Test this
126 126 def api_get_threads(request, count):
127 127 """
128 128 Gets the JSON thread opening posts list.
129 129 Parameters that can be used for filtering:
130 130 tag, offset (from which thread to get results)
131 131 """
132 132
133 133 if PARAMETER_TAG in request.GET:
134 134 tag_name = request.GET[PARAMETER_TAG]
135 135 if tag_name is not None:
136 136 tag = get_object_or_404(Tag, name=tag_name)
137 137 threads = tag.threads.filter(archived=False)
138 138 else:
139 139 threads = Thread.objects.filter(archived=False)
140 140
141 141 if PARAMETER_OFFSET in request.GET:
142 142 offset = request.GET[PARAMETER_OFFSET]
143 143 offset = int(offset) if offset is not None else 0
144 144 else:
145 145 offset = 0
146 146
147 147 threads = threads.order_by('-bump_time')
148 148 threads = threads[offset:offset + int(count)]
149 149
150 150 opening_posts = []
151 151 for thread in threads:
152 152 opening_post = thread.get_opening_post()
153 153
154 154 # TODO Add tags, replies and images count
155 155 opening_posts.append(_get_post_data(opening_post.id,
156 156 include_last_update=True))
157 157
158 158 return HttpResponse(content=json.dumps(opening_posts))
159 159
160 160
161 161 # TODO Test this
162 162 def api_get_tags(request):
163 163 """
164 164 Gets all tags or user tags.
165 165 """
166 166
167 167 # TODO Get favorite tags for the given user ID
168 168
169 169 tags = Tag.objects.get_not_empty_tags()
170 170 tag_names = []
171 171 for tag in tags:
172 172 tag_names.append(tag.name)
173 173
174 174 return HttpResponse(content=json.dumps(tag_names))
175 175
176 176
177 177 # TODO The result can be cached by the thread last update time
178 178 # TODO Test this
179 179 def api_get_thread_posts(request, opening_post_id):
180 180 """
181 181 Gets the JSON array of thread posts
182 182 """
183 183
184 184 opening_post = get_object_or_404(Post, id=opening_post_id)
185 185 thread = opening_post.get_thread()
186 186 posts = thread.get_replies()
187 187
188 188 json_data = {
189 189 'posts': [],
190 190 'last_update': None,
191 191 }
192 192 json_post_list = []
193 193
194 194 for post in posts:
195 195 json_post_list.append(_get_post_data(post.id))
196 196 json_data['last_update'] = datetime_to_epoch(thread.last_edit_time)
197 197 json_data['posts'] = json_post_list
198 198
199 199 return HttpResponse(content=json.dumps(json_data))
200 200
201 201
202 202 def api_get_post(request, post_id):
203 203 """
204 204 Gets the JSON of a post. This can be
205 205 used as and API for external clients.
206 206 """
207 207
208 208 post = get_object_or_404(Post, id=post_id)
209 209
210 210 json = serializers.serialize("json", [post], fields=(
211 211 "pub_time", "_text_rendered", "title", "text", "image",
212 212 "image_width", "image_height", "replies", "tags"
213 213 ))
214 214
215 215 return HttpResponse(content=json)
216 216
217 217
218 def get_tag_popularity(request, tag_name):
219 tag = get_object_or_404(Tag, name=tag_name)
220
221 json_data = []
222 json_data['popularity'] = tag.get_popularity()
223
224 return HttpResponse(content=json.dumps(json_data))
225
226
227 218 # TODO Add pub time and replies
228 219 def _get_post_data(post_id, format_type=DIFF_TYPE_JSON, request=None,
229 220 include_last_update=False):
230 221 if format_type == DIFF_TYPE_HTML:
231 222 return get_post(request, post_id).content.strip()
232 223 elif format_type == DIFF_TYPE_JSON:
233 224 post = get_object_or_404(Post, id=post_id)
234 225 post_json = {
235 226 'id': post.id,
236 227 'title': post.title,
237 228 'text': post.text.rendered,
238 229 }
239 230 if post.images.exists():
240 231 post_image = post.get_first_image()
241 232 post_json['image'] = post_image.image.url
242 233 post_json['image_preview'] = post_image.image.url_200x150
243 234 if include_last_update:
244 235 post_json['bump_time'] = datetime_to_epoch(
245 236 post.thread_new.bump_time)
246 237 return post_json
General Comments 0
You need to be logged in to leave comments. Login now