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