##// END OF EJS Templates
Use native django permission system in templates
neko259 -
r1388:674f43a1 default
parent child Browse files
Show More
@@ -1,68 +1,64 b''
1 from boards.abstracts.settingsmanager import get_settings_manager, \
1 from boards.abstracts.settingsmanager import get_settings_manager, \
2 SETTING_USERNAME, SETTING_LAST_NOTIFICATION_ID, SETTING_IMAGE_VIEWER
2 SETTING_USERNAME, SETTING_LAST_NOTIFICATION_ID, SETTING_IMAGE_VIEWER
3 from boards.models.user import Notification
3 from boards.models.user import Notification
4
4
5 __author__ = 'neko259'
5 __author__ = 'neko259'
6
6
7 from boards import settings, utils
7 from boards import settings, utils
8 from boards.models import Post, Tag
8 from boards.models import Post, Tag
9
9
10 CONTEXT_SITE_NAME = 'site_name'
10 CONTEXT_SITE_NAME = 'site_name'
11 CONTEXT_VERSION = 'version'
11 CONTEXT_VERSION = 'version'
12 CONTEXT_MODERATOR = 'moderator'
13 CONTEXT_THEME_CSS = 'theme_css'
12 CONTEXT_THEME_CSS = 'theme_css'
14 CONTEXT_THEME = 'theme'
13 CONTEXT_THEME = 'theme'
15 CONTEXT_PPD = 'posts_per_day'
14 CONTEXT_PPD = 'posts_per_day'
16 CONTEXT_TAGS = 'tags'
15 CONTEXT_TAGS = 'tags'
17 CONTEXT_USER = 'user'
16 CONTEXT_USER = 'user'
18 CONTEXT_NEW_NOTIFICATIONS_COUNT = 'new_notifications_count'
17 CONTEXT_NEW_NOTIFICATIONS_COUNT = 'new_notifications_count'
19 CONTEXT_USERNAME = 'username'
18 CONTEXT_USERNAME = 'username'
20 CONTEXT_TAGS_STR = 'tags_str'
19 CONTEXT_TAGS_STR = 'tags_str'
21 CONTEXT_IMAGE_VIEWER = 'image_viewer'
20 CONTEXT_IMAGE_VIEWER = 'image_viewer'
22 CONTEXT_HAS_FAV_THREADS = 'has_fav_threads'
21 CONTEXT_HAS_FAV_THREADS = 'has_fav_threads'
23
22
24
23
25 def get_notifications(context, request):
24 def get_notifications(context, request):
26 settings_manager = get_settings_manager(request)
25 settings_manager = get_settings_manager(request)
27 username = settings_manager.get_setting(SETTING_USERNAME)
26 username = settings_manager.get_setting(SETTING_USERNAME)
28 new_notifications_count = 0
27 new_notifications_count = 0
29 if username is not None and len(username) > 0:
28 if username is not None and len(username) > 0:
30 last_notification_id = settings_manager.get_setting(
29 last_notification_id = settings_manager.get_setting(
31 SETTING_LAST_NOTIFICATION_ID)
30 SETTING_LAST_NOTIFICATION_ID)
32
31
33 new_notifications_count = Notification.objects.get_notification_posts(
32 new_notifications_count = Notification.objects.get_notification_posts(
34 username=username, last=last_notification_id).count()
33 username=username, last=last_notification_id).count()
35 context[CONTEXT_NEW_NOTIFICATIONS_COUNT] = new_notifications_count
34 context[CONTEXT_NEW_NOTIFICATIONS_COUNT] = new_notifications_count
36 context[CONTEXT_USERNAME] = username
35 context[CONTEXT_USERNAME] = username
37
36
38
37
39 def user_and_ui_processor(request):
38 def user_and_ui_processor(request):
40 context = dict()
39 context = dict()
41
40
42 context[CONTEXT_PPD] = float(Post.objects.get_posts_per_day())
41 context[CONTEXT_PPD] = float(Post.objects.get_posts_per_day())
43
42
44 settings_manager = get_settings_manager(request)
43 settings_manager = get_settings_manager(request)
45 fav_tags = settings_manager.get_fav_tags()
44 fav_tags = settings_manager.get_fav_tags()
46 context[CONTEXT_TAGS] = fav_tags
45 context[CONTEXT_TAGS] = fav_tags
47
46
48 context[CONTEXT_TAGS_STR] = Tag.objects.get_tag_url_list(fav_tags)
47 context[CONTEXT_TAGS_STR] = Tag.objects.get_tag_url_list(fav_tags)
49 theme = settings_manager.get_theme()
48 theme = settings_manager.get_theme()
50 context[CONTEXT_THEME] = theme
49 context[CONTEXT_THEME] = theme
51 context[CONTEXT_THEME_CSS] = 'css/' + theme + '/base_page.css'
50 context[CONTEXT_THEME_CSS] = 'css/' + theme + '/base_page.css'
52
51
53 # This shows the moderator panel
54 context[CONTEXT_MODERATOR] = utils.is_moderator(request)
55
56 context[CONTEXT_VERSION] = settings.get('Version', 'Version')
52 context[CONTEXT_VERSION] = settings.get('Version', 'Version')
57 context[CONTEXT_SITE_NAME] = settings.get('Version', 'SiteName')
53 context[CONTEXT_SITE_NAME] = settings.get('Version', 'SiteName')
58
54
59 context[CONTEXT_IMAGE_VIEWER] = settings_manager.get_setting(
55 context[CONTEXT_IMAGE_VIEWER] = settings_manager.get_setting(
60 SETTING_IMAGE_VIEWER,
56 SETTING_IMAGE_VIEWER,
61 default=settings.get('View', 'DefaultImageViewer'))
57 default=settings.get('View', 'DefaultImageViewer'))
62
58
63 context[CONTEXT_HAS_FAV_THREADS] =\
59 context[CONTEXT_HAS_FAV_THREADS] =\
64 len(settings_manager.get_fav_threads()) > 0
60 len(settings_manager.get_fav_threads()) > 0
65
61
66 get_notifications(context, request)
62 get_notifications(context, request)
67
63
68 return context
64 return context
@@ -1,361 +1,361 b''
1 import logging
1 import logging
2 import re
2 import re
3 import uuid
3 import uuid
4
4
5 from django.core.exceptions import ObjectDoesNotExist
5 from django.core.exceptions import ObjectDoesNotExist
6 from django.core.urlresolvers import reverse
6 from django.core.urlresolvers import reverse
7 from django.db import models
7 from django.db import models
8 from django.db.models import TextField, QuerySet
8 from django.db.models import TextField, QuerySet
9 from django.template.defaultfilters import striptags, truncatewords
9 from django.template.defaultfilters import striptags, truncatewords
10 from django.template.loader import render_to_string
10 from django.template.loader import render_to_string
11 from django.utils import timezone
11 from django.utils import timezone
12
12
13 from boards import settings
13 from boards import settings
14 from boards.abstracts.tripcode import Tripcode
14 from boards.abstracts.tripcode import Tripcode
15 from boards.mdx_neboard import Parser
15 from boards.mdx_neboard import Parser
16 from boards.models import PostImage, Attachment
16 from boards.models import PostImage, Attachment
17 from boards.models.base import Viewable
17 from boards.models.base import Viewable
18 from boards.models.post.export import get_exporter, DIFF_TYPE_JSON
18 from boards.models.post.export import get_exporter, DIFF_TYPE_JSON
19 from boards.models.post.manager import PostManager
19 from boards.models.post.manager import PostManager
20 from boards.models.user import Notification
20 from boards.models.user import Notification
21
21
22 CSS_CLS_HIDDEN_POST = 'hidden_post'
22 CSS_CLS_HIDDEN_POST = 'hidden_post'
23 CSS_CLS_DEAD_POST = 'dead_post'
23 CSS_CLS_DEAD_POST = 'dead_post'
24 CSS_CLS_ARCHIVE_POST = 'archive_post'
24 CSS_CLS_ARCHIVE_POST = 'archive_post'
25 CSS_CLS_POST = 'post'
25 CSS_CLS_POST = 'post'
26
26
27 TITLE_MAX_WORDS = 10
27 TITLE_MAX_WORDS = 10
28
28
29 APP_LABEL_BOARDS = 'boards'
29 APP_LABEL_BOARDS = 'boards'
30
30
31 BAN_REASON_AUTO = 'Auto'
31 BAN_REASON_AUTO = 'Auto'
32
32
33 IMAGE_THUMB_SIZE = (200, 150)
33 IMAGE_THUMB_SIZE = (200, 150)
34
34
35 TITLE_MAX_LENGTH = 200
35 TITLE_MAX_LENGTH = 200
36
36
37 REGEX_REPLY = re.compile(r'\[post\](\d+)\[/post\]')
37 REGEX_REPLY = re.compile(r'\[post\](\d+)\[/post\]')
38 REGEX_NOTIFICATION = re.compile(r'\[user\](\w+)\[/user\]')
38 REGEX_NOTIFICATION = re.compile(r'\[user\](\w+)\[/user\]')
39
39
40 PARAMETER_TRUNCATED = 'truncated'
40 PARAMETER_TRUNCATED = 'truncated'
41 PARAMETER_TAG = 'tag'
41 PARAMETER_TAG = 'tag'
42 PARAMETER_OFFSET = 'offset'
42 PARAMETER_OFFSET = 'offset'
43 PARAMETER_DIFF_TYPE = 'type'
43 PARAMETER_DIFF_TYPE = 'type'
44 PARAMETER_CSS_CLASS = 'css_class'
44 PARAMETER_CSS_CLASS = 'css_class'
45 PARAMETER_THREAD = 'thread'
45 PARAMETER_THREAD = 'thread'
46 PARAMETER_IS_OPENING = 'is_opening'
46 PARAMETER_IS_OPENING = 'is_opening'
47 PARAMETER_MODERATOR = 'moderator'
47 PARAMETER_MODERATOR = 'moderator'
48 PARAMETER_POST = 'post'
48 PARAMETER_POST = 'post'
49 PARAMETER_OP_ID = 'opening_post_id'
49 PARAMETER_OP_ID = 'opening_post_id'
50 PARAMETER_NEED_OPEN_LINK = 'need_open_link'
50 PARAMETER_NEED_OPEN_LINK = 'need_open_link'
51 PARAMETER_REPLY_LINK = 'reply_link'
51 PARAMETER_REPLY_LINK = 'reply_link'
52 PARAMETER_NEED_OP_DATA = 'need_op_data'
52 PARAMETER_NEED_OP_DATA = 'need_op_data'
53
53
54 POST_VIEW_PARAMS = (
54 POST_VIEW_PARAMS = (
55 'need_op_data',
55 'need_op_data',
56 'reply_link',
56 'reply_link',
57 'moderator',
58 'need_open_link',
57 'need_open_link',
59 'truncated',
58 'truncated',
60 'mode_tree',
59 'mode_tree',
60 'perms',
61 )
61 )
62
62
63
63
64 class Post(models.Model, Viewable):
64 class Post(models.Model, Viewable):
65 """A post is a message."""
65 """A post is a message."""
66
66
67 objects = PostManager()
67 objects = PostManager()
68
68
69 class Meta:
69 class Meta:
70 app_label = APP_LABEL_BOARDS
70 app_label = APP_LABEL_BOARDS
71 ordering = ('id',)
71 ordering = ('id',)
72
72
73 title = models.CharField(max_length=TITLE_MAX_LENGTH, null=True, blank=True)
73 title = models.CharField(max_length=TITLE_MAX_LENGTH, null=True, blank=True)
74 pub_time = models.DateTimeField()
74 pub_time = models.DateTimeField()
75 text = TextField(blank=True, null=True)
75 text = TextField(blank=True, null=True)
76 _text_rendered = TextField(blank=True, null=True, editable=False)
76 _text_rendered = TextField(blank=True, null=True, editable=False)
77
77
78 images = models.ManyToManyField(PostImage, null=True, blank=True,
78 images = models.ManyToManyField(PostImage, null=True, blank=True,
79 related_name='post_images', db_index=True)
79 related_name='post_images', db_index=True)
80 attachments = models.ManyToManyField(Attachment, null=True, blank=True,
80 attachments = models.ManyToManyField(Attachment, null=True, blank=True,
81 related_name='attachment_posts')
81 related_name='attachment_posts')
82
82
83 poster_ip = models.GenericIPAddressField()
83 poster_ip = models.GenericIPAddressField()
84
84
85 # TODO This field can be removed cause UID is used for update now
85 # TODO This field can be removed cause UID is used for update now
86 last_edit_time = models.DateTimeField()
86 last_edit_time = models.DateTimeField()
87
87
88 referenced_posts = models.ManyToManyField('Post', symmetrical=False,
88 referenced_posts = models.ManyToManyField('Post', symmetrical=False,
89 null=True,
89 null=True,
90 blank=True, related_name='refposts',
90 blank=True, related_name='refposts',
91 db_index=True)
91 db_index=True)
92 refmap = models.TextField(null=True, blank=True)
92 refmap = models.TextField(null=True, blank=True)
93 threads = models.ManyToManyField('Thread', db_index=True,
93 threads = models.ManyToManyField('Thread', db_index=True,
94 related_name='multi_replies')
94 related_name='multi_replies')
95 thread = models.ForeignKey('Thread', db_index=True, related_name='pt+')
95 thread = models.ForeignKey('Thread', db_index=True, related_name='pt+')
96
96
97 url = models.TextField()
97 url = models.TextField()
98 uid = models.TextField(db_index=True)
98 uid = models.TextField(db_index=True)
99
99
100 tripcode = models.CharField(max_length=50, blank=True, default='')
100 tripcode = models.CharField(max_length=50, blank=True, default='')
101 opening = models.BooleanField(db_index=True)
101 opening = models.BooleanField(db_index=True)
102 hidden = models.BooleanField(default=False)
102 hidden = models.BooleanField(default=False)
103
103
104 def __str__(self):
104 def __str__(self):
105 return 'P#{}/{}'.format(self.id, self.get_title())
105 return 'P#{}/{}'.format(self.id, self.get_title())
106
106
107 def get_referenced_posts(self):
107 def get_referenced_posts(self):
108 threads = self.get_threads().all()
108 threads = self.get_threads().all()
109 return self.referenced_posts.filter(threads__in=threads)\
109 return self.referenced_posts.filter(threads__in=threads)\
110 .order_by('pub_time').distinct().all()
110 .order_by('pub_time').distinct().all()
111
111
112 def get_title(self) -> str:
112 def get_title(self) -> str:
113 return self.title
113 return self.title
114
114
115 def get_title_or_text(self):
115 def get_title_or_text(self):
116 title = self.get_title()
116 title = self.get_title()
117 if not title:
117 if not title:
118 title = truncatewords(striptags(self.get_text()), TITLE_MAX_WORDS)
118 title = truncatewords(striptags(self.get_text()), TITLE_MAX_WORDS)
119
119
120 return title
120 return title
121
121
122 def build_refmap(self) -> None:
122 def build_refmap(self) -> None:
123 """
123 """
124 Builds a replies map string from replies list. This is a cache to stop
124 Builds a replies map string from replies list. This is a cache to stop
125 the server from recalculating the map on every post show.
125 the server from recalculating the map on every post show.
126 """
126 """
127
127
128 post_urls = [refpost.get_link_view()
128 post_urls = [refpost.get_link_view()
129 for refpost in self.referenced_posts.all()]
129 for refpost in self.referenced_posts.all()]
130
130
131 self.refmap = ', '.join(post_urls)
131 self.refmap = ', '.join(post_urls)
132
132
133 def is_referenced(self) -> bool:
133 def is_referenced(self) -> bool:
134 return self.refmap and len(self.refmap) > 0
134 return self.refmap and len(self.refmap) > 0
135
135
136 def is_opening(self) -> bool:
136 def is_opening(self) -> bool:
137 """
137 """
138 Checks if this is an opening post or just a reply.
138 Checks if this is an opening post or just a reply.
139 """
139 """
140
140
141 return self.opening
141 return self.opening
142
142
143 def get_absolute_url(self, thread=None):
143 def get_absolute_url(self, thread=None):
144 url = None
144 url = None
145
145
146 if thread is None:
146 if thread is None:
147 thread = self.get_thread()
147 thread = self.get_thread()
148
148
149 # Url is cached only for the "main" thread. When getting url
149 # Url is cached only for the "main" thread. When getting url
150 # for other threads, do it manually.
150 # for other threads, do it manually.
151 if self.url:
151 if self.url:
152 url = self.url
152 url = self.url
153
153
154 if url is None:
154 if url is None:
155 opening_id = thread.get_opening_post_id()
155 opening_id = thread.get_opening_post_id()
156 url = reverse('thread', kwargs={'post_id': opening_id})
156 url = reverse('thread', kwargs={'post_id': opening_id})
157 if self.id != opening_id:
157 if self.id != opening_id:
158 url += '#' + str(self.id)
158 url += '#' + str(self.id)
159
159
160 return url
160 return url
161
161
162 def get_thread(self):
162 def get_thread(self):
163 return self.thread
163 return self.thread
164
164
165 def get_threads(self) -> QuerySet:
165 def get_threads(self) -> QuerySet:
166 """
166 """
167 Gets post's thread.
167 Gets post's thread.
168 """
168 """
169
169
170 return self.threads
170 return self.threads
171
171
172 def get_view(self, *args, **kwargs) -> str:
172 def get_view(self, *args, **kwargs) -> str:
173 """
173 """
174 Renders post's HTML view. Some of the post params can be passed over
174 Renders post's HTML view. Some of the post params can be passed over
175 kwargs for the means of caching (if we view the thread, some params
175 kwargs for the means of caching (if we view the thread, some params
176 are same for every post and don't need to be computed over and over.
176 are same for every post and don't need to be computed over and over.
177 """
177 """
178
178
179 thread = self.get_thread()
179 thread = self.get_thread()
180
180
181 css_classes = [CSS_CLS_POST]
181 css_classes = [CSS_CLS_POST]
182 if thread.archived:
182 if thread.archived:
183 css_classes.append(CSS_CLS_ARCHIVE_POST)
183 css_classes.append(CSS_CLS_ARCHIVE_POST)
184 elif not thread.can_bump():
184 elif not thread.can_bump():
185 css_classes.append(CSS_CLS_DEAD_POST)
185 css_classes.append(CSS_CLS_DEAD_POST)
186 if self.is_hidden():
186 if self.is_hidden():
187 css_classes.append(CSS_CLS_HIDDEN_POST)
187 css_classes.append(CSS_CLS_HIDDEN_POST)
188
188
189 params = dict()
189 params = dict()
190 for param in POST_VIEW_PARAMS:
190 for param in POST_VIEW_PARAMS:
191 if param in kwargs:
191 if param in kwargs:
192 params[param] = kwargs[param]
192 params[param] = kwargs[param]
193
193
194 params.update({
194 params.update({
195 PARAMETER_POST: self,
195 PARAMETER_POST: self,
196 PARAMETER_IS_OPENING: self.is_opening(),
196 PARAMETER_IS_OPENING: self.is_opening(),
197 PARAMETER_THREAD: thread,
197 PARAMETER_THREAD: thread,
198 PARAMETER_CSS_CLASS: ' '.join(css_classes),
198 PARAMETER_CSS_CLASS: ' '.join(css_classes),
199 })
199 })
200
200
201 return render_to_string('boards/post.html', params)
201 return render_to_string('boards/post.html', params)
202
202
203 def get_search_view(self, *args, **kwargs):
203 def get_search_view(self, *args, **kwargs):
204 return self.get_view(need_op_data=True, *args, **kwargs)
204 return self.get_view(need_op_data=True, *args, **kwargs)
205
205
206 def get_first_image(self) -> PostImage:
206 def get_first_image(self) -> PostImage:
207 return self.images.earliest('id')
207 return self.images.earliest('id')
208
208
209 def delete(self, using=None):
209 def delete(self, using=None):
210 """
210 """
211 Deletes all post images and the post itself.
211 Deletes all post images and the post itself.
212 """
212 """
213
213
214 for image in self.images.all():
214 for image in self.images.all():
215 image_refs_count = image.post_images.count()
215 image_refs_count = image.post_images.count()
216 if image_refs_count == 1:
216 if image_refs_count == 1:
217 image.delete()
217 image.delete()
218
218
219 for attachment in self.attachments.all():
219 for attachment in self.attachments.all():
220 attachment_refs_count = attachment.attachment_posts.count()
220 attachment_refs_count = attachment.attachment_posts.count()
221 if attachment_refs_count == 1:
221 if attachment_refs_count == 1:
222 attachment.delete()
222 attachment.delete()
223
223
224 thread = self.get_thread()
224 thread = self.get_thread()
225 thread.last_edit_time = timezone.now()
225 thread.last_edit_time = timezone.now()
226 thread.save()
226 thread.save()
227
227
228 super(Post, self).delete(using)
228 super(Post, self).delete(using)
229
229
230 logging.getLogger('boards.post.delete').info(
230 logging.getLogger('boards.post.delete').info(
231 'Deleted post {}'.format(self))
231 'Deleted post {}'.format(self))
232
232
233 def get_post_data(self, format_type=DIFF_TYPE_JSON, request=None,
233 def get_post_data(self, format_type=DIFF_TYPE_JSON, request=None,
234 include_last_update=False) -> str:
234 include_last_update=False) -> str:
235 """
235 """
236 Gets post HTML or JSON data that can be rendered on a page or used by
236 Gets post HTML or JSON data that can be rendered on a page or used by
237 API.
237 API.
238 """
238 """
239
239
240 return get_exporter(format_type).export(self, request,
240 return get_exporter(format_type).export(self, request,
241 include_last_update)
241 include_last_update)
242
242
243 def notify_clients(self, recursive=True):
243 def notify_clients(self, recursive=True):
244 """
244 """
245 Sends post HTML data to the thread web socket.
245 Sends post HTML data to the thread web socket.
246 """
246 """
247
247
248 if not settings.get_bool('External', 'WebsocketsEnabled'):
248 if not settings.get_bool('External', 'WebsocketsEnabled'):
249 return
249 return
250
250
251 thread_ids = list()
251 thread_ids = list()
252 for thread in self.get_threads().all():
252 for thread in self.get_threads().all():
253 thread_ids.append(thread.id)
253 thread_ids.append(thread.id)
254
254
255 thread.notify_clients()
255 thread.notify_clients()
256
256
257 if recursive:
257 if recursive:
258 for reply_number in re.finditer(REGEX_REPLY, self.get_raw_text()):
258 for reply_number in re.finditer(REGEX_REPLY, self.get_raw_text()):
259 post_id = reply_number.group(1)
259 post_id = reply_number.group(1)
260
260
261 try:
261 try:
262 ref_post = Post.objects.get(id=post_id)
262 ref_post = Post.objects.get(id=post_id)
263
263
264 if ref_post.get_threads().exclude(id__in=thread_ids).exists():
264 if ref_post.get_threads().exclude(id__in=thread_ids).exists():
265 # If post is in this thread, its thread was already notified.
265 # If post is in this thread, its thread was already notified.
266 # Otherwise, notify its thread separately.
266 # Otherwise, notify its thread separately.
267 ref_post.notify_clients(recursive=False)
267 ref_post.notify_clients(recursive=False)
268 except ObjectDoesNotExist:
268 except ObjectDoesNotExist:
269 pass
269 pass
270
270
271 def build_url(self):
271 def build_url(self):
272 self.url = self.get_absolute_url()
272 self.url = self.get_absolute_url()
273 self.save(update_fields=['url'])
273 self.save(update_fields=['url'])
274
274
275 def save(self, force_insert=False, force_update=False, using=None,
275 def save(self, force_insert=False, force_update=False, using=None,
276 update_fields=None):
276 update_fields=None):
277 self._text_rendered = Parser().parse(self.get_raw_text())
277 self._text_rendered = Parser().parse(self.get_raw_text())
278
278
279 self.uid = str(uuid.uuid4())
279 self.uid = str(uuid.uuid4())
280 if update_fields is not None and 'uid' not in update_fields:
280 if update_fields is not None and 'uid' not in update_fields:
281 update_fields += ['uid']
281 update_fields += ['uid']
282
282
283 if self.id:
283 if self.id:
284 for thread in self.get_threads().all():
284 for thread in self.get_threads().all():
285 thread.last_edit_time = self.last_edit_time
285 thread.last_edit_time = self.last_edit_time
286
286
287 thread.save(update_fields=['last_edit_time', 'bumpable'])
287 thread.save(update_fields=['last_edit_time', 'bumpable'])
288
288
289 super().save(force_insert, force_update, using, update_fields)
289 super().save(force_insert, force_update, using, update_fields)
290
290
291 def get_text(self) -> str:
291 def get_text(self) -> str:
292 return self._text_rendered
292 return self._text_rendered
293
293
294 def get_raw_text(self) -> str:
294 def get_raw_text(self) -> str:
295 return self.text
295 return self.text
296
296
297 def get_absolute_id(self) -> str:
297 def get_absolute_id(self) -> str:
298 """
298 """
299 If the post has many threads, shows its main thread OP id in the post
299 If the post has many threads, shows its main thread OP id in the post
300 ID.
300 ID.
301 """
301 """
302
302
303 if self.get_threads().count() > 1:
303 if self.get_threads().count() > 1:
304 return '{}/{}'.format(self.get_thread().get_opening_post_id(), self.id)
304 return '{}/{}'.format(self.get_thread().get_opening_post_id(), self.id)
305 else:
305 else:
306 return str(self.id)
306 return str(self.id)
307
307
308 def connect_notifications(self):
308 def connect_notifications(self):
309 for reply_number in re.finditer(REGEX_NOTIFICATION, self.get_raw_text()):
309 for reply_number in re.finditer(REGEX_NOTIFICATION, self.get_raw_text()):
310 user_name = reply_number.group(1).lower()
310 user_name = reply_number.group(1).lower()
311 Notification.objects.get_or_create(name=user_name, post=self)
311 Notification.objects.get_or_create(name=user_name, post=self)
312
312
313 def connect_replies(self):
313 def connect_replies(self):
314 """
314 """
315 Connects replies to a post to show them as a reflink map
315 Connects replies to a post to show them as a reflink map
316 """
316 """
317
317
318 for reply_number in re.finditer(REGEX_REPLY, self.get_raw_text()):
318 for reply_number in re.finditer(REGEX_REPLY, self.get_raw_text()):
319 post_id = reply_number.group(1)
319 post_id = reply_number.group(1)
320
320
321 try:
321 try:
322 referenced_post = Post.objects.get(id=post_id)
322 referenced_post = Post.objects.get(id=post_id)
323
323
324 referenced_post.referenced_posts.add(self)
324 referenced_post.referenced_posts.add(self)
325 referenced_post.last_edit_time = self.pub_time
325 referenced_post.last_edit_time = self.pub_time
326 referenced_post.build_refmap()
326 referenced_post.build_refmap()
327 referenced_post.save(update_fields=['refmap', 'last_edit_time'])
327 referenced_post.save(update_fields=['refmap', 'last_edit_time'])
328 except ObjectDoesNotExist:
328 except ObjectDoesNotExist:
329 pass
329 pass
330
330
331 def connect_threads(self, opening_posts):
331 def connect_threads(self, opening_posts):
332 for opening_post in opening_posts:
332 for opening_post in opening_posts:
333 threads = opening_post.get_threads().all()
333 threads = opening_post.get_threads().all()
334 for thread in threads:
334 for thread in threads:
335 if thread.can_bump():
335 if thread.can_bump():
336 thread.update_bump_status()
336 thread.update_bump_status()
337
337
338 thread.last_edit_time = self.last_edit_time
338 thread.last_edit_time = self.last_edit_time
339 thread.save(update_fields=['last_edit_time', 'bumpable'])
339 thread.save(update_fields=['last_edit_time', 'bumpable'])
340 self.threads.add(opening_post.get_thread())
340 self.threads.add(opening_post.get_thread())
341
341
342 def get_tripcode(self):
342 def get_tripcode(self):
343 if self.tripcode:
343 if self.tripcode:
344 return Tripcode(self.tripcode)
344 return Tripcode(self.tripcode)
345
345
346 def get_link_view(self):
346 def get_link_view(self):
347 """
347 """
348 Gets view of a reflink to the post.
348 Gets view of a reflink to the post.
349 """
349 """
350 result = '<a href="{}">&gt;&gt;{}</a>'.format(self.get_absolute_url(),
350 result = '<a href="{}">&gt;&gt;{}</a>'.format(self.get_absolute_url(),
351 self.id)
351 self.id)
352 if self.is_opening():
352 if self.is_opening():
353 result = '<b>{}</b>'.format(result)
353 result = '<b>{}</b>'.format(result)
354
354
355 return result
355 return result
356
356
357 def is_hidden(self) -> bool:
357 def is_hidden(self) -> bool:
358 return self.hidden
358 return self.hidden
359
359
360 def set_hidden(self, hidden):
360 def set_hidden(self, hidden):
361 self.hidden = hidden
361 self.hidden = hidden
@@ -1,186 +1,186 b''
1 {% extends "boards/base.html" %}
1 {% extends "boards/base.html" %}
2
2
3 {% load i18n %}
3 {% load i18n %}
4 {% load board %}
4 {% load board %}
5 {% load static %}
5 {% load static %}
6 {% load tz %}
6 {% load tz %}
7
7
8 {% block head %}
8 {% block head %}
9 <meta name="robots" content="noindex">
9 <meta name="robots" content="noindex">
10
10
11 {% if tag %}
11 {% if tag %}
12 <title>{{ tag.name }} - {{ site_name }}</title>
12 <title>{{ tag.name }} - {{ site_name }}</title>
13 {% else %}
13 {% else %}
14 <title>{{ site_name }}</title>
14 <title>{{ site_name }}</title>
15 {% endif %}
15 {% endif %}
16
16
17 {% if prev_page_link %}
17 {% if prev_page_link %}
18 <link rel="prev" href="{{ prev_page_link }}" />
18 <link rel="prev" href="{{ prev_page_link }}" />
19 {% endif %}
19 {% endif %}
20 {% if next_page_link %}
20 {% if next_page_link %}
21 <link rel="next" href="{{ next_page_link }}" />
21 <link rel="next" href="{{ next_page_link }}" />
22 {% endif %}
22 {% endif %}
23
23
24 {% endblock %}
24 {% endblock %}
25
25
26 {% block content %}
26 {% block content %}
27
27
28 {% get_current_language as LANGUAGE_CODE %}
28 {% get_current_language as LANGUAGE_CODE %}
29 {% get_current_timezone as TIME_ZONE %}
29 {% get_current_timezone as TIME_ZONE %}
30
30
31 {% for banner in banners %}
31 {% for banner in banners %}
32 <div class="post">
32 <div class="post">
33 <div class="title">{{ banner.title }}</div>
33 <div class="title">{{ banner.title }}</div>
34 <div>{{ banner.text }}</div>
34 <div>{{ banner.text }}</div>
35 <div>{% trans 'Related message' %}: <a href="{{ banner.post.get_absolute_url }}">>>{{ banner.post.id }}</a></div>
35 <div>{% trans 'Related message' %}: <a href="{{ banner.post.get_absolute_url }}">>>{{ banner.post.id }}</a></div>
36 </div>
36 </div>
37 {% endfor %}
37 {% endfor %}
38
38
39 {% if tag %}
39 {% if tag %}
40 <div class="tag_info" style="border-bottom: solid .5ex #{{ tag.get_color }}">
40 <div class="tag_info" style="border-bottom: solid .5ex #{{ tag.get_color }}">
41 {% if random_image_post %}
41 {% if random_image_post %}
42 <div class="tag-image">
42 <div class="tag-image">
43 {% with image=random_image_post.images.first %}
43 {% with image=random_image_post.images.first %}
44 <a href="{{ random_image_post.get_absolute_url }}"><img
44 <a href="{{ random_image_post.get_absolute_url }}"><img
45 src="{{ image.image.url_200x150 }}"
45 src="{{ image.image.url_200x150 }}"
46 width="{{ image.pre_width }}"
46 width="{{ image.pre_width }}"
47 height="{{ image.pre_height }}"/></a>
47 height="{{ image.pre_height }}"/></a>
48 {% endwith %}
48 {% endwith %}
49 </div>
49 </div>
50 {% endif %}
50 {% endif %}
51 <div class="tag-text-data">
51 <div class="tag-text-data">
52 <h2>
52 <h2>
53 <form action="{% url 'tag' tag.name %}" method="post" class="post-button-form">
53 <form action="{% url 'tag' tag.name %}" method="post" class="post-button-form">
54 {% if is_favorite %}
54 {% if is_favorite %}
55 <button name="method" value="unsubscribe" class="fav">β˜…</button>
55 <button name="method" value="unsubscribe" class="fav">β˜…</button>
56 {% else %}
56 {% else %}
57 <button name="method" value="subscribe" class="not_fav">β˜…</button>
57 <button name="method" value="subscribe" class="not_fav">β˜…</button>
58 {% endif %}
58 {% endif %}
59 </form>
59 </form>
60 <form action="{% url 'tag' tag.name %}" method="post" class="post-button-form">
60 <form action="{% url 'tag' tag.name %}" method="post" class="post-button-form">
61 {% if is_hidden %}
61 {% if is_hidden %}
62 <button name="method" value="unhide" class="fav">H</button>
62 <button name="method" value="unhide" class="fav">H</button>
63 {% else %}
63 {% else %}
64 <button name="method" value="hide" class="not_fav">H</button>
64 <button name="method" value="hide" class="not_fav">H</button>
65 {% endif %}
65 {% endif %}
66 </form>
66 </form>
67 {{ tag.get_view|safe }}
67 {{ tag.get_view|safe }}
68 {% if moderator %}
68 {% if moderator %}
69 <span class="moderator_info">| <a href="{% url 'admin:boards_tag_change' tag.id %}">{% trans 'Edit tag' %}</a></span>
69 <span class="moderator_info">| <a href="{% url 'admin:boards_tag_change' tag.id %}">{% trans 'Edit tag' %}</a></span>
70 {% endif %}
70 {% endif %}
71 </h2>
71 </h2>
72 {% if tag.get_description %}
72 {% if tag.get_description %}
73 <p>{{ tag.get_description|safe }}</p>
73 <p>{{ tag.get_description|safe }}</p>
74 {% endif %}
74 {% endif %}
75 <p>
75 <p>
76 {% blocktrans count count=tag.get_active_thread_count %}{{ count }} active thread{% plural %}active threads{% endblocktrans %},
76 {% blocktrans count count=tag.get_active_thread_count %}{{ count }} active thread{% plural %}active threads{% endblocktrans %},
77 {% blocktrans count count=tag.get_bumplimit_thread_count %}{{ count }} thread in bumplimit{% plural %} threads in bumplimit{% endblocktrans %},
77 {% blocktrans count count=tag.get_bumplimit_thread_count %}{{ count }} thread in bumplimit{% plural %} threads in bumplimit{% endblocktrans %},
78 {% blocktrans count count=tag.get_archived_thread_count %}{{ count }} archived thread{% plural %}archived threads{% endblocktrans %},
78 {% blocktrans count count=tag.get_archived_thread_count %}{{ count }} archived thread{% plural %}archived threads{% endblocktrans %},
79 {% blocktrans count count=tag.get_post_count %}{{ count }} message{% plural %}messages{% endblocktrans %}.
79 {% blocktrans count count=tag.get_post_count %}{{ count }} message{% plural %}messages{% endblocktrans %}.
80 </p>
80 </p>
81 {% if tag.get_all_parents %}
81 {% if tag.get_all_parents %}
82 <p>
82 <p>
83 {% for parent in tag.get_all_parents %}
83 {% for parent in tag.get_all_parents %}
84 {{ parent.get_view|safe }} &gt;
84 {{ parent.get_view|safe }} &gt;
85 {% endfor %}
85 {% endfor %}
86 {{ tag.get_view|safe }}
86 {{ tag.get_view|safe }}
87 </p>
87 </p>
88 {% endif %}
88 {% endif %}
89 </div>
89 </div>
90 </div>
90 </div>
91 {% endif %}
91 {% endif %}
92
92
93 {% if threads %}
93 {% if threads %}
94 {% if prev_page_link %}
94 {% if prev_page_link %}
95 <div class="page_link">
95 <div class="page_link">
96 <a href="{{ prev_page_link }}">{% trans "Previous page" %}</a>
96 <a href="{{ prev_page_link }}">{% trans "Previous page" %}</a>
97 </div>
97 </div>
98 {% endif %}
98 {% endif %}
99
99
100 {% for thread in threads %}
100 {% for thread in threads %}
101 <div class="thread">
101 <div class="thread">
102 {% post_view thread.get_opening_post moderator=moderator thread=thread truncated=True need_open_link=True %}
102 {% post_view thread.get_opening_post perms=perms thread=thread truncated=True need_open_link=True %}
103 {% if not thread.archived %}
103 {% if not thread.archived %}
104 {% with last_replies=thread.get_last_replies %}
104 {% with last_replies=thread.get_last_replies %}
105 {% if last_replies %}
105 {% if last_replies %}
106 {% with skipped_replies_count=thread.get_skipped_replies_count %}
106 {% with skipped_replies_count=thread.get_skipped_replies_count %}
107 {% if skipped_replies_count %}
107 {% if skipped_replies_count %}
108 <div class="skipped_replies">
108 <div class="skipped_replies">
109 <a href="{% url 'thread' thread.get_opening_post_id %}">
109 <a href="{% url 'thread' thread.get_opening_post_id %}">
110 {% blocktrans count count=skipped_replies_count %}Skipped {{ count }} reply. Open thread to see all replies.{% plural %}Skipped {{ count }} replies. Open thread to see all replies.{% endblocktrans %}
110 {% blocktrans count count=skipped_replies_count %}Skipped {{ count }} reply. Open thread to see all replies.{% plural %}Skipped {{ count }} replies. Open thread to see all replies.{% endblocktrans %}
111 </a>
111 </a>
112 </div>
112 </div>
113 {% endif %}
113 {% endif %}
114 {% endwith %}
114 {% endwith %}
115 <div class="last-replies">
115 <div class="last-replies">
116 {% for post in last_replies %}
116 {% for post in last_replies %}
117 {% post_view post moderator=moderator truncated=True %}
117 {% post_view post perms=perms truncated=True %}
118 {% endfor %}
118 {% endfor %}
119 </div>
119 </div>
120 {% endif %}
120 {% endif %}
121 {% endwith %}
121 {% endwith %}
122 {% endif %}
122 {% endif %}
123 </div>
123 </div>
124 {% endfor %}
124 {% endfor %}
125
125
126 {% if next_page_link %}
126 {% if next_page_link %}
127 <div class="page_link">
127 <div class="page_link">
128 <a href="{{ next_page_link }}">{% trans "Next page" %}</a>
128 <a href="{{ next_page_link }}">{% trans "Next page" %}</a>
129 </div>
129 </div>
130 {% endif %}
130 {% endif %}
131 {% else %}
131 {% else %}
132 <div class="post">
132 <div class="post">
133 {% trans 'No threads exist. Create the first one!' %}</div>
133 {% trans 'No threads exist. Create the first one!' %}</div>
134 {% endif %}
134 {% endif %}
135
135
136 <div class="post-form-w">
136 <div class="post-form-w">
137 <script src="{% static 'js/panel.js' %}"></script>
137 <script src="{% static 'js/panel.js' %}"></script>
138 <div class="post-form">
138 <div class="post-form">
139 <div class="form-title">{% trans "Create new thread" %}</div>
139 <div class="form-title">{% trans "Create new thread" %}</div>
140 <div class="swappable-form-full">
140 <div class="swappable-form-full">
141 <form enctype="multipart/form-data" method="post" id="form">{% csrf_token %}
141 <form enctype="multipart/form-data" method="post" id="form">{% csrf_token %}
142 {{ form.as_div }}
142 {{ form.as_div }}
143 <div class="form-submit">
143 <div class="form-submit">
144 <input type="submit" value="{% trans "Post" %}"/>
144 <input type="submit" value="{% trans "Post" %}"/>
145 <button id="preview-button" onclick="return false;">{% trans 'Preview' %}</button>
145 <button id="preview-button" onclick="return false;">{% trans 'Preview' %}</button>
146 </div>
146 </div>
147 </form>
147 </form>
148 </div>
148 </div>
149 <div>
149 <div>
150 {% trans 'Tags must be delimited by spaces. Text or image is required.' %}
150 {% trans 'Tags must be delimited by spaces. Text or image is required.' %}
151 </div>
151 </div>
152 <div id="preview-text"></div>
152 <div id="preview-text"></div>
153 <div><a href="{% url "staticpage" name="help" %}">{% trans 'Text syntax' %}</a></div>
153 <div><a href="{% url "staticpage" name="help" %}">{% trans 'Text syntax' %}</a></div>
154 <div><a href="{% url "tags" "required" %}">{% trans 'Tags' %}</a></div>
154 <div><a href="{% url "tags" "required" %}">{% trans 'Tags' %}</a></div>
155 </div>
155 </div>
156 </div>
156 </div>
157
157
158 <script src="{% static 'js/form.js' %}"></script>
158 <script src="{% static 'js/form.js' %}"></script>
159 <script src="{% static 'js/thread_create.js' %}"></script>
159 <script src="{% static 'js/thread_create.js' %}"></script>
160
160
161 {% endblock %}
161 {% endblock %}
162
162
163 {% block metapanel %}
163 {% block metapanel %}
164
164
165 <span class="metapanel">
165 <span class="metapanel">
166 <b><a href="{% url "authors" %}">{{ site_name }}</a> {{ version }}</b>
166 <b><a href="{% url "authors" %}">{{ site_name }}</a> {{ version }}</b>
167 {% trans "Pages:" %}
167 {% trans "Pages:" %}
168 [
168 [
169 {% with dividers=paginator.get_dividers %}
169 {% with dividers=paginator.get_dividers %}
170 {% for page in paginator.get_divided_range %}
170 {% for page in paginator.get_divided_range %}
171 {% if page in dividers %}
171 {% if page in dividers %}
172 …,
172 …,
173 {% endif %}
173 {% endif %}
174 <a
174 <a
175 {% ifequal page current_page.number %}
175 {% ifequal page current_page.number %}
176 class="current_page"
176 class="current_page"
177 {% endifequal %}
177 {% endifequal %}
178 href="{% page_url paginator page %}">{{ page }}</a>
178 href="{% page_url paginator page %}">{{ page }}</a>
179 {% if not forloop.last %},{% endif %}
179 {% if not forloop.last %},{% endif %}
180 {% endfor %}
180 {% endfor %}
181 {% endwith %}
181 {% endwith %}
182 ]
182 ]
183 [<a href="rss/">RSS</a>]
183 [<a href="rss/">RSS</a>]
184 </span>
184 </span>
185
185
186 {% endblock %}
186 {% endblock %}
@@ -1,110 +1,114 b''
1 {% load i18n %}
1 {% load i18n %}
2 {% load board %}
2 {% load board %}
3
3
4 {% get_current_language as LANGUAGE_CODE %}
4 {% get_current_language as LANGUAGE_CODE %}
5
5
6 <div class="{{ css_class }}" id="{{ post.id }}" data-uid="{{ post.uid }}">
6 <div class="{{ css_class }}" id="{{ post.id }}" data-uid="{{ post.uid }}">
7 <div class="post-info">
7 <div class="post-info">
8 <a class="post_id" href="{{ post.get_absolute_url }}">#{{ post.get_absolute_id }}</a>
8 <a class="post_id" href="{{ post.get_absolute_url }}">#{{ post.get_absolute_id }}</a>
9 <span class="title">{{ post.title }}</span>
9 <span class="title">{{ post.title }}</span>
10 <span class="pub_time"><time datetime="{{ post.pub_time|date:'c' }}">{{ post.pub_time }}</time></span>
10 <span class="pub_time"><time datetime="{{ post.pub_time|date:'c' }}">{{ post.pub_time }}</time></span>
11 {% if post.tripcode %}
11 {% if post.tripcode %}
12 /
12 /
13 {% with tripcode=post.get_tripcode %}
13 {% with tripcode=post.get_tripcode %}
14 <a href="{% url 'feed' %}?tripcode={{ tripcode.get_full_text }}"
14 <a href="{% url 'feed' %}?tripcode={{ tripcode.get_full_text }}"
15 class="tripcode" title="{{ tripcode.get_full_text }}"
15 class="tripcode" title="{{ tripcode.get_full_text }}"
16 style="border: solid 2px #{{ tripcode.get_color }}; border-left: solid 1ex #{{ tripcode.get_color }};">{{ tripcode.get_short_text }}</a>
16 style="border: solid 2px #{{ tripcode.get_color }}; border-left: solid 1ex #{{ tripcode.get_color }};">{{ tripcode.get_short_text }}</a>
17 {% endwith %}
17 {% endwith %}
18 {% endif %}
18 {% endif %}
19 {% comment %}
19 {% comment %}
20 Thread death time needs to be shown only if the thread is alredy archived
20 Thread death time needs to be shown only if the thread is alredy archived
21 and this is an opening post (thread death time) or a post for popup
21 and this is an opening post (thread death time) or a post for popup
22 (we don't see OP here so we show the death time in the post itself).
22 (we don't see OP here so we show the death time in the post itself).
23 {% endcomment %}
23 {% endcomment %}
24 {% if thread.archived %}
24 {% if thread.archived %}
25 {% if is_opening %}
25 {% if is_opening %}
26 β€” <time datetime="{{ thread.bump_time|date:'c' }}">{{ thread.bump_time }}</time>
26 β€” <time datetime="{{ thread.bump_time|date:'c' }}">{{ thread.bump_time }}</time>
27 {% endif %}
27 {% endif %}
28 {% endif %}
28 {% endif %}
29 {% if is_opening %}
29 {% if is_opening %}
30 {% if need_open_link %}
30 {% if need_open_link %}
31 {% if thread.archived %}
31 {% if thread.archived %}
32 <a class="link" href="{% url 'thread' post.id %}">{% trans "Open" %}</a>
32 <a class="link" href="{% url 'thread' post.id %}">{% trans "Open" %}</a>
33 {% else %}
33 {% else %}
34 <a class="link" href="{% url 'thread' post.id %}#form">{% trans "Reply" %}</a>
34 <a class="link" href="{% url 'thread' post.id %}#form">{% trans "Reply" %}</a>
35 {% endif %}
35 {% endif %}
36 {% endif %}
36 {% endif %}
37 {% else %}
37 {% else %}
38 {% if need_op_data %}
38 {% if need_op_data %}
39 {% with thread.get_opening_post as op %}
39 {% with thread.get_opening_post as op %}
40 {% trans " in " %}{{ op.get_link_view|safe }} <span class="title">{{ op.get_title_or_text }}</span>
40 {% trans " in " %}{{ op.get_link_view|safe }} <span class="title">{{ op.get_title_or_text }}</span>
41 {% endwith %}
41 {% endwith %}
42 {% endif %}
42 {% endif %}
43 {% endif %}
43 {% endif %}
44 {% if reply_link and not thread.archived %}
44 {% if reply_link and not thread.archived %}
45 <a href="#form" onclick="addQuickReply('{{ post.id }}'); return false;">{% trans 'Reply' %}</a>
45 <a href="#form" onclick="addQuickReply('{{ post.id }}'); return false;">{% trans 'Reply' %}</a>
46 {% endif %}
46 {% endif %}
47
47
48 {% if moderator %}
48 {% if perms.boards.change_post or perms.boards.delete_post or perms.boards.change_thread or perms_boards.delete_thread %}
49 <span class="moderator_info">
49 <span class="moderator_info">
50 | <a href="{% url 'admin:boards_post_change' post.id %}">{% trans 'Edit' %}</a>
50 {% if perms.boards.change_post or perms.boards.delete_post %}
51 {% if is_opening %}
51 | <a href="{% url 'admin:boards_post_change' post.id %}">{% trans 'Edit' %}</a>
52 | <a href="{% url 'admin:boards_thread_change' thread.id %}">{% trans 'Edit thread' %}</a>
52 <form action="{% url 'thread' thread.get_opening_post_id %}?post_id={{ post.id }}" method="post" class="post-button-form">
53 | <button name="method" value="toggle_hide_post">H</button>
54 </form>
53 {% endif %}
55 {% endif %}
54 <form action="{% url 'thread' thread.get_opening_post_id %}?post_id={{ post.id }}" method="post" class="post-button-form">
56 {% if perms.boards.change_thread or perms_boards.delete_thread %}
55 | <button name="method" value="toggle_hide_post">H</button>
57 {% if is_opening %}
56 </form>
58 | <a href="{% url 'admin:boards_thread_change' thread.id %}">{% trans 'Edit thread' %}</a>
59 {% endif %}
60 {% endif %}
57 </form>
61 </form>
58 </span>
62 </span>
59 {% endif %}
63 {% endif %}
60 </div>
64 </div>
61 {% comment %}
65 {% comment %}
62 Post images. Currently only 1 image can be posted and shown, but post model
66 Post images. Currently only 1 image can be posted and shown, but post model
63 supports multiple.
67 supports multiple.
64 {% endcomment %}
68 {% endcomment %}
65 {% for image in post.images.all %}
69 {% for image in post.images.all %}
66 {{ image.get_view|safe }}
70 {{ image.get_view|safe }}
67 {% endfor %}
71 {% endfor %}
68 {% for file in post.attachments.all %}
72 {% for file in post.attachments.all %}
69 {{ file.get_view|safe }}
73 {{ file.get_view|safe }}
70 {% endfor %}
74 {% endfor %}
71 {% comment %}
75 {% comment %}
72 Post message (text)
76 Post message (text)
73 {% endcomment %}
77 {% endcomment %}
74 <div class="message">
78 <div class="message">
75 {% autoescape off %}
79 {% autoescape off %}
76 {% if truncated %}
80 {% if truncated %}
77 {{ post.get_text|truncatewords_html:50 }}
81 {{ post.get_text|truncatewords_html:50 }}
78 {% else %}
82 {% else %}
79 {{ post.get_text }}
83 {{ post.get_text }}
80 {% endif %}
84 {% endif %}
81 {% endautoescape %}
85 {% endautoescape %}
82 </div>
86 </div>
83 {% if post.is_referenced %}
87 {% if post.is_referenced %}
84 {% if mode_tree %}
88 {% if mode_tree %}
85 <div class="tree_reply">
89 <div class="tree_reply">
86 {% for refpost in post.get_referenced_posts %}
90 {% for refpost in post.get_referenced_posts %}
87 {% post_view refpost mode_tree=True %}
91 {% post_view refpost mode_tree=True %}
88 {% endfor %}
92 {% endfor %}
89 </div>
93 </div>
90 {% else %}
94 {% else %}
91 <div class="refmap">
95 <div class="refmap">
92 {% trans "Replies" %}: {{ post.refmap|safe }}
96 {% trans "Replies" %}: {{ post.refmap|safe }}
93 </div>
97 </div>
94 {% endif %}
98 {% endif %}
95 {% endif %}
99 {% endif %}
96 {% comment %}
100 {% comment %}
97 Thread metadata: counters, tags etc
101 Thread metadata: counters, tags etc
98 {% endcomment %}
102 {% endcomment %}
99 {% if is_opening %}
103 {% if is_opening %}
100 <div class="metadata">
104 <div class="metadata">
101 {% if is_opening and need_open_link %}
105 {% if is_opening and need_open_link %}
102 {% blocktrans count count=thread.get_reply_count %}{{ count }} message{% plural %}{{ count }} messages{% endblocktrans %},
106 {% blocktrans count count=thread.get_reply_count %}{{ count }} message{% plural %}{{ count }} messages{% endblocktrans %},
103 {% blocktrans count count=thread.get_images_count %}{{ count }} image{% plural %}{{ count }} images{% endblocktrans %}.
107 {% blocktrans count count=thread.get_images_count %}{{ count }} image{% plural %}{{ count }} images{% endblocktrans %}.
104 {% endif %}
108 {% endif %}
105 <span class="tags">
109 <span class="tags">
106 {{ thread.get_tag_url_list|safe }}
110 {{ thread.get_tag_url_list|safe }}
107 </span>
111 </span>
108 </div>
112 </div>
109 {% endif %}
113 {% endif %}
110 </div>
114 </div>
@@ -1,70 +1,70 b''
1 {% extends "boards/thread.html" %}
1 {% extends "boards/thread.html" %}
2
2
3 {% load i18n %}
3 {% load i18n %}
4 {% load static from staticfiles %}
4 {% load static from staticfiles %}
5 {% load board %}
5 {% load board %}
6 {% load tz %}
6 {% load tz %}
7
7
8 {% block thread_content %}
8 {% block thread_content %}
9 {% get_current_language as LANGUAGE_CODE %}
9 {% get_current_language as LANGUAGE_CODE %}
10 {% get_current_timezone as TIME_ZONE %}
10 {% get_current_timezone as TIME_ZONE %}
11
11
12 <div class="tag_info">
12 <div class="tag_info">
13 <h2>
13 <h2>
14 <form action="{% url 'thread' opening_post.id %}" method="post" class="post-button-form">
14 <form action="{% url 'thread' opening_post.id %}" method="post" class="post-button-form">
15 {% if is_favorite %}
15 {% if is_favorite %}
16 <button name="method" value="unsubscribe" class="fav">β˜…</button>
16 <button name="method" value="unsubscribe" class="fav">β˜…</button>
17 {% else %}
17 {% else %}
18 <button name="method" value="subscribe" class="not_fav">β˜…</button>
18 <button name="method" value="subscribe" class="not_fav">β˜…</button>
19 {% endif %}
19 {% endif %}
20 </form>
20 </form>
21 {{ opening_post.get_title_or_text }}
21 {{ opening_post.get_title_or_text }}
22 </h2>
22 </h2>
23 </div>
23 </div>
24
24
25 {% if bumpable and thread.has_post_limit %}
25 {% if bumpable and thread.has_post_limit %}
26 <div class="bar-bg">
26 <div class="bar-bg">
27 <div class="bar-value" style="width:{{ bumplimit_progress }}%" id="bumplimit_progress">
27 <div class="bar-value" style="width:{{ bumplimit_progress }}%" id="bumplimit_progress">
28 </div>
28 </div>
29 <div class="bar-text">
29 <div class="bar-text">
30 <span id="left_to_limit">{{ posts_left }}</span> {% trans 'posts to bumplimit' %}
30 <span id="left_to_limit">{{ posts_left }}</span> {% trans 'posts to bumplimit' %}
31 </div>
31 </div>
32 </div>
32 </div>
33 {% endif %}
33 {% endif %}
34
34
35 <div class="thread">
35 <div class="thread">
36 {% for post in thread.get_replies %}
36 {% for post in thread.get_replies %}
37 {% post_view post moderator=moderator reply_link=True %}
37 {% post_view post perms=perms reply_link=True %}
38 {% endfor %}
38 {% endfor %}
39 </div>
39 </div>
40
40
41 {% if not thread.archived %}
41 {% if not thread.archived %}
42 <div class="post-form-w">
42 <div class="post-form-w">
43 <script src="{% static 'js/panel.js' %}"></script>
43 <script src="{% static 'js/panel.js' %}"></script>
44 <div class="form-title">{% trans "Reply to thread" %} #{{ opening_post.id }}<span class="reply-to-message"> {% trans "to message " %} #<span id="reply-to-message-id"></span></span></div>
44 <div class="form-title">{% trans "Reply to thread" %} #{{ opening_post.id }}<span class="reply-to-message"> {% trans "to message " %} #<span id="reply-to-message-id"></span></span></div>
45 <div class="post-form" id="compact-form">
45 <div class="post-form" id="compact-form">
46 <div class="swappable-form-full">
46 <div class="swappable-form-full">
47 <form enctype="multipart/form-data" method="post" id="form">{% csrf_token %}
47 <form enctype="multipart/form-data" method="post" id="form">{% csrf_token %}
48 <div class="compact-form-text"></div>
48 <div class="compact-form-text"></div>
49 {{ form.as_div }}
49 {{ form.as_div }}
50 <div class="form-submit">
50 <div class="form-submit">
51 <input type="submit" value="{% trans "Post" %}"/>
51 <input type="submit" value="{% trans "Post" %}"/>
52 <button id="preview-button" onclick="return false;">{% trans 'Preview' %}</button>
52 <button id="preview-button" onclick="return false;">{% trans 'Preview' %}</button>
53 </div>
53 </div>
54 </form>
54 </form>
55 </div>
55 </div>
56 <div id="preview-text"></div>
56 <div id="preview-text"></div>
57 <div><a href="{% url "staticpage" name="help" %}">
57 <div><a href="{% url "staticpage" name="help" %}">
58 {% trans 'Text syntax' %}</a></div>
58 {% trans 'Text syntax' %}</a></div>
59 <div><a id="form-close-button" href="#" onClick="resetFormPosition(); return false;">{% trans 'Close form' %}</a></div>
59 <div><a id="form-close-button" href="#" onClick="resetFormPosition(); return false;">{% trans 'Close form' %}</a></div>
60 </div>
60 </div>
61 </div>
61 </div>
62
62
63 <script src="{% static 'js/jquery.form.min.js' %}"></script>
63 <script src="{% static 'js/jquery.form.min.js' %}"></script>
64 {% endif %}
64 {% endif %}
65
65
66 <script src="{% static 'js/form.js' %}"></script>
66 <script src="{% static 'js/form.js' %}"></script>
67 <script src="{% static 'js/thread.js' %}"></script>
67 <script src="{% static 'js/thread.js' %}"></script>
68 <script src="{% static 'js/thread_update.js' %}"></script>
68 <script src="{% static 'js/thread_update.js' %}"></script>
69 <script src="{% static 'js/3party/centrifuge.js' %}"></script>
69 <script src="{% static 'js/3party/centrifuge.js' %}"></script>
70 {% endblock %}
70 {% endblock %}
@@ -1,148 +1,138 b''
1 """
1 """
2 This module contains helper functions and helper classes.
2 This module contains helper functions and helper classes.
3 """
3 """
4 import hashlib
4 import hashlib
5 from random import random
5 from random import random
6 import time
6 import time
7 import hmac
7 import hmac
8
8
9 from django.core.cache import cache
9 from django.core.cache import cache
10 from django.db.models import Model
10 from django.db.models import Model
11 from django import forms
11 from django import forms
12 from django.utils import timezone
12 from django.utils import timezone
13 from django.utils.translation import ugettext_lazy as _
13 from django.utils.translation import ugettext_lazy as _
14 import magic
14 import magic
15 from portage import os
15 from portage import os
16
16
17 import boards
17 import boards
18 from boards.settings import get_bool
18 from boards.settings import get_bool
19 from neboard import settings
19 from neboard import settings
20
20
21 CACHE_KEY_DELIMITER = '_'
21 CACHE_KEY_DELIMITER = '_'
22 PERMISSION_MODERATE = 'moderation'
23
22
24 HTTP_FORWARDED = 'HTTP_X_FORWARDED_FOR'
23 HTTP_FORWARDED = 'HTTP_X_FORWARDED_FOR'
25 META_REMOTE_ADDR = 'REMOTE_ADDR'
24 META_REMOTE_ADDR = 'REMOTE_ADDR'
26
25
27 SETTING_MESSAGES = 'Messages'
26 SETTING_MESSAGES = 'Messages'
28 SETTING_ANON_MODE = 'AnonymousMode'
27 SETTING_ANON_MODE = 'AnonymousMode'
29
28
30 ANON_IP = '127.0.0.1'
29 ANON_IP = '127.0.0.1'
31
30
32 UPLOAD_DIRS ={
31 UPLOAD_DIRS ={
33 'PostImage': 'images/',
32 'PostImage': 'images/',
34 'Attachment': 'files/',
33 'Attachment': 'files/',
35 }
34 }
36 FILE_EXTENSION_DELIMITER = '.'
35 FILE_EXTENSION_DELIMITER = '.'
37
36
38
37
39 def is_anonymous_mode():
38 def is_anonymous_mode():
40 return get_bool(SETTING_MESSAGES, SETTING_ANON_MODE)
39 return get_bool(SETTING_MESSAGES, SETTING_ANON_MODE)
41
40
42
41
43 def get_client_ip(request):
42 def get_client_ip(request):
44 if is_anonymous_mode():
43 if is_anonymous_mode():
45 ip = ANON_IP
44 ip = ANON_IP
46 else:
45 else:
47 x_forwarded_for = request.META.get(HTTP_FORWARDED)
46 x_forwarded_for = request.META.get(HTTP_FORWARDED)
48 if x_forwarded_for:
47 if x_forwarded_for:
49 ip = x_forwarded_for.split(',')[-1].strip()
48 ip = x_forwarded_for.split(',')[-1].strip()
50 else:
49 else:
51 ip = request.META.get(META_REMOTE_ADDR)
50 ip = request.META.get(META_REMOTE_ADDR)
52 return ip
51 return ip
53
52
54
53
55 # TODO The output format is not epoch because it includes microseconds
54 # TODO The output format is not epoch because it includes microseconds
56 def datetime_to_epoch(datetime):
55 def datetime_to_epoch(datetime):
57 return int(time.mktime(timezone.localtime(
56 return int(time.mktime(timezone.localtime(
58 datetime,timezone.get_current_timezone()).timetuple())
57 datetime,timezone.get_current_timezone()).timetuple())
59 * 1000000 + datetime.microsecond)
58 * 1000000 + datetime.microsecond)
60
59
61
60
62 def get_websocket_token(user_id='', timestamp=''):
61 def get_websocket_token(user_id='', timestamp=''):
63 """
62 """
64 Create token to validate information provided by new connection.
63 Create token to validate information provided by new connection.
65 """
64 """
66
65
67 sign = hmac.new(settings.CENTRIFUGE_PROJECT_SECRET.encode())
66 sign = hmac.new(settings.CENTRIFUGE_PROJECT_SECRET.encode())
68 sign.update(settings.CENTRIFUGE_PROJECT_ID.encode())
67 sign.update(settings.CENTRIFUGE_PROJECT_ID.encode())
69 sign.update(user_id.encode())
68 sign.update(user_id.encode())
70 sign.update(timestamp.encode())
69 sign.update(timestamp.encode())
71 token = sign.hexdigest()
70 token = sign.hexdigest()
72
71
73 return token
72 return token
74
73
75
74
76 def cached_result(key_method=None):
75 def cached_result(key_method=None):
77 """
76 """
78 Caches method result in the Django's cache system, persisted by object name,
77 Caches method result in the Django's cache system, persisted by object name,
79 object name and model id if object is a Django model.
78 object name and model id if object is a Django model.
80 """
79 """
81 def _cached_result(function):
80 def _cached_result(function):
82 def inner_func(obj, *args, **kwargs):
81 def inner_func(obj, *args, **kwargs):
83 # TODO Include method arguments to the cache key
82 # TODO Include method arguments to the cache key
84 cache_key_params = [obj.__class__.__name__, function.__name__]
83 cache_key_params = [obj.__class__.__name__, function.__name__]
85 if isinstance(obj, Model):
84 if isinstance(obj, Model):
86 cache_key_params.append(str(obj.id))
85 cache_key_params.append(str(obj.id))
87
86
88 if key_method is not None:
87 if key_method is not None:
89 cache_key_params += [str(arg) for arg in key_method(obj)]
88 cache_key_params += [str(arg) for arg in key_method(obj)]
90
89
91 cache_key = CACHE_KEY_DELIMITER.join(cache_key_params)
90 cache_key = CACHE_KEY_DELIMITER.join(cache_key_params)
92
91
93 persisted_result = cache.get(cache_key)
92 persisted_result = cache.get(cache_key)
94 if persisted_result is not None:
93 if persisted_result is not None:
95 result = persisted_result
94 result = persisted_result
96 else:
95 else:
97 result = function(obj, *args, **kwargs)
96 result = function(obj, *args, **kwargs)
98 cache.set(cache_key, result)
97 cache.set(cache_key, result)
99
98
100 return result
99 return result
101
100
102 return inner_func
101 return inner_func
103 return _cached_result
102 return _cached_result
104
103
105
104
106 def is_moderator(request):
107 try:
108 moderate = request.user.has_perm(PERMISSION_MODERATE)
109 except AttributeError:
110 moderate = False
111
112 return moderate
113
114
115 def get_file_hash(file) -> str:
105 def get_file_hash(file) -> str:
116 md5 = hashlib.md5()
106 md5 = hashlib.md5()
117 for chunk in file.chunks():
107 for chunk in file.chunks():
118 md5.update(chunk)
108 md5.update(chunk)
119 return md5.hexdigest()
109 return md5.hexdigest()
120
110
121
111
122 def validate_file_size(size: int):
112 def validate_file_size(size: int):
123 max_size = boards.settings.get_int('Forms', 'MaxFileSize')
113 max_size = boards.settings.get_int('Forms', 'MaxFileSize')
124 if size > max_size:
114 if size > max_size:
125 raise forms.ValidationError(
115 raise forms.ValidationError(
126 _('File must be less than %s bytes')
116 _('File must be less than %s bytes')
127 % str(max_size))
117 % str(max_size))
128
118
129
119
130 def get_extension(filename):
120 def get_extension(filename):
131 return filename.split(FILE_EXTENSION_DELIMITER)[-1:][0]
121 return filename.split(FILE_EXTENSION_DELIMITER)[-1:][0]
132
122
133
123
134 def get_upload_filename(model_instance, old_filename):
124 def get_upload_filename(model_instance, old_filename):
135 # TODO Use something other than random number in file name
125 # TODO Use something other than random number in file name
136 extension = get_extension(old_filename)
126 extension = get_extension(old_filename)
137 new_name = '{}{}.{}'.format(
127 new_name = '{}{}.{}'.format(
138 str(int(time.mktime(time.gmtime()))),
128 str(int(time.mktime(time.gmtime()))),
139 str(int(random() * 1000)),
129 str(int(random() * 1000)),
140 extension)
130 extension)
141
131
142 directory = UPLOAD_DIRS[type(model_instance).__name__]
132 directory = UPLOAD_DIRS[type(model_instance).__name__]
143
133
144 return os.path.join(directory, new_name)
134 return os.path.join(directory, new_name)
145
135
146
136
147 def get_file_mimetype(file) -> str:
137 def get_file_mimetype(file) -> str:
148 return magic.from_buffer(file.chunks().__next__(), mime=True).decode()
138 return magic.from_buffer(file.chunks().__next__(), mime=True).decode()
General Comments 0
You need to be logged in to leave comments. Login now