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