##// END OF EJS Templates
Check post-images and post-attachments relations by related name
neko259 -
r1281:643e4c1d default
parent child Browse files
Show More
@@ -1,438 +1,438 b''
1 1 from datetime import datetime, timedelta, date
2 2 from datetime import time as dtime
3 3 import logging
4 4 import re
5 5 import uuid
6 6
7 7 from django.core.exceptions import ObjectDoesNotExist
8 8 from django.core.urlresolvers import reverse
9 9 from django.db import models, transaction
10 10 from django.db.models import TextField, QuerySet
11 11 from django.template.loader import render_to_string
12 12 from django.utils import timezone
13 13
14 14 from boards import settings
15 15 from boards.mdx_neboard import Parser
16 16 from boards.models import PostImage, Attachment
17 17 from boards.models.base import Viewable
18 18 from boards import utils
19 19 from boards.models.post.export import get_exporter, DIFF_TYPE_JSON
20 20 from boards.models.user import Notification, Ban
21 21 import boards.models.thread
22 22
23 23
24 24 APP_LABEL_BOARDS = 'boards'
25 25
26 26 POSTS_PER_DAY_RANGE = 7
27 27
28 28 BAN_REASON_AUTO = 'Auto'
29 29
30 30 IMAGE_THUMB_SIZE = (200, 150)
31 31
32 32 TITLE_MAX_LENGTH = 200
33 33
34 34 # TODO This should be removed
35 35 NO_IP = '0.0.0.0'
36 36
37 37 REGEX_REPLY = re.compile(r'\[post\](\d+)\[/post\]')
38 38 REGEX_NOTIFICATION = re.compile(r'\[user\](\w+)\[/user\]')
39 39
40 40 PARAMETER_TRUNCATED = 'truncated'
41 41 PARAMETER_TAG = 'tag'
42 42 PARAMETER_OFFSET = 'offset'
43 43 PARAMETER_DIFF_TYPE = 'type'
44 44 PARAMETER_CSS_CLASS = 'css_class'
45 45 PARAMETER_THREAD = 'thread'
46 46 PARAMETER_IS_OPENING = 'is_opening'
47 47 PARAMETER_MODERATOR = 'moderator'
48 48 PARAMETER_POST = 'post'
49 49 PARAMETER_OP_ID = 'opening_post_id'
50 50 PARAMETER_NEED_OPEN_LINK = 'need_open_link'
51 51 PARAMETER_REPLY_LINK = 'reply_link'
52 52 PARAMETER_NEED_OP_DATA = 'need_op_data'
53 53
54 54 POST_VIEW_PARAMS = (
55 55 'need_op_data',
56 56 'reply_link',
57 57 'moderator',
58 58 'need_open_link',
59 59 'truncated',
60 60 'mode_tree',
61 61 )
62 62
63 63 REFMAP_STR = '<a href="{}">&gt;&gt;{}</a>'
64 64
65 65 IMAGE_TYPES = (
66 66 'jpeg',
67 67 'jpg',
68 68 'png',
69 69 'bmp',
70 70 'gif',
71 71 )
72 72
73 73
74 74 class PostManager(models.Manager):
75 75 @transaction.atomic
76 76 def create_post(self, title: str, text: str, file=None, thread=None,
77 77 ip=NO_IP, tags: list=None, opening_posts: list=None):
78 78 """
79 79 Creates new post
80 80 """
81 81
82 82 is_banned = Ban.objects.filter(ip=ip).exists()
83 83
84 84 # TODO Raise specific exception and catch it in the views
85 85 if is_banned:
86 86 raise Exception("This user is banned")
87 87
88 88 if not tags:
89 89 tags = []
90 90 if not opening_posts:
91 91 opening_posts = []
92 92
93 93 posting_time = timezone.now()
94 94 new_thread = False
95 95 if not thread:
96 96 thread = boards.models.thread.Thread.objects.create(
97 97 bump_time=posting_time, last_edit_time=posting_time)
98 98 list(map(thread.tags.add, tags))
99 99 boards.models.thread.Thread.objects.process_oldest_threads()
100 100 new_thread = True
101 101
102 102 pre_text = Parser().preparse(text)
103 103
104 104 post = self.create(title=title,
105 105 text=pre_text,
106 106 pub_time=posting_time,
107 107 poster_ip=ip,
108 108 thread=thread,
109 109 last_edit_time=posting_time)
110 110 post.threads.add(thread)
111 111
112 112 logger = logging.getLogger('boards.post.create')
113 113
114 114 logger.info('Created post {} by {}'.format(post, post.poster_ip))
115 115
116 116 # TODO Move this to other place
117 117 if file:
118 118 file_type = file.name.split('.')[-1].lower()
119 119 if file_type in IMAGE_TYPES:
120 120 post.images.add(PostImage.objects.create_with_hash(file))
121 121 else:
122 122 post.attachments.add(Attachment.objects.create_with_hash(file))
123 123
124 124 post.build_url()
125 125 post.connect_replies()
126 126 post.connect_threads(opening_posts)
127 127 post.connect_notifications()
128 128
129 129 # Thread needs to be bumped only when the post is already created
130 130 if not new_thread:
131 131 thread.last_edit_time = posting_time
132 132 thread.bump()
133 133 thread.save()
134 134
135 135 return post
136 136
137 137 def delete_posts_by_ip(self, ip):
138 138 """
139 139 Deletes all posts of the author with same IP
140 140 """
141 141
142 142 posts = self.filter(poster_ip=ip)
143 143 for post in posts:
144 144 post.delete()
145 145
146 146 @utils.cached_result()
147 147 def get_posts_per_day(self) -> float:
148 148 """
149 149 Gets average count of posts per day for the last 7 days
150 150 """
151 151
152 152 day_end = date.today()
153 153 day_start = day_end - timedelta(POSTS_PER_DAY_RANGE)
154 154
155 155 day_time_start = timezone.make_aware(datetime.combine(
156 156 day_start, dtime()), timezone.get_current_timezone())
157 157 day_time_end = timezone.make_aware(datetime.combine(
158 158 day_end, dtime()), timezone.get_current_timezone())
159 159
160 160 posts_per_period = float(self.filter(
161 161 pub_time__lte=day_time_end,
162 162 pub_time__gte=day_time_start).count())
163 163
164 164 ppd = posts_per_period / POSTS_PER_DAY_RANGE
165 165
166 166 return ppd
167 167
168 168
169 169 class Post(models.Model, Viewable):
170 170 """A post is a message."""
171 171
172 172 objects = PostManager()
173 173
174 174 class Meta:
175 175 app_label = APP_LABEL_BOARDS
176 176 ordering = ('id',)
177 177
178 178 title = models.CharField(max_length=TITLE_MAX_LENGTH, null=True, blank=True)
179 179 pub_time = models.DateTimeField()
180 180 text = TextField(blank=True, null=True)
181 181 _text_rendered = TextField(blank=True, null=True, editable=False)
182 182
183 183 images = models.ManyToManyField(PostImage, null=True, blank=True,
184 184 related_name='post_images', db_index=True)
185 185 attachments = models.ManyToManyField(Attachment, null=True, blank=True,
186 186 related_name='attachment_posts')
187 187
188 188 poster_ip = models.GenericIPAddressField()
189 189
190 190 # TODO This field can be removed cause UID is used for update now
191 191 last_edit_time = models.DateTimeField()
192 192
193 193 referenced_posts = models.ManyToManyField('Post', symmetrical=False,
194 194 null=True,
195 195 blank=True, related_name='refposts',
196 196 db_index=True)
197 197 refmap = models.TextField(null=True, blank=True)
198 198 threads = models.ManyToManyField('Thread', db_index=True)
199 199 thread = models.ForeignKey('Thread', db_index=True, related_name='pt+')
200 200
201 201 url = models.TextField()
202 202 uid = models.TextField(db_index=True)
203 203
204 204 def __str__(self):
205 205 return 'P#{}/{}'.format(self.id, self.title)
206 206
207 207 def get_referenced_posts(self):
208 208 threads = self.get_threads().all()
209 209 return self.referenced_posts.filter(threads__in=threads)\
210 210 .order_by('pub_time').distinct().all()
211 211
212 212 def get_title(self) -> str:
213 213 """
214 214 Gets original post title or part of its text.
215 215 """
216 216
217 217 title = self.title
218 218 if not title:
219 219 title = self.get_text()
220 220
221 221 return title
222 222
223 223 def build_refmap(self) -> None:
224 224 """
225 225 Builds a replies map string from replies list. This is a cache to stop
226 226 the server from recalculating the map on every post show.
227 227 """
228 228
229 229 post_urls = [REFMAP_STR.format(refpost.get_absolute_url(), refpost.id)
230 230 for refpost in self.referenced_posts.all()]
231 231
232 232 self.refmap = ', '.join(post_urls)
233 233
234 234 def is_referenced(self) -> bool:
235 235 return self.refmap and len(self.refmap) > 0
236 236
237 237 def is_opening(self) -> bool:
238 238 """
239 239 Checks if this is an opening post or just a reply.
240 240 """
241 241
242 242 return self.get_thread().get_opening_post_id() == self.id
243 243
244 244 def get_absolute_url(self):
245 245 if self.url:
246 246 return self.url
247 247 else:
248 248 opening_id = self.get_thread().get_opening_post_id()
249 249 post_url = reverse('thread', kwargs={'post_id': opening_id})
250 250 if self.id != opening_id:
251 251 post_url += '#' + str(self.id)
252 252 return post_url
253 253
254 254
255 255 def get_thread(self):
256 256 return self.thread
257 257
258 258 def get_threads(self) -> QuerySet:
259 259 """
260 260 Gets post's thread.
261 261 """
262 262
263 263 return self.threads
264 264
265 265 def get_view(self, *args, **kwargs) -> str:
266 266 """
267 267 Renders post's HTML view. Some of the post params can be passed over
268 268 kwargs for the means of caching (if we view the thread, some params
269 269 are same for every post and don't need to be computed over and over.
270 270 """
271 271
272 272 thread = self.get_thread()
273 273 is_opening = kwargs.get(PARAMETER_IS_OPENING, self.is_opening())
274 274
275 275 if is_opening:
276 276 opening_post_id = self.id
277 277 else:
278 278 opening_post_id = thread.get_opening_post_id()
279 279
280 280 css_class = 'post'
281 281 if thread.archived:
282 282 css_class += ' archive_post'
283 283 elif not thread.can_bump():
284 284 css_class += ' dead_post'
285 285
286 286 params = dict()
287 287 for param in POST_VIEW_PARAMS:
288 288 if param in kwargs:
289 289 params[param] = kwargs[param]
290 290
291 291 params.update({
292 292 PARAMETER_POST: self,
293 293 PARAMETER_IS_OPENING: is_opening,
294 294 PARAMETER_THREAD: thread,
295 295 PARAMETER_CSS_CLASS: css_class,
296 296 PARAMETER_OP_ID: opening_post_id,
297 297 })
298 298
299 299 return render_to_string('boards/post.html', params)
300 300
301 301 def get_search_view(self, *args, **kwargs):
302 302 return self.get_view(need_op_data=True, *args, **kwargs)
303 303
304 304 def get_first_image(self) -> PostImage:
305 305 return self.images.earliest('id')
306 306
307 307 def delete(self, using=None):
308 308 """
309 309 Deletes all post images and the post itself.
310 310 """
311 311
312 312 for image in self.images.all():
313 image_refs_count = Post.objects.filter(images__in=[image]).count()
313 image_refs_count = image.post_images.count()
314 314 if image_refs_count == 1:
315 315 image.delete()
316 316
317 317 for attachment in self.attachments.all():
318 attachment_refs_count = Post.objects.filter(attachments__in=[attachment]).count()
318 attachment_refs_count = attachment.attachment_posts.count()
319 319 if attachment_refs_count == 1:
320 320 attachment.delete()
321 321
322 322 thread = self.get_thread()
323 323 thread.last_edit_time = timezone.now()
324 324 thread.save()
325 325
326 326 super(Post, self).delete(using)
327 327
328 328 logging.getLogger('boards.post.delete').info(
329 329 'Deleted post {}'.format(self))
330 330
331 331 def get_post_data(self, format_type=DIFF_TYPE_JSON, request=None,
332 332 include_last_update=False) -> str:
333 333 """
334 334 Gets post HTML or JSON data that can be rendered on a page or used by
335 335 API.
336 336 """
337 337
338 338 return get_exporter(format_type).export(self, request,
339 339 include_last_update)
340 340
341 341 def notify_clients(self, recursive=True):
342 342 """
343 343 Sends post HTML data to the thread web socket.
344 344 """
345 345
346 346 if not settings.get_bool('External', 'WebsocketsEnabled'):
347 347 return
348 348
349 349 thread_ids = list()
350 350 for thread in self.get_threads().all():
351 351 thread_ids.append(thread.id)
352 352
353 353 thread.notify_clients()
354 354
355 355 if recursive:
356 356 for reply_number in re.finditer(REGEX_REPLY, self.get_raw_text()):
357 357 post_id = reply_number.group(1)
358 358
359 359 try:
360 360 ref_post = Post.objects.get(id=post_id)
361 361
362 362 if ref_post.get_threads().exclude(id__in=thread_ids).exists():
363 363 # If post is in this thread, its thread was already notified.
364 364 # Otherwise, notify its thread separately.
365 365 ref_post.notify_clients(recursive=False)
366 366 except ObjectDoesNotExist:
367 367 pass
368 368
369 369 def build_url(self):
370 370 self.url = self.get_absolute_url()
371 371 self.save(update_fields=['url'])
372 372
373 373 def save(self, force_insert=False, force_update=False, using=None,
374 374 update_fields=None):
375 375 self._text_rendered = Parser().parse(self.get_raw_text())
376 376
377 377 self.uid = str(uuid.uuid4())
378 378 if update_fields is not None and 'uid' not in update_fields:
379 379 update_fields += ['uid']
380 380
381 381 if self.id:
382 382 for thread in self.get_threads().all():
383 383 thread.last_edit_time = self.last_edit_time
384 384
385 385 thread.save(update_fields=['last_edit_time', 'bumpable'])
386 386
387 387 super().save(force_insert, force_update, using, update_fields)
388 388
389 389 def get_text(self) -> str:
390 390 return self._text_rendered
391 391
392 392 def get_raw_text(self) -> str:
393 393 return self.text
394 394
395 395 def get_absolute_id(self) -> str:
396 396 """
397 397 If the post has many threads, shows its main thread OP id in the post
398 398 ID.
399 399 """
400 400
401 401 if self.get_threads().count() > 1:
402 402 return '{}/{}'.format(self.get_thread().get_opening_post_id(), self.id)
403 403 else:
404 404 return str(self.id)
405 405
406 406 def connect_notifications(self):
407 407 for reply_number in re.finditer(REGEX_NOTIFICATION, self.get_raw_text()):
408 408 user_name = reply_number.group(1).lower()
409 409 Notification.objects.get_or_create(name=user_name, post=self)
410 410
411 411 def connect_replies(self):
412 412 """
413 413 Connects replies to a post to show them as a reflink map
414 414 """
415 415
416 416 for reply_number in re.finditer(REGEX_REPLY, self.get_raw_text()):
417 417 post_id = reply_number.group(1)
418 418
419 419 try:
420 420 referenced_post = Post.objects.get(id=post_id)
421 421
422 422 referenced_post.referenced_posts.add(self)
423 423 referenced_post.last_edit_time = self.pub_time
424 424 referenced_post.build_refmap()
425 425 referenced_post.save(update_fields=['refmap', 'last_edit_time'])
426 426 except ObjectDoesNotExist:
427 427 pass
428 428
429 429 def connect_threads(self, opening_posts):
430 430 for opening_post in opening_posts:
431 431 threads = opening_post.get_threads().all()
432 432 for thread in threads:
433 433 if thread.can_bump():
434 434 thread.update_bump_status()
435 435
436 436 thread.last_edit_time = self.last_edit_time
437 437 thread.save(update_fields=['last_edit_time', 'bumpable'])
438 438 self.threads.add(opening_post.get_thread())
General Comments 0
You need to be logged in to leave comments. Login now