##// END OF EJS Templates
Show threads' sections in the landing page
neko259 -
r1739:a7aa222d default
parent child Browse files
Show More
@@ -1,325 +1,328 b''
1 1 import logging
2 2 from adjacent import Client
3 3 from datetime import timedelta
4 4
5 5
6 6 from django.db.models import Count, Sum, QuerySet, Q
7 7 from django.utils import timezone
8 8 from django.db import models, transaction
9 9
10 10 from boards.models.attachment import FILE_TYPES_IMAGE
11 11 from boards.models import STATUS_BUMPLIMIT, STATUS_ACTIVE, STATUS_ARCHIVE
12 12
13 13 from boards import settings
14 14 import boards
15 15 from boards.utils import cached_result, datetime_to_epoch
16 16 from boards.models.post import Post
17 17 from boards.models.tag import Tag
18 18
19 19 FAV_THREAD_NO_UPDATES = -1
20 20
21 21
22 22 __author__ = 'neko259'
23 23
24 24
25 25 logger = logging.getLogger(__name__)
26 26
27 27
28 28 WS_NOTIFICATION_TYPE_NEW_POST = 'new_post'
29 29 WS_NOTIFICATION_TYPE = 'notification_type'
30 30
31 31 WS_CHANNEL_THREAD = "thread:"
32 32
33 33 STATUS_CHOICES = (
34 34 (STATUS_ACTIVE, STATUS_ACTIVE),
35 35 (STATUS_BUMPLIMIT, STATUS_BUMPLIMIT),
36 36 (STATUS_ARCHIVE, STATUS_ARCHIVE),
37 37 )
38 38
39 39
40 40 class ThreadManager(models.Manager):
41 41 def process_old_threads(self):
42 42 """
43 43 Preserves maximum thread count. If there are too many threads,
44 44 archive or delete the old ones.
45 45 """
46 46 old_time_delta = settings.get_int('Messages', 'ThreadArchiveDays')
47 47 old_time = timezone.now() - timedelta(days=old_time_delta)
48 48 old_ops = Post.objects.filter(opening=True, pub_time__lte=old_time).exclude(thread__status=STATUS_ARCHIVE)
49 49
50 50 for op in old_ops:
51 51 thread = op.get_thread()
52 52 if settings.get_bool('Storage', 'ArchiveThreads'):
53 53 self._archive_thread(thread)
54 54 else:
55 55 thread.delete()
56 56 logger.info('Processed old thread {}'.format(thread))
57 57
58 58
59 59 def _archive_thread(self, thread):
60 60 thread.status = STATUS_ARCHIVE
61 61 thread.last_edit_time = timezone.now()
62 62 thread.update_posts_time()
63 63 thread.save(update_fields=['last_edit_time', 'status'])
64 64
65 65 def get_new_posts(self, datas):
66 66 query = None
67 67 # TODO Use classes instead of dicts
68 68 for data in datas:
69 69 if data['last_id'] != FAV_THREAD_NO_UPDATES:
70 70 q = (Q(id=data['op'].get_thread_id())
71 71 & Q(replies__id__gt=data['last_id']))
72 72 if query is None:
73 73 query = q
74 74 else:
75 75 query = query | q
76 76 if query is not None:
77 77 return self.filter(query).annotate(
78 78 new_post_count=Count('replies'))
79 79
80 80 def get_new_post_count(self, datas):
81 81 new_posts = self.get_new_posts(datas)
82 82 return new_posts.aggregate(total_count=Count('replies'))\
83 83 ['total_count'] if new_posts else 0
84 84
85 85
86 86 def get_thread_max_posts():
87 87 return settings.get_int('Messages', 'MaxPostsPerThread')
88 88
89 89
90 90 class Thread(models.Model):
91 91 objects = ThreadManager()
92 92
93 93 class Meta:
94 94 app_label = 'boards'
95 95
96 96 tags = models.ManyToManyField('Tag', related_name='thread_tags')
97 97 bump_time = models.DateTimeField(db_index=True)
98 98 last_edit_time = models.DateTimeField()
99 99 max_posts = models.IntegerField(default=get_thread_max_posts)
100 100 status = models.CharField(max_length=50, default=STATUS_ACTIVE,
101 101 choices=STATUS_CHOICES, db_index=True)
102 102 monochrome = models.BooleanField(default=False)
103 103
104 104 def get_tags(self) -> QuerySet:
105 105 """
106 106 Gets a sorted tag list.
107 107 """
108 108
109 109 return self.tags.order_by('name')
110 110
111 111 def bump(self):
112 112 """
113 113 Bumps (moves to up) thread if possible.
114 114 """
115 115
116 116 if self.can_bump():
117 117 self.bump_time = self.last_edit_time
118 118
119 119 self.update_bump_status()
120 120
121 121 logger.info('Bumped thread %d' % self.id)
122 122
123 123 def has_post_limit(self) -> bool:
124 124 return self.max_posts > 0
125 125
126 126 def update_bump_status(self, exclude_posts=None):
127 127 if self.has_post_limit() and self.get_reply_count() >= self.max_posts:
128 128 self.status = STATUS_BUMPLIMIT
129 129 self.update_posts_time(exclude_posts=exclude_posts)
130 130
131 131 def _get_cache_key(self):
132 132 return [datetime_to_epoch(self.last_edit_time)]
133 133
134 134 @cached_result(key_method=_get_cache_key)
135 135 def get_reply_count(self) -> int:
136 136 return self.get_replies().count()
137 137
138 138 @cached_result(key_method=_get_cache_key)
139 139 def get_images_count(self) -> int:
140 140 return self.get_replies().filter(
141 141 attachments__mimetype__in=FILE_TYPES_IMAGE)\
142 142 .annotate(images_count=Count(
143 143 'attachments')).aggregate(Sum('images_count'))['images_count__sum'] or 0
144 144
145 145 def can_bump(self) -> bool:
146 146 """
147 147 Checks if the thread can be bumped by replying to it.
148 148 """
149 149
150 150 return self.get_status() == STATUS_ACTIVE
151 151
152 152 def get_last_replies(self) -> QuerySet:
153 153 """
154 154 Gets several last replies, not including opening post
155 155 """
156 156
157 157 last_replies_count = settings.get_int('View', 'LastRepliesCount')
158 158
159 159 if last_replies_count > 0:
160 160 reply_count = self.get_reply_count()
161 161
162 162 if reply_count > 0:
163 163 reply_count_to_show = min(last_replies_count,
164 164 reply_count - 1)
165 165 replies = self.get_replies()
166 166 last_replies = replies[reply_count - reply_count_to_show:]
167 167
168 168 return last_replies
169 169
170 170 def get_skipped_replies_count(self) -> int:
171 171 """
172 172 Gets number of posts between opening post and last replies.
173 173 """
174 174 reply_count = self.get_reply_count()
175 175 last_replies_count = min(settings.get_int('View', 'LastRepliesCount'),
176 176 reply_count - 1)
177 177 return reply_count - last_replies_count - 1
178 178
179 179 # TODO Remove argument, it is not used
180 180 def get_replies(self, view_fields_only=True) -> QuerySet:
181 181 """
182 182 Gets sorted thread posts
183 183 """
184 184 query = self.replies.order_by('pub_time').prefetch_related(
185 185 'attachments')
186 186 return query
187 187
188 188 def get_viewable_replies(self) -> QuerySet:
189 189 """
190 190 Gets replies with only fields that are used for viewing.
191 191 """
192 192 return self.get_replies().defer('text', 'last_edit_time', 'version')
193 193
194 194 def get_top_level_replies(self) -> QuerySet:
195 195 return self.get_replies().exclude(refposts__threads__in=[self])
196 196
197 197 def get_replies_with_images(self, view_fields_only=False) -> QuerySet:
198 198 """
199 199 Gets replies that have at least one image attached
200 200 """
201 201 return self.get_replies(view_fields_only).filter(
202 202 attachments__mimetype__in=FILE_TYPES_IMAGE).annotate(images_count=Count(
203 203 'attachments')).filter(images_count__gt=0)
204 204
205 205 def get_opening_post(self, only_id=False) -> Post:
206 206 """
207 207 Gets the first post of the thread
208 208 """
209 209
210 210 query = self.get_replies().filter(opening=True)
211 211 if only_id:
212 212 query = query.only('id')
213 213 opening_post = query.first()
214 214
215 215 return opening_post
216 216
217 217 @cached_result()
218 218 def get_opening_post_id(self) -> int:
219 219 """
220 220 Gets ID of the first thread post.
221 221 """
222 222
223 223 return self.get_opening_post(only_id=True).id
224 224
225 225 def get_pub_time(self):
226 226 """
227 227 Gets opening post's pub time because thread does not have its own one.
228 228 """
229 229
230 230 return self.get_opening_post().pub_time
231 231
232 232 def __str__(self):
233 233 return 'T#{}'.format(self.id)
234 234
235 235 def get_tag_url_list(self) -> list:
236 236 return boards.models.Tag.objects.get_tag_url_list(self.get_tags())
237 237
238 238 def update_posts_time(self, exclude_posts=None):
239 239 last_edit_time = self.last_edit_time
240 240
241 241 for post in self.replies.all():
242 242 if exclude_posts is None or post not in exclude_posts:
243 243 # Manual update is required because uids are generated on save
244 244 post.last_edit_time = last_edit_time
245 245 post.save(update_fields=['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 def get_sections_str(self):
266 return Tag.objects.get_tag_url_list(self.get_required_tags())
267
265 268 def get_replies_newer(self, post_id):
266 269 return self.get_replies().filter(id__gt=post_id)
267 270
268 271 def is_archived(self):
269 272 return self.get_status() == STATUS_ARCHIVE
270 273
271 274 def get_status(self):
272 275 return self.status
273 276
274 277 def is_monochrome(self):
275 278 return self.monochrome
276 279
277 280 # If tags have parent, add them to the tag list
278 281 @transaction.atomic
279 282 def refresh_tags(self):
280 283 for tag in self.get_tags().all():
281 284 parents = tag.get_all_parents()
282 285 if len(parents) > 0:
283 286 self.tags.add(*parents)
284 287
285 288 def get_reply_tree(self):
286 289 replies = self.get_replies().prefetch_related('refposts')
287 290 tree = []
288 291 for reply in replies:
289 292 parents = reply.refposts.all()
290 293
291 294 found_parent = False
292 295 searching_for_index = False
293 296
294 297 if len(parents) > 0:
295 298 index = 0
296 299 parent_depth = 0
297 300
298 301 indexes_to_insert = []
299 302
300 303 for depth, element in tree:
301 304 index += 1
302 305
303 306 # If this element is next after parent on the same level,
304 307 # insert child before it
305 308 if searching_for_index and depth <= parent_depth:
306 309 indexes_to_insert.append((index - 1, parent_depth))
307 310 searching_for_index = False
308 311
309 312 if element in parents:
310 313 found_parent = True
311 314 searching_for_index = True
312 315 parent_depth = depth
313 316
314 317 if not found_parent:
315 318 tree.append((0, reply))
316 319 else:
317 320 if searching_for_index:
318 321 tree.append((parent_depth + 1, reply))
319 322
320 323 offset = 0
321 324 for last_index, parent_depth in indexes_to_insert:
322 325 tree.insert(last_index + offset, (parent_depth + 1, reply))
323 326 offset += 1
324 327
325 328 return tree
@@ -1,31 +1,33 b''
1 1 {% extends "boards/base.html" %}
2 2
3 3 {% load i18n %}
4 4 {% load static %}
5 5
6 6 {% block head %}
7 7 <title>{{ site_name }}</title>
8 8 {% endblock %}
9 9
10 10 {% block content %}
11 11 <div class="post">
12 12 <div class="landing-images">
13 13 {% for image in images %}
14 14 <div class="gallery_image">
15 15 {{ image.get_view|safe }}
16 16 {% with image.get_random_associated_post as post %}
17 17 {{ post.get_link_view|safe }}
18 18 {% endwith %}
19 19 </div>
20 20 {% endfor %}
21 21 </div>
22 <br />
22 23 <div class="landing-tags">
23 24 {{ section_str|safe }}
24 25 </div>
26 <br />
25 27 <div class="landing-threads">
26 28 {% for op in latest_threads %}
27 {{ op.get_link_view|safe }} {{ op.get_title_or_text }}<br />
29 {{ op.get_link_view|safe }} {{ op.get_title_or_text }} ({{ op.thread.get_sections_str|safe }})<br />
28 30 {% endfor %}
29 31 </div>
30 32 </div>
31 33 {% endblock %}
General Comments 0
You need to be logged in to leave comments. Login now