##// END OF EJS Templates
Merged with default branch
neko259 -
r1543:9a0b5221 merge decentral
parent child Browse files
Show More
@@ -1,124 +1,127 b''
1 1 from django.contrib import admin
2 2 from django.utils.translation import ugettext_lazy as _
3 3 from django.core.urlresolvers import reverse
4 4 from boards.models import Post, Tag, Ban, Thread, Banner, PostImage, KeyPair, GlobalId
5 5
6 6
7 7 @admin.register(Post)
8 8 class PostAdmin(admin.ModelAdmin):
9 9
10 10 list_display = ('id', 'title', 'text', 'poster_ip', 'linked_images')
11 11 list_filter = ('pub_time',)
12 12 search_fields = ('id', 'title', 'text', 'poster_ip')
13 exclude = ('referenced_posts', 'refmap')
13 exclude = ('referenced_posts', 'refmap', 'images')
14 14 readonly_fields = ('poster_ip', 'threads', 'thread', 'linked_images',
15 15 'attachments', 'uid', 'url', 'pub_time', 'opening')
16 16
17 17 def ban_poster(self, request, queryset):
18 18 bans = 0
19 19 for post in queryset:
20 20 poster_ip = post.poster_ip
21 21 ban, created = Ban.objects.get_or_create(ip=poster_ip)
22 22 if created:
23 23 bans += 1
24 24 self.message_user(request, _('{} posters were banned').format(bans))
25 25
26 26 def ban_with_hiding(self, request, queryset):
27 27 bans = 0
28 28 hidden = 0
29 29 for post in queryset:
30 30 poster_ip = post.poster_ip
31 31 ban, created = Ban.objects.get_or_create(ip=poster_ip)
32 32 if created:
33 33 bans += 1
34 34 posts = Post.objects.filter(poster_ip=poster_ip, id__gte=post.id)
35 35 hidden += posts.count()
36 36 posts.update(hidden=True)
37 37 self.message_user(request, _('{} posters were banned, {} messages were hidden').format(bans, hidden))
38 38
39 39 def linked_images(self, obj: Post):
40 40 images = obj.images.all()
41 image_urls = ['<a href="{}">{}</a>'.format(reverse('admin:%s_%s_change' %(image._meta.app_label, image._meta.model_name), args=[image.id]), image.hash) for image in images]
41 image_urls = ['<a href="{}"><img src="{}" /></a>'.format(
42 reverse('admin:%s_%s_change' % (image._meta.app_label,
43 image._meta.model_name),
44 args=[image.id]), image.image.url_200x150) for image in images]
42 45 return ', '.join(image_urls)
43 46 linked_images.allow_tags = True
44 47
45 48
46 49 actions = ['ban_poster', 'ban_with_hiding']
47 50
48 51
49 52 @admin.register(Tag)
50 53 class TagAdmin(admin.ModelAdmin):
51 54
52 55 def thread_count(self, obj: Tag) -> int:
53 56 return obj.get_thread_count()
54 57
55 58 def display_children(self, obj: Tag):
56 59 return ', '.join([str(child) for child in obj.get_children().all()])
57 60
58 61 def save_model(self, request, obj, form, change):
59 62 super().save_model(request, obj, form, change)
60 63 for thread in obj.get_threads().all():
61 64 thread.refresh_tags()
62 65 list_display = ('name', 'thread_count', 'display_children')
63 66 search_fields = ('name',)
64 67
65 68
66 69 @admin.register(Thread)
67 70 class ThreadAdmin(admin.ModelAdmin):
68 71
69 72 def title(self, obj: Thread) -> str:
70 73 return obj.get_opening_post().get_title()
71 74
72 75 def reply_count(self, obj: Thread) -> int:
73 76 return obj.get_reply_count()
74 77
75 78 def ip(self, obj: Thread):
76 79 return obj.get_opening_post().poster_ip
77 80
78 81 def display_tags(self, obj: Thread):
79 82 return ', '.join([str(tag) for tag in obj.get_tags().all()])
80 83
81 84 def op(self, obj: Thread):
82 85 return obj.get_opening_post_id()
83 86
84 87 # Save parent tags when editing tags
85 88 def save_related(self, request, form, formsets, change):
86 89 super().save_related(request, form, formsets, change)
87 90 form.instance.refresh_tags()
88 91 list_display = ('id', 'op', 'title', 'reply_count', 'status', 'ip',
89 92 'display_tags')
90 93 list_filter = ('bump_time', 'status')
91 94 search_fields = ('id', 'title')
92 95 filter_horizontal = ('tags',)
93 96
94 97
95 98 @admin.register(KeyPair)
96 99 class KeyPairAdmin(admin.ModelAdmin):
97 100 list_display = ('public_key', 'primary')
98 101 list_filter = ('primary',)
99 102 search_fields = ('public_key',)
100 103
101 104
102 105 @admin.register(Ban)
103 106 class BanAdmin(admin.ModelAdmin):
104 107 list_display = ('ip', 'can_read')
105 108 list_filter = ('can_read',)
106 109 search_fields = ('ip',)
107 110
108 111
109 112 @admin.register(Banner)
110 113 class BannerAdmin(admin.ModelAdmin):
111 114 list_display = ('title', 'text')
112 115
113 116
114 117 @admin.register(PostImage)
115 118 class PostImageAdmin(admin.ModelAdmin):
116 119 search_fields = ('alias',)
117 120
118 121
119 122 @admin.register(GlobalId)
120 123 class GlobalIdAdmin(admin.ModelAdmin):
121 124 def is_linked(self, obj):
122 125 return Post.objects.filter(global_id=obj).exists()
123 126
124 127 list_display = ('__str__', 'is_linked',) No newline at end of file
@@ -1,381 +1,410 b''
1 1 import uuid
2 2
3 3 import re
4 4 from boards import settings
5 5 from boards.abstracts.tripcode import Tripcode
6 6 from boards.models import PostImage, Attachment, KeyPair, GlobalId
7 7 from boards.models.base import Viewable
8 8 from boards.models.post.export import get_exporter, DIFF_TYPE_JSON
9 9 from boards.models.post.manager import PostManager
10 10 from boards.utils import datetime_to_epoch
11 11 from django.core.exceptions import ObjectDoesNotExist
12 12 from django.core.urlresolvers import reverse
13 13 from django.db import models
14 14 from django.db.models import TextField, QuerySet
15 15 from django.template.defaultfilters import truncatewords, striptags
16 16 from django.template.loader import render_to_string
17 17
18 18 CSS_CLS_HIDDEN_POST = 'hidden_post'
19 19 CSS_CLS_DEAD_POST = 'dead_post'
20 20 CSS_CLS_ARCHIVE_POST = 'archive_post'
21 21 CSS_CLS_POST = 'post'
22 22 CSS_CLS_MONOCHROME = 'monochrome'
23 23
24 24 TITLE_MAX_WORDS = 10
25 25
26 26 APP_LABEL_BOARDS = 'boards'
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 REGEX_REPLY = re.compile(r'\[post\](\d+)\[/post\]')
35 35 REGEX_GLOBAL_REPLY = re.compile(r'\[post\](\w+)::([^:]+)::(\d+)\[/post\]')
36 36 REGEX_URL = re.compile(r'https?\://[a-zA-Z0-9\-\.]+\.[a-zA-Z]{2,3}(/\S*)?')
37 37 REGEX_NOTIFICATION = re.compile(r'\[user\](\w+)\[/user\]')
38 38
39 39 PARAMETER_TRUNCATED = 'truncated'
40 40 PARAMETER_TAG = 'tag'
41 41 PARAMETER_OFFSET = 'offset'
42 42 PARAMETER_DIFF_TYPE = 'type'
43 43 PARAMETER_CSS_CLASS = 'css_class'
44 44 PARAMETER_THREAD = 'thread'
45 45 PARAMETER_IS_OPENING = 'is_opening'
46 46 PARAMETER_POST = 'post'
47 47 PARAMETER_OP_ID = 'opening_post_id'
48 48 PARAMETER_NEED_OPEN_LINK = 'need_open_link'
49 49 PARAMETER_REPLY_LINK = 'reply_link'
50 50 PARAMETER_NEED_OP_DATA = 'need_op_data'
51 51
52 52 POST_VIEW_PARAMS = (
53 53 'need_op_data',
54 54 'reply_link',
55 55 'need_open_link',
56 56 'truncated',
57 57 'mode_tree',
58 58 'perms',
59 59 'tree_depth',
60 60 )
61 61
62 62
63 63 class Post(models.Model, Viewable):
64 64 """A post is a message."""
65 65
66 66 objects = PostManager()
67 67
68 68 class Meta:
69 69 app_label = APP_LABEL_BOARDS
70 70 ordering = ('id',)
71 71
72 72 title = models.CharField(max_length=TITLE_MAX_LENGTH, null=True, blank=True)
73 73 pub_time = models.DateTimeField()
74 74 text = TextField(blank=True, null=True)
75 75 _text_rendered = TextField(blank=True, null=True, editable=False)
76 76
77 77 images = models.ManyToManyField(PostImage, null=True, blank=True,
78 78 related_name='post_images', db_index=True)
79 79 attachments = models.ManyToManyField(Attachment, null=True, blank=True,
80 80 related_name='attachment_posts')
81 81
82 82 poster_ip = models.GenericIPAddressField()
83 83
84 84 # TODO This field can be removed cause UID is used for update now
85 85 last_edit_time = models.DateTimeField()
86 86
87 87 referenced_posts = models.ManyToManyField('Post', symmetrical=False,
88 88 null=True,
89 89 blank=True, related_name='refposts',
90 90 db_index=True)
91 91 refmap = models.TextField(null=True, blank=True)
92 92 threads = models.ManyToManyField('Thread', db_index=True,
93 93 related_name='multi_replies')
94 94 thread = models.ForeignKey('Thread', db_index=True, related_name='pt+')
95 95
96 96 url = models.TextField()
97 97 uid = models.TextField(db_index=True)
98 98
99 99 # Global ID with author key. If the message was downloaded from another
100 100 # server, this indicates the server.
101 101 global_id = models.OneToOneField(GlobalId, null=True, blank=True,
102 102 on_delete=models.CASCADE)
103 103
104 104 tripcode = models.CharField(max_length=50, blank=True, default='')
105 105 opening = models.BooleanField(db_index=True)
106 106 hidden = models.BooleanField(default=False)
107 107
108 108 def __str__(self):
109 109 return 'P#{}/{}'.format(self.id, self.get_title())
110 110
111 111 def get_title(self) -> str:
112 112 return self.title
113 113
114 114 def get_title_or_text(self):
115 115 title = self.get_title()
116 116 if not title:
117 117 title = truncatewords(striptags(self.get_text()), TITLE_MAX_WORDS)
118 118
119 119 return title
120 120
121 121 def build_refmap(self) -> None:
122 122 """
123 123 Builds a replies map string from replies list. This is a cache to stop
124 124 the server from recalculating the map on every post show.
125 125 """
126 126
127 127 post_urls = [refpost.get_link_view()
128 128 for refpost in self.referenced_posts.all()]
129 129
130 130 self.refmap = ', '.join(post_urls)
131 131
132 132 def is_referenced(self) -> bool:
133 133 return self.refmap and len(self.refmap) > 0
134 134
135 135 def is_opening(self) -> bool:
136 136 """
137 137 Checks if this is an opening post or just a reply.
138 138 """
139 139
140 140 return self.opening
141 141
142 142 def get_absolute_url(self, thread=None):
143 143 url = None
144 144
145 145 if thread is None:
146 146 thread = self.get_thread()
147 147
148 148 # Url is cached only for the "main" thread. When getting url
149 149 # for other threads, do it manually.
150 150 if self.url:
151 151 url = self.url
152 152
153 153 if url is None:
154 154 opening = self.is_opening()
155 155 opening_id = self.id if opening else thread.get_opening_post_id()
156 156 url = reverse('thread', kwargs={'post_id': opening_id})
157 157 if not opening:
158 158 url += '#' + str(self.id)
159 159
160 160 return url
161 161
162 162 def get_thread(self):
163 163 return self.thread
164 164
165 165 def get_thread_id(self):
166 166 return self.thread_id
167 167
168 168 def get_threads(self) -> QuerySet:
169 169 """
170 170 Gets post's thread.
171 171 """
172 172
173 173 return self.threads
174 174
175 175 def _get_cache_key(self):
176 176 return [datetime_to_epoch(self.last_edit_time)]
177 177
178 178 def get_view(self, *args, **kwargs) -> str:
179 179 """
180 180 Renders post's HTML view. Some of the post params can be passed over
181 181 kwargs for the means of caching (if we view the thread, some params
182 182 are same for every post and don't need to be computed over and over.
183 183 """
184 184
185 185 thread = self.get_thread()
186 186
187 187 css_classes = [CSS_CLS_POST]
188 188 if thread.is_archived():
189 189 css_classes.append(CSS_CLS_ARCHIVE_POST)
190 190 elif not thread.can_bump():
191 191 css_classes.append(CSS_CLS_DEAD_POST)
192 192 if self.is_hidden():
193 193 css_classes.append(CSS_CLS_HIDDEN_POST)
194 194 if thread.is_monochrome():
195 195 css_classes.append(CSS_CLS_MONOCHROME)
196 196
197 197 params = dict()
198 198 for param in POST_VIEW_PARAMS:
199 199 if param in kwargs:
200 200 params[param] = kwargs[param]
201 201
202 202 params.update({
203 203 PARAMETER_POST: self,
204 204 PARAMETER_IS_OPENING: self.is_opening(),
205 205 PARAMETER_THREAD: thread,
206 206 PARAMETER_CSS_CLASS: ' '.join(css_classes),
207 207 })
208 208
209 209 return render_to_string('boards/post.html', params)
210 210
211 211 def get_search_view(self, *args, **kwargs):
212 212 return self.get_view(need_op_data=True, *args, **kwargs)
213 213
214 214 def get_first_image(self) -> PostImage:
215 215 return self.images.earliest('id')
216 216
217 217 def set_global_id(self, key_pair=None):
218 218 """
219 219 Sets global id based on the given key pair. If no key pair is given,
220 220 default one is used.
221 221 """
222 222
223 223 if key_pair:
224 224 key = key_pair
225 225 else:
226 226 try:
227 227 key = KeyPair.objects.get(primary=True)
228 228 except KeyPair.DoesNotExist:
229 229 # Do not update the global id because there is no key defined
230 230 return
231 231 global_id = GlobalId(key_type=key.key_type,
232 232 key=key.public_key,
233 233 local_id=self.id)
234 234 global_id.save()
235 235
236 236 self.global_id = global_id
237 237
238 238 self.save(update_fields=['global_id'])
239 239
240 240 def get_pub_time_str(self):
241 241 return str(self.pub_time)
242 242
243 243 def get_replied_ids(self):
244 244 """
245 245 Gets ID list of the posts that this post replies.
246 246 """
247 247
248 248 raw_text = self.get_raw_text()
249 249
250 250 local_replied = REGEX_REPLY.findall(raw_text)
251 251 global_replied = []
252 252 for match in REGEX_GLOBAL_REPLY.findall(raw_text):
253 253 key_type = match[0]
254 254 key = match[1]
255 255 local_id = match[2]
256 256
257 257 try:
258 258 global_id = GlobalId.objects.get(key_type=key_type,
259 259 key=key, local_id=local_id)
260 260 for post in Post.objects.filter(global_id=global_id).only('id'):
261 261 global_replied.append(post.id)
262 262 except GlobalId.DoesNotExist:
263 263 pass
264 264 return local_replied + global_replied
265 265
266 266 def get_post_data(self, format_type=DIFF_TYPE_JSON, request=None,
267 267 include_last_update=False) -> str:
268 268 """
269 269 Gets post HTML or JSON data that can be rendered on a page or used by
270 270 API.
271 271 """
272 272
273 273 return get_exporter(format_type).export(self, request,
274 274 include_last_update)
275 275
276 276 def notify_clients(self, recursive=True):
277 277 """
278 278 Sends post HTML data to the thread web socket.
279 279 """
280 280
281 281 if not settings.get_bool('External', 'WebsocketsEnabled'):
282 282 return
283 283
284 284 thread_ids = list()
285 285 for thread in self.get_threads().all():
286 286 thread_ids.append(thread.id)
287 287
288 288 thread.notify_clients()
289 289
290 290 if recursive:
291 291 for reply_number in re.finditer(REGEX_REPLY, self.get_raw_text()):
292 292 post_id = reply_number.group(1)
293 293
294 294 try:
295 295 ref_post = Post.objects.get(id=post_id)
296 296
297 297 if ref_post.get_threads().exclude(id__in=thread_ids).exists():
298 298 # If post is in this thread, its thread was already notified.
299 299 # Otherwise, notify its thread separately.
300 300 ref_post.notify_clients(recursive=False)
301 301 except ObjectDoesNotExist:
302 302 pass
303 303
304 304 def build_url(self):
305 305 self.url = self.get_absolute_url()
306 306 self.save(update_fields=['url'])
307 307
308 308 def save(self, force_insert=False, force_update=False, using=None,
309 309 update_fields=None):
310 310 new_post = self.id is None
311 311
312 312 self.uid = str(uuid.uuid4())
313 313 if update_fields is not None and 'uid' not in update_fields:
314 314 update_fields += ['uid']
315 315
316 316 if not new_post:
317 317 for thread in self.get_threads().all():
318 318 thread.last_edit_time = self.last_edit_time
319 319
320 320 thread.save(update_fields=['last_edit_time', 'status'])
321 321
322 322 super().save(force_insert, force_update, using, update_fields)
323 323
324 324 if self.url is None:
325 325 self.build_url()
326 326
327 327 def get_text(self) -> str:
328 328 return self._text_rendered
329 329
330 330 def get_raw_text(self) -> str:
331 331 return self.text
332 332
333 333 def get_sync_text(self) -> str:
334 334 """
335 335 Returns text applicable for sync. It has absolute post reflinks.
336 336 """
337 337
338 338 replacements = dict()
339 339 for post_id in REGEX_REPLY.findall(self.get_raw_text()):
340 340 absolute_post_id = str(Post.objects.get(id=post_id).global_id)
341 341 replacements[post_id] = absolute_post_id
342 342
343 343 text = self.get_raw_text() or ''
344 344 for key in replacements:
345 345 text = text.replace('[post]{}[/post]'.format(key),
346 346 '[post]{}[/post]'.format(replacements[key]))
347 347 text = text.replace('\r\n', '\n').replace('\r', '\n')
348 348
349 349 return text
350 350
351 351 def connect_threads(self, opening_posts):
352 352 for opening_post in opening_posts:
353 353 threads = opening_post.get_threads().all()
354 354 for thread in threads:
355 355 if thread.can_bump():
356 356 thread.update_bump_status()
357 357
358 358 thread.last_edit_time = self.last_edit_time
359 359 thread.save(update_fields=['last_edit_time', 'status'])
360 360 self.threads.add(opening_post.get_thread())
361 361
362 362 def get_tripcode(self):
363 363 if self.tripcode:
364 364 return Tripcode(self.tripcode)
365 365
366 366 def get_link_view(self):
367 367 """
368 368 Gets view of a reflink to the post.
369 369 """
370 370 result = '<a href="{}">&gt;&gt;{}</a>'.format(self.get_absolute_url(),
371 371 self.id)
372 372 if self.is_opening():
373 373 result = '<b>{}</b>'.format(result)
374 374
375 375 return result
376 376
377 377 def is_hidden(self) -> bool:
378 378 return self.hidden
379 379
380 380 def set_hidden(self, hidden):
381 381 self.hidden = hidden
382
383
384 # SIGNALS (Maybe move to other module?)
385 @receiver(post_save, sender=Post)
386 def connect_replies(instance, **kwargs):
387 for reply_number in re.finditer(REGEX_REPLY, instance.get_raw_text()):
388 post_id = reply_number.group(1)
389
390 try:
391 referenced_post = Post.objects.get(id=post_id)
392
393 referenced_post.referenced_posts.add(instance)
394 referenced_post.last_edit_time = instance.pub_time
395 referenced_post.build_refmap()
396 referenced_post.save(update_fields=['refmap', 'last_edit_time'])
397 except ObjectDoesNotExist:
398 pass
399
400
401 @receiver(post_save, sender=Post)
402 def connect_notifications(instance, **kwargs):
403 for reply_number in re.finditer(REGEX_NOTIFICATION, instance.get_raw_text()):
404 user_name = reply_number.group(1).lower()
405 Notification.objects.get_or_create(name=user_name, post=instance)
406
407
408 @receiver(pre_save, sender=Post)
409 def preparse_text(instance, **kwargs):
410 instance._text_rendered = get_parser().parse(instance.get_raw_text())
@@ -1,87 +1,88 b''
1 1 import re
2 2 from boards.mdx_neboard import get_parser
3 3
4 4 from boards.models import Post, GlobalId
5 5 from boards.models.post import REGEX_NOTIFICATION
6 6 from boards.models.post import REGEX_REPLY, REGEX_GLOBAL_REPLY
7 7 from boards.models.user import Notification
8 8 from django.db.models.signals import post_save, pre_save, pre_delete, \
9 9 post_delete
10 10 from django.dispatch import receiver
11 11 from django.utils import timezone
12 12
13 13
14 14 @receiver(post_save, sender=Post)
15 15 def connect_replies(instance, **kwargs):
16 16 for reply_number in re.finditer(REGEX_REPLY, instance.get_raw_text()):
17 17 post_id = reply_number.group(1)
18 18
19 19 try:
20 20 referenced_post = Post.objects.get(id=post_id)
21 21
22 if not referenced_post.referenced_posts.filter(id=instance.id).exists():
22 23 referenced_post.referenced_posts.add(instance)
23 24 referenced_post.last_edit_time = instance.pub_time
24 25 referenced_post.build_refmap()
25 26 referenced_post.save(update_fields=['refmap', 'last_edit_time'])
26 27 except Post.ObjectDoesNotExist:
27 28 pass
28 29
29 30
30 31 @receiver(post_save, sender=Post)
31 32 def connect_global_replies(instance, **kwargs):
32 33 for reply_number in re.finditer(REGEX_GLOBAL_REPLY, instance.get_raw_text()):
33 34 key_type = reply_number.group(1)
34 35 key = reply_number.group(2)
35 36 local_id = reply_number.group(3)
36 37
37 38 try:
38 39 global_id = GlobalId.objects.get(key_type=key_type, key=key,
39 40 local_id=local_id)
40 41 referenced_post = Post.objects.get(global_id=global_id)
41 42 referenced_post.referenced_posts.add(instance)
42 43 referenced_post.last_edit_time = instance.pub_time
43 44 referenced_post.build_refmap()
44 45 referenced_post.save(update_fields=['refmap', 'last_edit_time'])
45 46 except (GlobalId.DoesNotExist, Post.DoesNotExist):
46 47 pass
47 48
48 49
49 50 @receiver(post_save, sender=Post)
50 51 def connect_notifications(instance, **kwargs):
51 52 for reply_number in re.finditer(REGEX_NOTIFICATION, instance.get_raw_text()):
52 53 user_name = reply_number.group(1).lower()
53 54 Notification.objects.get_or_create(name=user_name, post=instance)
54 55
55 56
56 57 @receiver(pre_save, sender=Post)
57 58 def preparse_text(instance, **kwargs):
58 59 instance._text_rendered = get_parser().parse(instance.get_raw_text())
59 60
60 61
61 62 @receiver(pre_delete, sender=Post)
62 63 def delete_images(instance, **kwargs):
63 64 for image in instance.images.all():
64 65 image_refs_count = image.post_images.count()
65 66 if image_refs_count == 1:
66 67 image.delete()
67 68
68 69
69 70 @receiver(pre_delete, sender=Post)
70 71 def delete_attachments(instance, **kwargs):
71 72 for attachment in instance.attachments.all():
72 73 attachment_refs_count = attachment.attachment_posts.count()
73 74 if attachment_refs_count == 1:
74 75 attachment.delete()
75 76
76 77
77 78 @receiver(post_delete, sender=Post)
78 79 def update_thread_on_delete(instance, **kwargs):
79 80 thread = instance.get_thread()
80 81 thread.last_edit_time = timezone.now()
81 82 thread.save()
82 83
83 84
84 85 @receiver(post_delete, sender=Post)
85 86 def delete_global_id(instance, **kwargs):
86 87 if instance.global_id and instance.global_id.id:
87 88 instance.global_id.delete()
General Comments 0
You need to be logged in to leave comments. Login now