##// END OF EJS Templates
Show all post images in gallery
neko259 -
r1755:5aa523da default
parent child Browse files
Show More
@@ -1,388 +1,389 b''
1 1 import uuid
2 2 import hashlib
3 3 import re
4 4
5 5 from boards import settings
6 6 from boards.abstracts.tripcode import Tripcode
7 7 from boards.models import Attachment, KeyPair, GlobalId
8 8 from boards.models.attachment import FILE_TYPES_IMAGE
9 9 from boards.models.base import Viewable
10 10 from boards.models.post.export import get_exporter, DIFF_TYPE_JSON
11 11 from boards.models.post.manager import PostManager, NO_IP
12 12 from boards.utils import datetime_to_epoch
13 13 from django.core.exceptions import ObjectDoesNotExist
14 14 from django.core.urlresolvers import reverse
15 15 from django.db import models
16 16 from django.db.models import TextField, QuerySet, F
17 17 from django.template.defaultfilters import truncatewords, striptags
18 18 from django.template.loader import render_to_string
19 19
20 20 CSS_CLS_HIDDEN_POST = 'hidden_post'
21 21 CSS_CLS_DEAD_POST = 'dead_post'
22 22 CSS_CLS_ARCHIVE_POST = 'archive_post'
23 23 CSS_CLS_POST = 'post'
24 24 CSS_CLS_MONOCHROME = 'monochrome'
25 25
26 26 TITLE_MAX_WORDS = 10
27 27
28 28 APP_LABEL_BOARDS = 'boards'
29 29
30 30 BAN_REASON_AUTO = 'Auto'
31 31
32 32 TITLE_MAX_LENGTH = 200
33 33
34 34 REGEX_REPLY = re.compile(r'\[post\](\d+)\[/post\]')
35 35 REGEX_GLOBAL_REPLY = re.compile(r'\[post\](\w+)::([^:]+)::(\d+)\[/post\]')
36 36 REGEX_URL = re.compile(r'https?\://[a-zA-Z0-9\-\.]+\.[a-zA-Z]{2,3}(/\S*)?')
37 37 REGEX_NOTIFICATION = re.compile(r'\[user\](\w+)\[/user\]')
38 38
39 39 PARAMETER_TRUNCATED = 'truncated'
40 40 PARAMETER_TAG = 'tag'
41 41 PARAMETER_OFFSET = 'offset'
42 42 PARAMETER_DIFF_TYPE = 'type'
43 43 PARAMETER_CSS_CLASS = 'css_class'
44 44 PARAMETER_THREAD = 'thread'
45 45 PARAMETER_IS_OPENING = 'is_opening'
46 46 PARAMETER_POST = 'post'
47 47 PARAMETER_OP_ID = 'opening_post_id'
48 48 PARAMETER_NEED_OPEN_LINK = 'need_open_link'
49 49 PARAMETER_REPLY_LINK = 'reply_link'
50 50 PARAMETER_NEED_OP_DATA = 'need_op_data'
51 51
52 52 POST_VIEW_PARAMS = (
53 53 'need_op_data',
54 54 'reply_link',
55 55 'need_open_link',
56 56 'truncated',
57 57 'mode_tree',
58 58 'perms',
59 59 'tree_depth',
60 60 )
61 61
62 62
63 63 class Post(models.Model, Viewable):
64 64 """A post is a message."""
65 65
66 66 objects = PostManager()
67 67
68 68 class Meta:
69 69 app_label = APP_LABEL_BOARDS
70 70 ordering = ('id',)
71 71
72 72 title = models.CharField(max_length=TITLE_MAX_LENGTH, blank=True, default='')
73 73 pub_time = models.DateTimeField(db_index=True)
74 74 text = TextField(blank=True, default='')
75 75 _text_rendered = TextField(blank=True, null=True, editable=False)
76 76
77 77 attachments = models.ManyToManyField(Attachment, null=True, blank=True,
78 78 related_name='attachment_posts')
79 79
80 80 poster_ip = models.GenericIPAddressField()
81 81
82 82 # Used for cache and threads updating
83 83 last_edit_time = models.DateTimeField()
84 84
85 85 referenced_posts = models.ManyToManyField('Post', symmetrical=False,
86 86 null=True,
87 87 blank=True, related_name='refposts',
88 88 db_index=True)
89 89 refmap = models.TextField(null=True, blank=True)
90 90 thread = models.ForeignKey('Thread', db_index=True, related_name='replies')
91 91
92 92 url = models.TextField()
93 93 uid = models.TextField(db_index=True)
94 94
95 95 # Global ID with author key. If the message was downloaded from another
96 96 # server, this indicates the server.
97 97 global_id = models.OneToOneField(GlobalId, null=True, blank=True,
98 98 on_delete=models.CASCADE)
99 99
100 100 tripcode = models.CharField(max_length=50, blank=True, default='')
101 101 opening = models.BooleanField(db_index=True)
102 102 hidden = models.BooleanField(default=False)
103 103 version = models.IntegerField(default=1)
104 104
105 105 def __str__(self):
106 106 return 'P#{}/{}'.format(self.id, self.get_title())
107 107
108 108 def get_title(self) -> str:
109 109 return self.title
110 110
111 111 def get_title_or_text(self):
112 112 title = self.get_title()
113 113 if not title:
114 114 title = truncatewords(striptags(self.get_text()), TITLE_MAX_WORDS)
115 115
116 116 return title
117 117
118 118 def build_refmap(self, excluded_ids=None) -> None:
119 119 """
120 120 Builds a replies map string from replies list. This is a cache to stop
121 121 the server from recalculating the map on every post show.
122 122 """
123 123
124 124 replies = self.referenced_posts
125 125 if excluded_ids is not None:
126 126 replies = replies.exclude(id__in=excluded_ids)
127 127 else:
128 128 replies = replies.all()
129 129
130 130 post_urls = [refpost.get_link_view() for refpost in replies]
131 131
132 132 self.refmap = ', '.join(post_urls)
133 133
134 134 def is_referenced(self) -> bool:
135 135 return self.refmap and len(self.refmap) > 0
136 136
137 137 def is_opening(self) -> bool:
138 138 """
139 139 Checks if this is an opening post or just a reply.
140 140 """
141 141
142 142 return self.opening
143 143
144 144 def get_absolute_url(self, thread=None):
145 145 # Url is cached only for the "main" thread. When getting url
146 146 # for other threads, do it manually.
147 147 return self.url
148 148
149 149 def get_thread(self):
150 150 return self.thread
151 151
152 152 def get_thread_id(self):
153 153 return self.thread_id
154 154
155 155 def _get_cache_key(self):
156 156 return [datetime_to_epoch(self.last_edit_time)]
157 157
158 158 def get_view_params(self, *args, **kwargs):
159 159 """
160 160 Gets the parameters required for viewing the post based on the arguments
161 161 given and the post itself.
162 162 """
163 163 thread = kwargs.get('thread') or self.get_thread()
164 164
165 165 css_classes = [CSS_CLS_POST]
166 166 if thread.is_archived():
167 167 css_classes.append(CSS_CLS_ARCHIVE_POST)
168 168 elif not thread.can_bump():
169 169 css_classes.append(CSS_CLS_DEAD_POST)
170 170 if self.is_hidden():
171 171 css_classes.append(CSS_CLS_HIDDEN_POST)
172 172 if thread.is_monochrome():
173 173 css_classes.append(CSS_CLS_MONOCHROME)
174 174
175 175 params = dict()
176 176 for param in POST_VIEW_PARAMS:
177 177 if param in kwargs:
178 178 params[param] = kwargs[param]
179 179
180 180 params.update({
181 181 PARAMETER_POST: self,
182 182 PARAMETER_IS_OPENING: self.is_opening(),
183 183 PARAMETER_THREAD: thread,
184 184 PARAMETER_CSS_CLASS: ' '.join(css_classes),
185 185 })
186 186
187 187 return params
188 188
189 189 def get_view(self, *args, **kwargs) -> str:
190 190 """
191 191 Renders post's HTML view. Some of the post params can be passed over
192 192 kwargs for the means of caching (if we view the thread, some params
193 193 are same for every post and don't need to be computed over and over.
194 194 """
195 195 params = self.get_view_params(*args, **kwargs)
196 196
197 197 return render_to_string('boards/post.html', params)
198 198
199 def get_images(self) -> Attachment:
200 return self.attachments.filter(mimetype__in=FILE_TYPES_IMAGE)
201
199 202 def get_first_image(self) -> Attachment:
200 image = None
201 203 try:
202 image = self.attachments.filter(mimetype__in=FILE_TYPES_IMAGE).earliest('id')
204 return self.get_images().earliest('-id')
203 205 except Attachment.DoesNotExist:
204 pass
205 return image
206 return None
206 207
207 208 def set_global_id(self, key_pair=None):
208 209 """
209 210 Sets global id based on the given key pair. If no key pair is given,
210 211 default one is used.
211 212 """
212 213
213 214 if key_pair:
214 215 key = key_pair
215 216 else:
216 217 try:
217 218 key = KeyPair.objects.get(primary=True)
218 219 except KeyPair.DoesNotExist:
219 220 # Do not update the global id because there is no key defined
220 221 return
221 222 global_id = GlobalId(key_type=key.key_type,
222 223 key=key.public_key,
223 224 local_id=self.id)
224 225 global_id.save()
225 226
226 227 self.global_id = global_id
227 228
228 229 self.save(update_fields=['global_id'])
229 230
230 231 def get_pub_time_str(self):
231 232 return str(self.pub_time)
232 233
233 234 def get_replied_ids(self):
234 235 """
235 236 Gets ID list of the posts that this post replies.
236 237 """
237 238
238 239 raw_text = self.get_raw_text()
239 240
240 241 local_replied = REGEX_REPLY.findall(raw_text)
241 242 global_replied = []
242 243 for match in REGEX_GLOBAL_REPLY.findall(raw_text):
243 244 key_type = match[0]
244 245 key = match[1]
245 246 local_id = match[2]
246 247
247 248 try:
248 249 global_id = GlobalId.objects.get(key_type=key_type,
249 250 key=key, local_id=local_id)
250 251 for post in Post.objects.filter(global_id=global_id).only('id'):
251 252 global_replied.append(post.id)
252 253 except GlobalId.DoesNotExist:
253 254 pass
254 255 return local_replied + global_replied
255 256
256 257 def get_post_data(self, format_type=DIFF_TYPE_JSON, request=None,
257 258 include_last_update=False) -> str:
258 259 """
259 260 Gets post HTML or JSON data that can be rendered on a page or used by
260 261 API.
261 262 """
262 263
263 264 return get_exporter(format_type).export(self, request,
264 265 include_last_update)
265 266
266 267 def notify_clients(self, recursive=True):
267 268 """
268 269 Sends post HTML data to the thread web socket.
269 270 """
270 271
271 272 if not settings.get_bool('External', 'WebsocketsEnabled'):
272 273 return
273 274
274 275 thread_ids = list()
275 276 self.get_thread().notify_clients()
276 277
277 278 if recursive:
278 279 for reply_number in re.finditer(REGEX_REPLY, self.get_raw_text()):
279 280 post_id = reply_number.group(1)
280 281
281 282 try:
282 283 ref_post = Post.objects.get(id=post_id)
283 284
284 285 if ref_post.get_thread().id not in thread_ids:
285 286 # If post is in this thread, its thread was already notified.
286 287 # Otherwise, notify its thread separately.
287 288 ref_post.notify_clients(recursive=False)
288 289 except ObjectDoesNotExist:
289 290 pass
290 291
291 292 def _build_url(self):
292 293 opening = self.is_opening()
293 294 opening_id = self.id if opening else self.get_thread().get_opening_post_id()
294 295 url = reverse('thread', kwargs={'post_id': opening_id})
295 296 if not opening:
296 297 url += '#' + str(self.id)
297 298
298 299 return url
299 300
300 301 def save(self, force_insert=False, force_update=False, using=None,
301 302 update_fields=None):
302 303 new_post = self.id is None
303 304
304 305 self.uid = str(uuid.uuid4())
305 306 if update_fields is not None and 'uid' not in update_fields:
306 307 update_fields += ['uid']
307 308
308 309 if not new_post:
309 310 thread = self.get_thread()
310 311 if thread:
311 312 thread.last_edit_time = self.last_edit_time
312 313 thread.save(update_fields=['last_edit_time', 'status'])
313 314
314 315 super().save(force_insert, force_update, using, update_fields)
315 316
316 317 if new_post:
317 318 self.url = self._build_url()
318 319 super().save(update_fields=['url'])
319 320
320 321 def get_text(self) -> str:
321 322 return self._text_rendered
322 323
323 324 def get_raw_text(self) -> str:
324 325 return self.text
325 326
326 327 def get_sync_text(self) -> str:
327 328 """
328 329 Returns text applicable for sync. It has absolute post reflinks.
329 330 """
330 331
331 332 replacements = dict()
332 333 for post_id in REGEX_REPLY.findall(self.get_raw_text()):
333 334 try:
334 335 absolute_post_id = str(Post.objects.get(id=post_id).global_id)
335 336 replacements[post_id] = absolute_post_id
336 337 except Post.DoesNotExist:
337 338 pass
338 339
339 340 text = self.get_raw_text() or ''
340 341 for key in replacements:
341 342 text = text.replace('[post]{}[/post]'.format(key),
342 343 '[post]{}[/post]'.format(replacements[key]))
343 344 text = text.replace('\r\n', '\n').replace('\r', '\n')
344 345
345 346 return text
346 347
347 348 def get_tripcode(self):
348 349 if self.tripcode:
349 350 return Tripcode(self.tripcode)
350 351
351 352 def get_link_view(self):
352 353 """
353 354 Gets view of a reflink to the post.
354 355 """
355 356 result = '<a href="{}">&gt;&gt;{}</a>'.format(self.get_absolute_url(),
356 357 self.id)
357 358 if self.is_opening():
358 359 result = '<b>{}</b>'.format(result)
359 360
360 361 return result
361 362
362 363 def is_hidden(self) -> bool:
363 364 return self.hidden
364 365
365 366 def set_hidden(self, hidden):
366 367 self.hidden = hidden
367 368
368 369 def increment_version(self):
369 370 self.version = F('version') + 1
370 371
371 372 def clear_cache(self):
372 373 """
373 374 Clears sync data (content cache, signatures etc).
374 375 """
375 376 global_id = self.global_id
376 377 if global_id is not None and global_id.is_local()\
377 378 and global_id.content is not None:
378 379 global_id.clear_cache()
379 380
380 381 def get_tags(self):
381 382 return self.get_thread().get_tags()
382 383
383 384 def get_ip_color(self):
384 385 return hashlib.md5(self.poster_ip.encode()).hexdigest()[:6]
385 386
386 387 def has_ip(self):
387 388 return self.poster_ip != NO_IP
388 389
@@ -1,39 +1,39 b''
1 1 {% extends "boards/thread.html" %}
2 2
3 3 {% load i18n %}
4 4 {% load static from staticfiles %}
5 5 {% load board %}
6 6 {% load tz %}
7 7
8 8 {% block head %}
9 9 <meta name="robots" content="noindex">
10 10 <title>{{ thread.get_opening_post.get_title|striptags|truncatewords:10 }}
11 11 - {{ site_name }}</title>
12 12 {% endblock %}
13 13
14 14 {% block thread_content %}
15 15 {% get_current_language as LANGUAGE_CODE %}
16 16 {% get_current_timezone as TIME_ZONE %}
17 17
18 18 <div id="posts-table">
19 19 {% if posts %}
20 20 {% for post in posts %}
21 <div class="gallery_image">
22 {% with post.get_first_image as image %}
23 {% autoescape off %}
24 {{ image.get_view }}
25 <div class="gallery_image_metadata">
26 {{ image.get_size.0 }}x{{ image.get_size.1 }}
27 {% image_actions image.file.url request.get_host %}
28 <br />
29 <a href="{{ post.get_absolute_url }}">>>{{ post.id }}</a>
30 </div>
31 {% endautoescape %}
32 {% endwith %}
33 </div>
21 {% for image in post.get_images %}
22 <div class="gallery_image">
23 {% autoescape off %}
24 {{ image.get_view }}
25 <div class="gallery_image_metadata">
26 {{ image.get_size.0 }}x{{ image.get_size.1 }}
27 {% image_actions image.file.url request.get_host %}
28 <br />
29 <a href="{{ post.get_absolute_url }}">>>{{ post.id }}</a>
30 </div>
31 {% endautoescape %}
32 </div>
33 {% endfor %}
34 34 {% endfor %}
35 35 {% else %}
36 36 {% trans 'No images.' %}
37 37 {% endif %}
38 38 </div>
39 39 {% endblock %}
General Comments 0
You need to be logged in to leave comments. Login now