##// END OF EJS Templates
Fixed issues with new images storage
neko259 -
r1591:af1d8bd1 default
parent child Browse files
Show More
@@ -1,150 +1,150 b''
1 1 import hashlib
2 2 from boards.models.attachment import FILE_TYPES_IMAGE
3 3 from django.template.loader import render_to_string
4 4 from django.db import models
5 5 from django.db.models import Count
6 6 from django.core.urlresolvers import reverse
7 7
8 8 from boards.models import Attachment
9 9 from boards.models.base import Viewable
10 10 from boards.models.thread import STATUS_ACTIVE, STATUS_BUMPLIMIT, STATUS_ARCHIVE
11 11 from boards.utils import cached_result
12 12 import boards
13 13
14 14 __author__ = 'neko259'
15 15
16 16
17 17 RELATED_TAGS_COUNT = 5
18 18
19 19
20 20 class TagManager(models.Manager):
21 21
22 22 def get_not_empty_tags(self):
23 23 """
24 24 Gets tags that have non-archived threads.
25 25 """
26 26
27 27 return self.annotate(num_threads=Count('thread_tags')).filter(num_threads__gt=0)\
28 28 .order_by('-required', 'name')
29 29
30 30 def get_tag_url_list(self, tags: list) -> str:
31 31 """
32 32 Gets a comma-separated list of tag links.
33 33 """
34 34
35 35 return ', '.join([tag.get_view() for tag in tags])
36 36
37 37
38 38 class Tag(models.Model, Viewable):
39 39 """
40 40 A tag is a text node assigned to the thread. The tag serves as a board
41 41 section. There can be multiple tags for each thread
42 42 """
43 43
44 44 objects = TagManager()
45 45
46 46 class Meta:
47 47 app_label = 'boards'
48 48 ordering = ('name',)
49 49
50 50 name = models.CharField(max_length=100, db_index=True, unique=True)
51 51 required = models.BooleanField(default=False, db_index=True)
52 52 description = models.TextField(blank=True)
53 53
54 54 parent = models.ForeignKey('Tag', null=True, blank=True,
55 55 related_name='children')
56 56
57 57 def __str__(self):
58 58 return self.name
59 59
60 60 def is_empty(self) -> bool:
61 61 """
62 62 Checks if the tag has some threads.
63 63 """
64 64
65 65 return self.get_thread_count() == 0
66 66
67 67 def get_thread_count(self, status=None) -> int:
68 68 threads = self.get_threads()
69 69 if status is not None:
70 70 threads = threads.filter(status=status)
71 71 return threads.count()
72 72
73 73 def get_active_thread_count(self) -> int:
74 74 return self.get_thread_count(status=STATUS_ACTIVE)
75 75
76 76 def get_bumplimit_thread_count(self) -> int:
77 77 return self.get_thread_count(status=STATUS_BUMPLIMIT)
78 78
79 79 def get_archived_thread_count(self) -> int:
80 80 return self.get_thread_count(status=STATUS_ARCHIVE)
81 81
82 82 def get_absolute_url(self):
83 83 return reverse('tag', kwargs={'tag_name': self.name})
84 84
85 85 def get_threads(self):
86 86 return self.thread_tags.order_by('-bump_time')
87 87
88 88 def is_required(self):
89 89 return self.required
90 90
91 91 def get_view(self):
92 92 link = '<a class="tag" href="{}">{}</a>'.format(
93 93 self.get_absolute_url(), self.name)
94 94 if self.is_required():
95 95 link = '<b>{}</b>'.format(link)
96 96 return link
97 97
98 98 def get_search_view(self, *args, **kwargs):
99 99 return render_to_string('boards/tag.html', {
100 100 'tag': self,
101 101 })
102 102
103 103 @cached_result()
104 104 def get_post_count(self):
105 105 return self.get_threads().aggregate(num_posts=Count('multi_replies'))['num_posts']
106 106
107 107 def get_description(self):
108 108 return self.description
109 109
110 110 def get_random_image_post(self, status=[STATUS_ACTIVE, STATUS_BUMPLIMIT]):
111 111 posts = boards.models.Post.objects.filter(attachments__mimetype__in=FILE_TYPES_IMAGE)\
112 112 .annotate(images_count=Count(
113 113 'attachments')).filter(images_count__gt=0, threads__tags__in=[self])
114 114 if status is not None:
115 115 posts = posts.filter(thread__status__in=status)
116 116 return posts.order_by('?').first()
117 117
118 118 def get_first_letter(self):
119 119 return self.name and self.name[0] or ''
120 120
121 121 def get_related_tags(self):
122 122 return set(Tag.objects.filter(thread_tags__in=self.get_threads()).exclude(
123 123 id=self.id).order_by('?')[:RELATED_TAGS_COUNT])
124 124
125 125 @cached_result()
126 126 def get_color(self):
127 127 """
128 128 Gets color hashed from the tag name.
129 129 """
130 130 return hashlib.md5(self.name.encode()).hexdigest()[:6]
131 131
132 132 def get_parent(self):
133 133 return self.parent
134 134
135 135 def get_all_parents(self):
136 136 parents = list()
137 137 parent = self.get_parent()
138 138 if parent and parent not in parents:
139 139 parents.insert(0, parent)
140 140 parents = parent.get_all_parents() + parents
141 141
142 142 return parents
143 143
144 144 def get_children(self):
145 145 return self.children
146 146
147 147 def get_images(self):
148 148 return Attachment.objects.filter(
149 post_attachments__thread__tags__in=[self]).filter(
150 mimetype__in=FILE_TYPES_IMAGE).order_by('-post_images__pub_time') No newline at end of file
149 attachment_posts__thread__tags__in=[self]).filter(
150 mimetype__in=FILE_TYPES_IMAGE).order_by('-attachment_posts__pub_time') No newline at end of file
@@ -1,325 +1,325 b''
1 1 import logging
2 2 from adjacent import Client
3 3 from boards.models.attachment import FILE_TYPES_IMAGE
4 4
5 5 from django.db.models import Count, Sum, QuerySet, Q
6 6 from django.utils import timezone
7 7 from django.db import models, transaction
8 8
9 9 from boards.models import STATUS_BUMPLIMIT, STATUS_ACTIVE, STATUS_ARCHIVE
10 10
11 11 from boards import settings
12 12 import boards
13 13 from boards.utils import cached_result, datetime_to_epoch
14 14 from boards.models.post import Post
15 15 from boards.models.tag import Tag
16 16
17 17 FAV_THREAD_NO_UPDATES = -1
18 18
19 19
20 20 __author__ = 'neko259'
21 21
22 22
23 23 logger = logging.getLogger(__name__)
24 24
25 25
26 26 WS_NOTIFICATION_TYPE_NEW_POST = 'new_post'
27 27 WS_NOTIFICATION_TYPE = 'notification_type'
28 28
29 29 WS_CHANNEL_THREAD = "thread:"
30 30
31 31 STATUS_CHOICES = (
32 32 (STATUS_ACTIVE, STATUS_ACTIVE),
33 33 (STATUS_BUMPLIMIT, STATUS_BUMPLIMIT),
34 34 (STATUS_ARCHIVE, STATUS_ARCHIVE),
35 35 )
36 36
37 37
38 38 class ThreadManager(models.Manager):
39 39 def process_oldest_threads(self):
40 40 """
41 41 Preserves maximum thread count. If there are too many threads,
42 42 archive or delete the old ones.
43 43 """
44 44
45 45 threads = Thread.objects.exclude(status=STATUS_ARCHIVE).order_by('-bump_time')
46 46 thread_count = threads.count()
47 47
48 48 max_thread_count = settings.get_int('Messages', 'MaxThreadCount')
49 49 if thread_count > max_thread_count:
50 50 num_threads_to_delete = thread_count - max_thread_count
51 51 old_threads = threads[thread_count - num_threads_to_delete:]
52 52
53 53 for thread in old_threads:
54 54 if settings.get_bool('Storage', 'ArchiveThreads'):
55 55 self._archive_thread(thread)
56 56 else:
57 57 thread.delete()
58 58
59 59 logger.info('Processed %d old threads' % num_threads_to_delete)
60 60
61 61 def _archive_thread(self, thread):
62 62 thread.status = STATUS_ARCHIVE
63 63 thread.last_edit_time = timezone.now()
64 64 thread.update_posts_time()
65 65 thread.save(update_fields=['last_edit_time', 'status'])
66 66
67 67 def get_new_posts(self, datas):
68 68 query = None
69 69 # TODO Use classes instead of dicts
70 70 for data in datas:
71 71 if data['last_id'] != FAV_THREAD_NO_UPDATES:
72 72 q = (Q(id=data['op'].get_thread_id())
73 73 & Q(multi_replies__id__gt=data['last_id']))
74 74 if query is None:
75 75 query = q
76 76 else:
77 77 query = query | q
78 78 if query is not None:
79 79 return self.filter(query).annotate(
80 80 new_post_count=Count('multi_replies'))
81 81
82 82 def get_new_post_count(self, datas):
83 83 new_posts = self.get_new_posts(datas)
84 84 return new_posts.aggregate(total_count=Count('multi_replies'))\
85 85 ['total_count'] if new_posts else 0
86 86
87 87
88 88 def get_thread_max_posts():
89 89 return settings.get_int('Messages', 'MaxPostsPerThread')
90 90
91 91
92 92 class Thread(models.Model):
93 93 objects = ThreadManager()
94 94
95 95 class Meta:
96 96 app_label = 'boards'
97 97
98 98 tags = models.ManyToManyField('Tag', related_name='thread_tags')
99 99 bump_time = models.DateTimeField(db_index=True)
100 100 last_edit_time = models.DateTimeField()
101 101 max_posts = models.IntegerField(default=get_thread_max_posts)
102 102 status = models.CharField(max_length=50, default=STATUS_ACTIVE,
103 103 choices=STATUS_CHOICES)
104 104 monochrome = models.BooleanField(default=False)
105 105
106 106 def get_tags(self) -> QuerySet:
107 107 """
108 108 Gets a sorted tag list.
109 109 """
110 110
111 111 return self.tags.order_by('name')
112 112
113 113 def bump(self):
114 114 """
115 115 Bumps (moves to up) thread if possible.
116 116 """
117 117
118 118 if self.can_bump():
119 119 self.bump_time = self.last_edit_time
120 120
121 121 self.update_bump_status()
122 122
123 123 logger.info('Bumped thread %d' % self.id)
124 124
125 125 def has_post_limit(self) -> bool:
126 126 return self.max_posts > 0
127 127
128 128 def update_bump_status(self, exclude_posts=None):
129 129 if self.has_post_limit() and self.get_reply_count() >= self.max_posts:
130 130 self.status = STATUS_BUMPLIMIT
131 131 self.update_posts_time(exclude_posts=exclude_posts)
132 132
133 133 def _get_cache_key(self):
134 134 return [datetime_to_epoch(self.last_edit_time)]
135 135
136 136 @cached_result(key_method=_get_cache_key)
137 137 def get_reply_count(self) -> int:
138 138 return self.get_replies().count()
139 139
140 140 @cached_result(key_method=_get_cache_key)
141 141 def get_images_count(self) -> int:
142 142 return self.get_replies().filter(
143 143 attachments__mimetype__in=FILE_TYPES_IMAGE)\
144 144 .annotate(images_count=Count(
145 'attachments')).aggregate(Sum('images_count'))['images_count__sum']
145 'attachments')).aggregate(Sum('images_count'))['images_count__sum'] or 0
146 146
147 147 def can_bump(self) -> bool:
148 148 """
149 149 Checks if the thread can be bumped by replying to it.
150 150 """
151 151
152 152 return self.get_status() == STATUS_ACTIVE
153 153
154 154 def get_last_replies(self) -> QuerySet:
155 155 """
156 156 Gets several last replies, not including opening post
157 157 """
158 158
159 159 last_replies_count = settings.get_int('View', 'LastRepliesCount')
160 160
161 161 if last_replies_count > 0:
162 162 reply_count = self.get_reply_count()
163 163
164 164 if reply_count > 0:
165 165 reply_count_to_show = min(last_replies_count,
166 166 reply_count - 1)
167 167 replies = self.get_replies()
168 168 last_replies = replies[reply_count - reply_count_to_show:]
169 169
170 170 return last_replies
171 171
172 172 def get_skipped_replies_count(self) -> int:
173 173 """
174 174 Gets number of posts between opening post and last replies.
175 175 """
176 176 reply_count = self.get_reply_count()
177 177 last_replies_count = min(settings.get_int('View', 'LastRepliesCount'),
178 178 reply_count - 1)
179 179 return reply_count - last_replies_count - 1
180 180
181 181 def get_replies(self, view_fields_only=False) -> QuerySet:
182 182 """
183 183 Gets sorted thread posts
184 184 """
185 185
186 186 query = self.multi_replies.order_by('pub_time').prefetch_related(
187 187 'thread', 'attachments')
188 188 if view_fields_only:
189 189 query = query.defer('poster_ip')
190 190 return query
191 191
192 192 def get_top_level_replies(self) -> QuerySet:
193 193 return self.get_replies().exclude(refposts__threads__in=[self])
194 194
195 195 def get_replies_with_images(self, view_fields_only=False) -> QuerySet:
196 196 """
197 197 Gets replies that have at least one image attached
198 198 """
199 199 return self.get_replies(view_fields_only).filter(
200 200 attachments__mimetype__in=FILE_TYPES_IMAGE).annotate(images_count=Count(
201 201 'attachments')).filter(images_count__gt=0)
202 202
203 203 def get_opening_post(self, only_id=False) -> Post:
204 204 """
205 205 Gets the first post of the thread
206 206 """
207 207
208 208 query = self.get_replies().filter(opening=True)
209 209 if only_id:
210 210 query = query.only('id')
211 211 opening_post = query.first()
212 212
213 213 return opening_post
214 214
215 215 @cached_result()
216 216 def get_opening_post_id(self) -> int:
217 217 """
218 218 Gets ID of the first thread post.
219 219 """
220 220
221 221 return self.get_opening_post(only_id=True).id
222 222
223 223 def get_pub_time(self):
224 224 """
225 225 Gets opening post's pub time because thread does not have its own one.
226 226 """
227 227
228 228 return self.get_opening_post().pub_time
229 229
230 230 def __str__(self):
231 231 return 'T#{}/{}'.format(self.id, self.get_opening_post_id())
232 232
233 233 def get_tag_url_list(self) -> list:
234 234 return boards.models.Tag.objects.get_tag_url_list(self.get_tags())
235 235
236 236 def update_posts_time(self, exclude_posts=None):
237 237 last_edit_time = self.last_edit_time
238 238
239 239 for post in self.multi_replies.all():
240 240 if exclude_posts is None or post not in exclude_posts:
241 241 # Manual update is required because uids are generated on save
242 242 post.last_edit_time = last_edit_time
243 243 post.save(update_fields=['last_edit_time'])
244 244
245 245 post.get_threads().update(last_edit_time=last_edit_time)
246 246
247 247 def notify_clients(self):
248 248 if not settings.get_bool('External', 'WebsocketsEnabled'):
249 249 return
250 250
251 251 client = Client()
252 252
253 253 channel_name = WS_CHANNEL_THREAD + str(self.get_opening_post_id())
254 254 client.publish(channel_name, {
255 255 WS_NOTIFICATION_TYPE: WS_NOTIFICATION_TYPE_NEW_POST,
256 256 })
257 257 client.send()
258 258
259 259 def get_absolute_url(self):
260 260 return self.get_opening_post().get_absolute_url()
261 261
262 262 def get_required_tags(self):
263 263 return self.get_tags().filter(required=True)
264 264
265 265 def get_replies_newer(self, post_id):
266 266 return self.get_replies().filter(id__gt=post_id)
267 267
268 268 def is_archived(self):
269 269 return self.get_status() == STATUS_ARCHIVE
270 270
271 271 def get_status(self):
272 272 return self.status
273 273
274 274 def is_monochrome(self):
275 275 return self.monochrome
276 276
277 277 # If tags have parent, add them to the tag list
278 278 @transaction.atomic
279 279 def refresh_tags(self):
280 280 for tag in self.get_tags().all():
281 281 parents = tag.get_all_parents()
282 282 if len(parents) > 0:
283 283 self.tags.add(*parents)
284 284
285 285 def get_reply_tree(self):
286 286 replies = self.get_replies().prefetch_related('refposts')
287 287 tree = []
288 288 for reply in replies:
289 289 parents = reply.refposts.all()
290 290
291 291 found_parent = False
292 292 searching_for_index = False
293 293
294 294 if len(parents) > 0:
295 295 index = 0
296 296 parent_depth = 0
297 297
298 298 indexes_to_insert = []
299 299
300 300 for depth, element in tree:
301 301 index += 1
302 302
303 303 # If this element is next after parent on the same level,
304 304 # insert child before it
305 305 if searching_for_index and depth <= parent_depth:
306 306 indexes_to_insert.append((index - 1, parent_depth))
307 307 searching_for_index = False
308 308
309 309 if element in parents:
310 310 found_parent = True
311 311 searching_for_index = True
312 312 parent_depth = depth
313 313
314 314 if not found_parent:
315 315 tree.append((0, reply))
316 316 else:
317 317 if searching_for_index:
318 318 tree.append((parent_depth + 1, reply))
319 319
320 320 offset = 0
321 321 for last_index, parent_depth in indexes_to_insert:
322 322 tree.insert(last_index + offset, (parent_depth + 1, reply))
323 323 offset += 1
324 324
325 325 return tree
General Comments 0
You need to be logged in to leave comments. Login now