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