Show More
@@ -1,21 +1,28 b'' | |||||
1 | # -*- coding: utf-8 -*- |
|
1 | # -*- coding: utf-8 -*- | |
2 | from __future__ import unicode_literals |
|
2 | from __future__ import unicode_literals | |
3 |
|
3 | |||
4 | from django.db import migrations |
|
4 | from django.db import migrations | |
5 | from boards.models import Post |
|
|||
6 |
|
5 | |||
7 |
|
6 | |||
8 | class Migration(migrations.Migration): |
|
7 | class Migration(migrations.Migration): | |
9 |
|
8 | |||
10 |
def re |
|
9 | def rebuild_refmap(apps, schema_editor): | |
|
10 | Post = apps.get_model('boards', 'Post') | |||
11 | for post in Post.objects.all(): |
|
11 | for post in Post.objects.all(): | |
12 |
|
|
12 | refposts = list() | |
|
13 | for refpost in post.referenced_posts.all(): | |||
|
14 | result = '<a href="{}">>>{}</a>'.format(refpost.get_absolute_url(), | |||
|
15 | self.id) | |||
|
16 | if refpost.is_opening(): | |||
|
17 | result = '<b>{}</b>'.format(result) | |||
|
18 | refposts += result | |||
|
19 | post.refmap = ', '.join(refposts) | |||
13 | post.save(update_fields=['refmap']) |
|
20 | post.save(update_fields=['refmap']) | |
14 |
|
21 | |||
15 | dependencies = [ |
|
22 | dependencies = [ | |
16 | ('boards', '0024_post_tripcode'), |
|
23 | ('boards', '0024_post_tripcode'), | |
17 | ] |
|
24 | ] | |
18 |
|
25 | |||
19 | operations = [ |
|
26 | operations = [ | |
20 |
migrations.RunPython(re |
|
27 | migrations.RunPython(rebuild_refmap), | |
21 | ] |
|
28 | ] |
@@ -1,257 +1,258 b'' | |||||
1 | import logging |
|
1 | import logging | |
2 | from adjacent import Client |
|
2 | from adjacent import Client | |
3 |
|
3 | |||
4 | from django.db.models import Count, Sum, QuerySet, Q |
|
4 | from django.db.models import Count, Sum, QuerySet, Q | |
5 | from django.utils import timezone |
|
5 | from django.utils import timezone | |
6 | from django.db import models |
|
6 | from django.db import models | |
7 |
|
7 | |||
8 | from boards import settings |
|
8 | from boards import settings | |
9 | import boards |
|
9 | import boards | |
10 | from boards.utils import cached_result, datetime_to_epoch |
|
10 | from boards.utils import cached_result, datetime_to_epoch | |
11 | from boards.models.post import Post |
|
11 | from boards.models.post import Post | |
12 | from boards.models.tag import Tag |
|
12 | from boards.models.tag import Tag | |
13 |
|
13 | |||
14 | FAV_THREAD_NO_UPDATES = -1 |
|
14 | FAV_THREAD_NO_UPDATES = -1 | |
15 |
|
15 | |||
16 |
|
16 | |||
17 | __author__ = 'neko259' |
|
17 | __author__ = 'neko259' | |
18 |
|
18 | |||
19 |
|
19 | |||
20 | logger = logging.getLogger(__name__) |
|
20 | logger = logging.getLogger(__name__) | |
21 |
|
21 | |||
22 |
|
22 | |||
23 | WS_NOTIFICATION_TYPE_NEW_POST = 'new_post' |
|
23 | WS_NOTIFICATION_TYPE_NEW_POST = 'new_post' | |
24 | WS_NOTIFICATION_TYPE = 'notification_type' |
|
24 | WS_NOTIFICATION_TYPE = 'notification_type' | |
25 |
|
25 | |||
26 | WS_CHANNEL_THREAD = "thread:" |
|
26 | WS_CHANNEL_THREAD = "thread:" | |
27 |
|
27 | |||
28 |
|
28 | |||
29 | class ThreadManager(models.Manager): |
|
29 | class ThreadManager(models.Manager): | |
30 | def process_oldest_threads(self): |
|
30 | def process_oldest_threads(self): | |
31 | """ |
|
31 | """ | |
32 | Preserves maximum thread count. If there are too many threads, |
|
32 | Preserves maximum thread count. If there are too many threads, | |
33 | archive or delete the old ones. |
|
33 | archive or delete the old ones. | |
34 | """ |
|
34 | """ | |
35 |
|
35 | |||
36 | threads = Thread.objects.filter(archived=False).order_by('-bump_time') |
|
36 | threads = Thread.objects.filter(archived=False).order_by('-bump_time') | |
37 | thread_count = threads.count() |
|
37 | thread_count = threads.count() | |
38 |
|
38 | |||
39 | max_thread_count = settings.get_int('Messages', 'MaxThreadCount') |
|
39 | max_thread_count = settings.get_int('Messages', 'MaxThreadCount') | |
40 | if thread_count > max_thread_count: |
|
40 | if thread_count > max_thread_count: | |
41 | num_threads_to_delete = thread_count - max_thread_count |
|
41 | num_threads_to_delete = thread_count - max_thread_count | |
42 | old_threads = threads[thread_count - num_threads_to_delete:] |
|
42 | old_threads = threads[thread_count - num_threads_to_delete:] | |
43 |
|
43 | |||
44 | for thread in old_threads: |
|
44 | for thread in old_threads: | |
45 | if settings.get_bool('Storage', 'ArchiveThreads'): |
|
45 | if settings.get_bool('Storage', 'ArchiveThreads'): | |
46 | self._archive_thread(thread) |
|
46 | self._archive_thread(thread) | |
47 | else: |
|
47 | else: | |
48 | thread.delete() |
|
48 | thread.delete() | |
49 |
|
49 | |||
50 | logger.info('Processed %d old threads' % num_threads_to_delete) |
|
50 | logger.info('Processed %d old threads' % num_threads_to_delete) | |
51 |
|
51 | |||
52 | def _archive_thread(self, thread): |
|
52 | def _archive_thread(self, thread): | |
53 | thread.archived = True |
|
53 | thread.archived = True | |
54 | thread.bumpable = False |
|
54 | thread.bumpable = False | |
55 | thread.last_edit_time = timezone.now() |
|
55 | thread.last_edit_time = timezone.now() | |
56 | thread.update_posts_time() |
|
56 | thread.update_posts_time() | |
57 | thread.save(update_fields=['archived', 'last_edit_time', 'bumpable']) |
|
57 | thread.save(update_fields=['archived', 'last_edit_time', 'bumpable']) | |
58 |
|
58 | |||
59 | def get_new_posts(self, datas): |
|
59 | def get_new_posts(self, datas): | |
60 | query = None |
|
60 | query = None | |
61 | # TODO Use classes instead of dicts |
|
61 | # TODO Use classes instead of dicts | |
62 | for data in datas: |
|
62 | for data in datas: | |
63 | if data['last_id'] != FAV_THREAD_NO_UPDATES: |
|
63 | if data['last_id'] != FAV_THREAD_NO_UPDATES: | |
64 | q = (Q(id=data['op'].get_thread().id) |
|
64 | q = (Q(id=data['op'].get_thread().id) | |
65 | & Q(multi_replies__id__gt=data['last_id'])) |
|
65 | & Q(multi_replies__id__gt=data['last_id'])) | |
66 | if query is None: |
|
66 | if query is None: | |
67 | query = q |
|
67 | query = q | |
68 | else: |
|
68 | else: | |
69 | query = query | q |
|
69 | query = query | q | |
70 | if query is not None: |
|
70 | if query is not None: | |
71 | return self.filter(query).annotate( |
|
71 | return self.filter(query).annotate( | |
72 | new_post_count=Count('multi_replies')) |
|
72 | new_post_count=Count('multi_replies')) | |
73 |
|
73 | |||
74 | def get_new_post_count(self, datas): |
|
74 | def get_new_post_count(self, datas): | |
75 |
|
|
75 | new_posts = self.get_new_posts(datas) | |
76 |
|
|
76 | return new_posts.aggregate(total_count=Count('multi_replies'))\ | |
|
77 | ['total_count'] if new_posts else 0 | |||
77 |
|
78 | |||
78 |
|
79 | |||
79 | def get_thread_max_posts(): |
|
80 | def get_thread_max_posts(): | |
80 | return settings.get_int('Messages', 'MaxPostsPerThread') |
|
81 | return settings.get_int('Messages', 'MaxPostsPerThread') | |
81 |
|
82 | |||
82 |
|
83 | |||
83 | class Thread(models.Model): |
|
84 | class Thread(models.Model): | |
84 | objects = ThreadManager() |
|
85 | objects = ThreadManager() | |
85 |
|
86 | |||
86 | class Meta: |
|
87 | class Meta: | |
87 | app_label = 'boards' |
|
88 | app_label = 'boards' | |
88 |
|
89 | |||
89 | tags = models.ManyToManyField('Tag', related_name='thread_tags') |
|
90 | tags = models.ManyToManyField('Tag', related_name='thread_tags') | |
90 | bump_time = models.DateTimeField(db_index=True) |
|
91 | bump_time = models.DateTimeField(db_index=True) | |
91 | last_edit_time = models.DateTimeField() |
|
92 | last_edit_time = models.DateTimeField() | |
92 | archived = models.BooleanField(default=False) |
|
93 | archived = models.BooleanField(default=False) | |
93 | bumpable = models.BooleanField(default=True) |
|
94 | bumpable = models.BooleanField(default=True) | |
94 | max_posts = models.IntegerField(default=get_thread_max_posts) |
|
95 | max_posts = models.IntegerField(default=get_thread_max_posts) | |
95 |
|
96 | |||
96 | def get_tags(self) -> QuerySet: |
|
97 | def get_tags(self) -> QuerySet: | |
97 | """ |
|
98 | """ | |
98 | Gets a sorted tag list. |
|
99 | Gets a sorted tag list. | |
99 | """ |
|
100 | """ | |
100 |
|
101 | |||
101 | return self.tags.order_by('name') |
|
102 | return self.tags.order_by('name') | |
102 |
|
103 | |||
103 | def bump(self): |
|
104 | def bump(self): | |
104 | """ |
|
105 | """ | |
105 | Bumps (moves to up) thread if possible. |
|
106 | Bumps (moves to up) thread if possible. | |
106 | """ |
|
107 | """ | |
107 |
|
108 | |||
108 | if self.can_bump(): |
|
109 | if self.can_bump(): | |
109 | self.bump_time = self.last_edit_time |
|
110 | self.bump_time = self.last_edit_time | |
110 |
|
111 | |||
111 | self.update_bump_status() |
|
112 | self.update_bump_status() | |
112 |
|
113 | |||
113 | logger.info('Bumped thread %d' % self.id) |
|
114 | logger.info('Bumped thread %d' % self.id) | |
114 |
|
115 | |||
115 | def has_post_limit(self) -> bool: |
|
116 | def has_post_limit(self) -> bool: | |
116 | return self.max_posts > 0 |
|
117 | return self.max_posts > 0 | |
117 |
|
118 | |||
118 | def update_bump_status(self, exclude_posts=None): |
|
119 | def update_bump_status(self, exclude_posts=None): | |
119 | if self.has_post_limit() and self.get_reply_count() >= self.max_posts: |
|
120 | if self.has_post_limit() and self.get_reply_count() >= self.max_posts: | |
120 | self.bumpable = False |
|
121 | self.bumpable = False | |
121 | self.update_posts_time(exclude_posts=exclude_posts) |
|
122 | self.update_posts_time(exclude_posts=exclude_posts) | |
122 |
|
123 | |||
123 | def _get_cache_key(self): |
|
124 | def _get_cache_key(self): | |
124 | return [datetime_to_epoch(self.last_edit_time)] |
|
125 | return [datetime_to_epoch(self.last_edit_time)] | |
125 |
|
126 | |||
126 | @cached_result(key_method=_get_cache_key) |
|
127 | @cached_result(key_method=_get_cache_key) | |
127 | def get_reply_count(self) -> int: |
|
128 | def get_reply_count(self) -> int: | |
128 | return self.get_replies().count() |
|
129 | return self.get_replies().count() | |
129 |
|
130 | |||
130 | @cached_result(key_method=_get_cache_key) |
|
131 | @cached_result(key_method=_get_cache_key) | |
131 | def get_images_count(self) -> int: |
|
132 | def get_images_count(self) -> int: | |
132 | return self.get_replies().annotate(images_count=Count( |
|
133 | return self.get_replies().annotate(images_count=Count( | |
133 | 'images')).aggregate(Sum('images_count'))['images_count__sum'] |
|
134 | 'images')).aggregate(Sum('images_count'))['images_count__sum'] | |
134 |
|
135 | |||
135 | def can_bump(self) -> bool: |
|
136 | def can_bump(self) -> bool: | |
136 | """ |
|
137 | """ | |
137 | Checks if the thread can be bumped by replying to it. |
|
138 | Checks if the thread can be bumped by replying to it. | |
138 | """ |
|
139 | """ | |
139 |
|
140 | |||
140 | return self.bumpable and not self.is_archived() |
|
141 | return self.bumpable and not self.is_archived() | |
141 |
|
142 | |||
142 | def get_last_replies(self) -> QuerySet: |
|
143 | def get_last_replies(self) -> QuerySet: | |
143 | """ |
|
144 | """ | |
144 | Gets several last replies, not including opening post |
|
145 | Gets several last replies, not including opening post | |
145 | """ |
|
146 | """ | |
146 |
|
147 | |||
147 | last_replies_count = settings.get_int('View', 'LastRepliesCount') |
|
148 | last_replies_count = settings.get_int('View', 'LastRepliesCount') | |
148 |
|
149 | |||
149 | if last_replies_count > 0: |
|
150 | if last_replies_count > 0: | |
150 | reply_count = self.get_reply_count() |
|
151 | reply_count = self.get_reply_count() | |
151 |
|
152 | |||
152 | if reply_count > 0: |
|
153 | if reply_count > 0: | |
153 | reply_count_to_show = min(last_replies_count, |
|
154 | reply_count_to_show = min(last_replies_count, | |
154 | reply_count - 1) |
|
155 | reply_count - 1) | |
155 | replies = self.get_replies() |
|
156 | replies = self.get_replies() | |
156 | last_replies = replies[reply_count - reply_count_to_show:] |
|
157 | last_replies = replies[reply_count - reply_count_to_show:] | |
157 |
|
158 | |||
158 | return last_replies |
|
159 | return last_replies | |
159 |
|
160 | |||
160 | def get_skipped_replies_count(self) -> int: |
|
161 | def get_skipped_replies_count(self) -> int: | |
161 | """ |
|
162 | """ | |
162 | Gets number of posts between opening post and last replies. |
|
163 | Gets number of posts between opening post and last replies. | |
163 | """ |
|
164 | """ | |
164 | reply_count = self.get_reply_count() |
|
165 | reply_count = self.get_reply_count() | |
165 | last_replies_count = min(settings.get_int('View', 'LastRepliesCount'), |
|
166 | last_replies_count = min(settings.get_int('View', 'LastRepliesCount'), | |
166 | reply_count - 1) |
|
167 | reply_count - 1) | |
167 | return reply_count - last_replies_count - 1 |
|
168 | return reply_count - last_replies_count - 1 | |
168 |
|
169 | |||
169 | def get_replies(self, view_fields_only=False) -> QuerySet: |
|
170 | def get_replies(self, view_fields_only=False) -> QuerySet: | |
170 | """ |
|
171 | """ | |
171 | Gets sorted thread posts |
|
172 | Gets sorted thread posts | |
172 | """ |
|
173 | """ | |
173 |
|
174 | |||
174 | query = self.multi_replies.order_by('pub_time').prefetch_related( |
|
175 | query = self.multi_replies.order_by('pub_time').prefetch_related( | |
175 | 'images', 'thread', 'threads', 'attachments') |
|
176 | 'images', 'thread', 'threads', 'attachments') | |
176 | if view_fields_only: |
|
177 | if view_fields_only: | |
177 | query = query.defer('poster_ip') |
|
178 | query = query.defer('poster_ip') | |
178 | return query.all() |
|
179 | return query.all() | |
179 |
|
180 | |||
180 | def get_top_level_replies(self) -> QuerySet: |
|
181 | def get_top_level_replies(self) -> QuerySet: | |
181 | return self.get_replies().exclude(refposts__threads__in=[self]) |
|
182 | return self.get_replies().exclude(refposts__threads__in=[self]) | |
182 |
|
183 | |||
183 | def get_replies_with_images(self, view_fields_only=False) -> QuerySet: |
|
184 | def get_replies_with_images(self, view_fields_only=False) -> QuerySet: | |
184 | """ |
|
185 | """ | |
185 | Gets replies that have at least one image attached |
|
186 | Gets replies that have at least one image attached | |
186 | """ |
|
187 | """ | |
187 |
|
188 | |||
188 | return self.get_replies(view_fields_only).annotate(images_count=Count( |
|
189 | return self.get_replies(view_fields_only).annotate(images_count=Count( | |
189 | 'images')).filter(images_count__gt=0) |
|
190 | 'images')).filter(images_count__gt=0) | |
190 |
|
191 | |||
191 | def get_opening_post(self, only_id=False) -> Post: |
|
192 | def get_opening_post(self, only_id=False) -> Post: | |
192 | """ |
|
193 | """ | |
193 | Gets the first post of the thread |
|
194 | Gets the first post of the thread | |
194 | """ |
|
195 | """ | |
195 |
|
196 | |||
196 | query = self.get_replies().order_by('pub_time') |
|
197 | query = self.get_replies().order_by('pub_time') | |
197 | if only_id: |
|
198 | if only_id: | |
198 | query = query.only('id') |
|
199 | query = query.only('id') | |
199 | opening_post = query.first() |
|
200 | opening_post = query.first() | |
200 |
|
201 | |||
201 | return opening_post |
|
202 | return opening_post | |
202 |
|
203 | |||
203 | @cached_result() |
|
204 | @cached_result() | |
204 | def get_opening_post_id(self) -> int: |
|
205 | def get_opening_post_id(self) -> int: | |
205 | """ |
|
206 | """ | |
206 | Gets ID of the first thread post. |
|
207 | Gets ID of the first thread post. | |
207 | """ |
|
208 | """ | |
208 |
|
209 | |||
209 | return self.get_opening_post(only_id=True).id |
|
210 | return self.get_opening_post(only_id=True).id | |
210 |
|
211 | |||
211 | def get_pub_time(self): |
|
212 | def get_pub_time(self): | |
212 | """ |
|
213 | """ | |
213 | Gets opening post's pub time because thread does not have its own one. |
|
214 | Gets opening post's pub time because thread does not have its own one. | |
214 | """ |
|
215 | """ | |
215 |
|
216 | |||
216 | return self.get_opening_post().pub_time |
|
217 | return self.get_opening_post().pub_time | |
217 |
|
218 | |||
218 | def __str__(self): |
|
219 | def __str__(self): | |
219 | return 'T#{}/{}'.format(self.id, self.get_opening_post_id()) |
|
220 | return 'T#{}/{}'.format(self.id, self.get_opening_post_id()) | |
220 |
|
221 | |||
221 | def get_tag_url_list(self) -> list: |
|
222 | def get_tag_url_list(self) -> list: | |
222 | return boards.models.Tag.objects.get_tag_url_list(self.get_tags()) |
|
223 | return boards.models.Tag.objects.get_tag_url_list(self.get_tags()) | |
223 |
|
224 | |||
224 | def update_posts_time(self, exclude_posts=None): |
|
225 | def update_posts_time(self, exclude_posts=None): | |
225 | last_edit_time = self.last_edit_time |
|
226 | last_edit_time = self.last_edit_time | |
226 |
|
227 | |||
227 |
for post in self. |
|
228 | for post in self.multi_replies.all(): | |
228 | if exclude_posts is None or post not in exclude_posts: |
|
229 | if exclude_posts is None or post not in exclude_posts: | |
229 | # Manual update is required because uids are generated on save |
|
230 | # Manual update is required because uids are generated on save | |
230 | post.last_edit_time = last_edit_time |
|
231 | post.last_edit_time = last_edit_time | |
231 | post.save(update_fields=['last_edit_time']) |
|
232 | post.save(update_fields=['last_edit_time']) | |
232 |
|
233 | |||
233 | post.get_threads().update(last_edit_time=last_edit_time) |
|
234 | post.get_threads().update(last_edit_time=last_edit_time) | |
234 |
|
235 | |||
235 | def notify_clients(self): |
|
236 | def notify_clients(self): | |
236 | if not settings.get_bool('External', 'WebsocketsEnabled'): |
|
237 | if not settings.get_bool('External', 'WebsocketsEnabled'): | |
237 | return |
|
238 | return | |
238 |
|
239 | |||
239 | client = Client() |
|
240 | client = Client() | |
240 |
|
241 | |||
241 | channel_name = WS_CHANNEL_THREAD + str(self.get_opening_post_id()) |
|
242 | channel_name = WS_CHANNEL_THREAD + str(self.get_opening_post_id()) | |
242 | client.publish(channel_name, { |
|
243 | client.publish(channel_name, { | |
243 | WS_NOTIFICATION_TYPE: WS_NOTIFICATION_TYPE_NEW_POST, |
|
244 | WS_NOTIFICATION_TYPE: WS_NOTIFICATION_TYPE_NEW_POST, | |
244 | }) |
|
245 | }) | |
245 | client.send() |
|
246 | client.send() | |
246 |
|
247 | |||
247 | def get_absolute_url(self): |
|
248 | def get_absolute_url(self): | |
248 | return self.get_opening_post().get_absolute_url() |
|
249 | return self.get_opening_post().get_absolute_url() | |
249 |
|
250 | |||
250 | def get_required_tags(self): |
|
251 | def get_required_tags(self): | |
251 | return self.get_tags().filter(required=True) |
|
252 | return self.get_tags().filter(required=True) | |
252 |
|
253 | |||
253 | def get_replies_newer(self, post_id): |
|
254 | def get_replies_newer(self, post_id): | |
254 | return self.get_replies().filter(id__gt=post_id) |
|
255 | return self.get_replies().filter(id__gt=post_id) | |
255 |
|
256 | |||
256 | def is_archived(self): |
|
257 | def is_archived(self): | |
257 | return self.archived |
|
258 | return self.archived |
General Comments 0
You need to be logged in to leave comments.
Login now