##// END OF EJS Templates
Fixed reflink migration. Fixed new_post API when there are not favorite...
neko259 -
r1347:239fa253 default
parent child Browse files
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 refuild_refmap(apps, schema_editor):
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 post.build_refmap()
12 refposts = list()
13 for refpost in post.referenced_posts.all():
14 result = '<a href="{}">&gt;&gt;{}</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(refuild_refmap),
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 return self.get_new_posts(datas).aggregate(
75 new_posts = self.get_new_posts(datas)
76 total_count=Count('multi_replies'))['total_count']
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.post_set.all():
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