Show More
@@ -25,3 +25,4 b' 957e2fec91468f739b0fc2b9936d564505048c68' | |||
|
25 | 25 | bb91141c6ea5c822ccbe2d46c3c48bdab683b77d 2.4.0 |
|
26 | 26 | 97eb184637e5691b288eaf6b03e8971f3364c239 2.5.0 |
|
27 | 27 | 119fafc5381b933bf30d97be0b278349f6135075 2.5.1 |
|
28 | d528d76d3242cced614fa11bb63f3d342e4e1d09 2.5.2 |
@@ -5,7 +5,7 b' from boards.models.user import Notificat' | |||
|
5 | 5 | __author__ = 'neko259' |
|
6 | 6 | |
|
7 | 7 | from boards import settings |
|
8 | from boards.models import Post | |
|
8 | from boards.models import Post, Tag | |
|
9 | 9 | |
|
10 | 10 | CONTEXT_SITE_NAME = 'site_name' |
|
11 | 11 | CONTEXT_VERSION = 'version' |
@@ -17,6 +17,7 b" CONTEXT_TAGS = 'tags'" | |||
|
17 | 17 | CONTEXT_USER = 'user' |
|
18 | 18 | CONTEXT_NEW_NOTIFICATIONS_COUNT = 'new_notifications_count' |
|
19 | 19 | CONTEXT_USERNAME = 'username' |
|
20 | CONTEXT_TAGS_STR = 'tags_str' | |
|
20 | 21 | |
|
21 | 22 | PERMISSION_MODERATE = 'moderation' |
|
22 | 23 | |
@@ -49,7 +50,9 b' def user_and_ui_processor(request):' | |||
|
49 | 50 | context[CONTEXT_PPD] = float(Post.objects.get_posts_per_day()) |
|
50 | 51 | |
|
51 | 52 | settings_manager = get_settings_manager(request) |
|
52 |
|
|
|
53 | fav_tags = settings_manager.get_fav_tags() | |
|
54 | context[CONTEXT_TAGS] = fav_tags | |
|
55 | context[CONTEXT_TAGS_STR] = Tag.objects.get_tag_url_list(fav_tags) | |
|
53 | 56 | theme = settings_manager.get_theme() |
|
54 | 57 | context[CONTEXT_THEME] = theme |
|
55 | 58 | context[CONTEXT_THEME_CSS] = 'css/' + theme + '/base_page.css' |
@@ -1,4 +1,4 b'' | |||
|
1 |
VERSION = '2.5. |
|
|
1 | VERSION = '2.5.2 Yasako' | |
|
2 | 2 | SITE_NAME = 'Neboard' |
|
3 | 3 | |
|
4 | 4 | CACHE_TIMEOUT = 600 # Timeout for caching, if cache is used |
@@ -166,7 +166,8 b' class PostForm(NeboardForm):' | |||
|
166 | 166 | def clean_image(self): |
|
167 | 167 | image = self.cleaned_data['image'] |
|
168 | 168 | |
|
169 | self._validate_image(image) | |
|
169 | if image: | |
|
170 | self.validate_image_size(image.size) | |
|
170 | 171 | |
|
171 | 172 | return image |
|
172 | 173 | |
@@ -179,8 +180,8 b' class PostForm(NeboardForm):' | |||
|
179 | 180 | |
|
180 | 181 | if not image: |
|
181 | 182 | raise forms.ValidationError(_('Invalid URL')) |
|
182 | ||
|
183 |
self. |
|
|
183 | else: | |
|
184 | self.validate_image_size(image.size) | |
|
184 | 185 | |
|
185 | 186 | return image |
|
186 | 187 | |
@@ -218,13 +219,6 b' class PostForm(NeboardForm):' | |||
|
218 | 219 | error_message = _('Either text or image must be entered.') |
|
219 | 220 | self._errors['text'] = self.error_class([error_message]) |
|
220 | 221 | |
|
221 | def _validate_image(self, image): | |
|
222 | if image: | |
|
223 | if image.size > board_settings.MAX_IMAGE_SIZE: | |
|
224 | raise forms.ValidationError( | |
|
225 | _('Image must be less than %s bytes') | |
|
226 | % str(board_settings.MAX_IMAGE_SIZE)) | |
|
227 | ||
|
228 | 222 | def _validate_posting_speed(self): |
|
229 | 223 | can_post = True |
|
230 | 224 | |
@@ -247,6 +241,12 b' class PostForm(NeboardForm):' | |||
|
247 | 241 | if can_post: |
|
248 | 242 | self.session[LAST_POST_TIME] = time.time() |
|
249 | 243 | |
|
244 | def validate_image_size(self, size: int): | |
|
245 | if size > board_settings.MAX_IMAGE_SIZE: | |
|
246 | raise forms.ValidationError( | |
|
247 | _('Image must be less than %s bytes') | |
|
248 | % str(board_settings.MAX_IMAGE_SIZE)) | |
|
249 | ||
|
250 | 250 | def _get_image_from_url(self, url: str) -> SimpleUploadedFile: |
|
251 | 251 | """ |
|
252 | 252 | Gets an image file from URL. |
@@ -262,11 +262,7 b' class PostForm(NeboardForm):' | |||
|
262 | 262 | length_header = response_head.headers.get('content-length') |
|
263 | 263 | if length_header: |
|
264 | 264 | length = int(length_header) |
|
265 | if length > board_settings.MAX_IMAGE_SIZE: | |
|
266 | raise forms.ValidationError( | |
|
267 | _('Image must be less than %s bytes') | |
|
268 | % str(board_settings.MAX_IMAGE_SIZE)) | |
|
269 | ||
|
265 | self.validate_image_size(length) | |
|
270 | 266 | # Get the actual content into memory |
|
271 | 267 | response = requests.get(url, verify=False, stream=True) |
|
272 | 268 | |
@@ -275,11 +271,7 b' class PostForm(NeboardForm):' | |||
|
275 | 271 | content = b'' |
|
276 | 272 | for chunk in response.iter_content(IMAGE_DOWNLOAD_CHUNK_BYTES): |
|
277 | 273 | size += len(chunk) |
|
278 | if size > board_settings.MAX_IMAGE_SIZE: | |
|
279 | # TODO Dedup this code into a method | |
|
280 | raise forms.ValidationError( | |
|
281 | _('Image must be less than %s bytes') | |
|
282 | % str(board_settings.MAX_IMAGE_SIZE)) | |
|
274 | self.validate_image_size(size) | |
|
283 | 275 | content += chunk |
|
284 | 276 | |
|
285 | 277 | if response.status_code == HTTP_RESULT_OK and content: |
@@ -18,7 +18,30 b" CSS_CLASS_IMAGE = 'image'" | |||
|
18 | 18 | CSS_CLASS_THUMB = 'thumb' |
|
19 | 19 | |
|
20 | 20 | |
|
21 | class PostImageManager(models.Manager): | |
|
22 | def create_with_hash(self, image): | |
|
23 | image_hash = self.get_hash(image) | |
|
24 | existing = self.filter(hash=image_hash) | |
|
25 | if len(existing) > 0: | |
|
26 | post_image = existing[0] | |
|
27 | else: | |
|
28 | post_image = PostImage.objects.create(image=image) | |
|
29 | ||
|
30 | return post_image | |
|
31 | ||
|
32 | def get_hash(self, image): | |
|
33 | """ | |
|
34 | Gets hash of an image. | |
|
35 | """ | |
|
36 | md5 = hashlib.md5() | |
|
37 | for chunk in image.chunks(): | |
|
38 | md5.update(chunk) | |
|
39 | return md5.hexdigest() | |
|
40 | ||
|
41 | ||
|
21 | 42 | class PostImage(models.Model, Viewable): |
|
43 | objects = PostImageManager() | |
|
44 | ||
|
22 | 45 | class Meta: |
|
23 | 46 | app_label = 'boards' |
|
24 | 47 | ordering = ('id',) |
@@ -29,10 +52,12 b' class PostImage(models.Model, Viewable):' | |||
|
29 | 52 | """ |
|
30 | 53 | |
|
31 | 54 | path = IMAGES_DIRECTORY |
|
32 | new_name = str(int(time.mktime(time.gmtime()))) | |
|
33 | new_name += str(int(random() * 1000)) | |
|
34 | new_name += FILE_EXTENSION_DELIMITER | |
|
35 | new_name += filename.split(FILE_EXTENSION_DELIMITER)[-1:][0] | |
|
55 | ||
|
56 | # TODO Use something other than random number in file name | |
|
57 | new_name = '{}{}.{}'.format( | |
|
58 | str(int(time.mktime(time.gmtime()))), | |
|
59 | str(int(random() * 1000)), | |
|
60 | filename.split(FILE_EXTENSION_DELIMITER)[-1:][0]) | |
|
36 | 61 | |
|
37 | 62 | return os.path.join(path, new_name) |
|
38 | 63 | |
@@ -56,7 +81,7 b' class PostImage(models.Model, Viewable):' | |||
|
56 | 81 | """ |
|
57 | 82 | |
|
58 | 83 | if not self.pk and self.image: |
|
59 | self.hash = PostImage.get_hash(self.image) | |
|
84 | self.hash = PostImage.objects.get_hash(self.image) | |
|
60 | 85 | super(PostImage, self).save(*args, **kwargs) |
|
61 | 86 | |
|
62 | 87 | def __str__(self): |
@@ -78,13 +103,3 b' class PostImage(models.Model, Viewable):' | |||
|
78 | 103 | self.image.url_200x150, |
|
79 | 104 | str(self.hash), str(self.pre_width), |
|
80 | 105 | str(self.pre_height), str(self.width), str(self.height)) |
|
81 | ||
|
82 | @staticmethod | |
|
83 | def get_hash(image): | |
|
84 | """ | |
|
85 | Gets hash of an image. | |
|
86 | """ | |
|
87 | md5 = hashlib.md5() | |
|
88 | for chunk in image.chunks(): | |
|
89 | md5.update(chunk) | |
|
90 | return md5.hexdigest() |
@@ -137,25 +137,15 b' class PostManager(models.Manager):' | |||
|
137 | 137 | post, post.poster_ip)) |
|
138 | 138 | |
|
139 | 139 | if image: |
|
140 | # Try to find existing image. If it exists, assign it to the post | |
|
141 | # instead of createing the new one | |
|
142 | image_hash = PostImage.get_hash(image) | |
|
143 | existing = PostImage.objects.filter(hash=image_hash) | |
|
144 | if len(existing) > 0: | |
|
145 | post_image = existing[0] | |
|
146 | else: | |
|
147 | post_image = PostImage.objects.create(image=image) | |
|
148 | logger.info('Created new image #{} for post #{}'.format( | |
|
149 | post_image.id, post.id)) | |
|
150 | post.images.add(post_image) | |
|
140 | post.images.add(PostImage.objects.create_with_hash(image)) | |
|
151 | 141 | |
|
152 | 142 | list(map(thread.add_tag, tags)) |
|
153 | 143 | |
|
154 | 144 | if new_thread: |
|
155 | 145 | boards.models.thread.Thread.objects.process_oldest_threads() |
|
156 | 146 | else: |
|
147 | thread.last_edit_time = posting_time | |
|
157 | 148 | thread.bump() |
|
158 | thread.last_edit_time = posting_time | |
|
159 | 149 | thread.save() |
|
160 | 150 | |
|
161 | 151 | post.connect_replies() |
@@ -21,6 +21,13 b' class TagManager(models.Manager):' | |||
|
21 | 21 | .annotate(num_threads=Count('thread')).filter(num_threads__gt=0)\ |
|
22 | 22 | .order_by('-required', 'name') |
|
23 | 23 | |
|
24 | def get_tag_url_list(self, tags: list) -> str: | |
|
25 | """ | |
|
26 | Gets a comma-separated list of tag links. | |
|
27 | """ | |
|
28 | ||
|
29 | return ', '.join([tag.get_view() for tag in tags]) | |
|
30 | ||
|
24 | 31 | |
|
25 | 32 | class Tag(models.Model, Viewable): |
|
26 | 33 | """ |
@@ -5,6 +5,7 b' from django.utils import timezone' | |||
|
5 | 5 | from django.db import models |
|
6 | 6 | |
|
7 | 7 | from boards import settings |
|
8 | import boards | |
|
8 | 9 | from boards.utils import cached_result |
|
9 | 10 | from boards.models.post import Post |
|
10 | 11 | |
@@ -41,6 +42,7 b' class ThreadManager(models.Manager):' | |||
|
41 | 42 | thread.archived = True |
|
42 | 43 | thread.bumpable = False |
|
43 | 44 | thread.last_edit_time = timezone.now() |
|
45 | thread.update_posts_time() | |
|
44 | 46 | thread.save(update_fields=['archived', 'last_edit_time', 'bumpable']) |
|
45 | 47 | |
|
46 | 48 | |
@@ -69,10 +71,11 b' class Thread(models.Model):' | |||
|
69 | 71 | """ |
|
70 | 72 | |
|
71 | 73 | if self.can_bump(): |
|
72 |
self.bump_time = |
|
|
74 | self.bump_time = self.last_edit_time | |
|
73 | 75 | |
|
74 | 76 | if self.get_reply_count() >= settings.MAX_POSTS_PER_THREAD: |
|
75 | 77 | self.bumpable = False |
|
78 | self.update_posts_time() | |
|
76 | 79 | |
|
77 | 80 | logger.info('Bumped thread %d' % self.id) |
|
78 | 81 | |
@@ -88,7 +91,7 b' class Thread(models.Model):' | |||
|
88 | 91 | Checks if the thread can be bumped by replying to it. |
|
89 | 92 | """ |
|
90 | 93 | |
|
91 | return self.bumpable | |
|
94 | return self.bumpable and not self.archived | |
|
92 | 95 | |
|
93 | 96 | def get_last_replies(self): |
|
94 | 97 | """ |
@@ -161,9 +164,6 b' class Thread(models.Model):' | |||
|
161 | 164 | |
|
162 | 165 | return self.get_opening_post(only_id=True).id |
|
163 | 166 | |
|
164 | def __unicode__(self): | |
|
165 | return str(self.id) | |
|
166 | ||
|
167 | 167 | def get_pub_time(self): |
|
168 | 168 | """ |
|
169 | 169 | Gets opening post's pub time because thread does not have its own one. |
@@ -183,3 +183,9 b' class Thread(models.Model):' | |||
|
183 | 183 | |
|
184 | 184 | def __str__(self): |
|
185 | 185 | return 'T#{}/{}'.format(self.id, self.get_opening_post_id()) |
|
186 | ||
|
187 | def get_tag_url_list(self): | |
|
188 | return boards.models.Tag.objects.get_tag_url_list(self.get_tags()) | |
|
189 | ||
|
190 | def update_posts_time(self): | |
|
191 | self.post_set.update(last_edit_time=self.last_edit_time) |
@@ -23,14 +23,16 b'' | |||
|
23 | 23 | for the JavaScript code in this page. |
|
24 | 24 | */ |
|
25 | 25 | |
|
26 | var LOCALE = window.navigator.language; | |
|
27 | var FORMATTER = new Intl.DateTimeFormat( | |
|
28 | LOCALE, | |
|
29 | { | |
|
30 | weekday: 'short', year: 'numeric', month: 'short', day: 'numeric', | |
|
31 | hour: 'numeric', minute: '2-digit', second: '2-digit' | |
|
32 | } | |
|
33 | ); | |
|
26 | if (window.Intl) { | |
|
27 | var LOCALE = window.navigator.language; | |
|
28 | var FORMATTER = new Intl.DateTimeFormat( | |
|
29 | LOCALE, | |
|
30 | { | |
|
31 | weekday: 'short', year: 'numeric', month: 'short', day: 'numeric', | |
|
32 | hour: 'numeric', minute: '2-digit', second: '2-digit' | |
|
33 | } | |
|
34 | ); | |
|
35 | } | |
|
34 | 36 | |
|
35 | 37 | /** |
|
36 | 38 | * An email is a hidden file to prevent spam bots from posting. It has to be |
@@ -53,6 +55,10 b' function highlightCode(node) {' | |||
|
53 | 55 | * Translate timestamps to local ones for all <time> tags inside node. |
|
54 | 56 | */ |
|
55 | 57 | function translate_time(node) { |
|
58 | if (window.Intl === null) { | |
|
59 | return; | |
|
60 | } | |
|
61 | ||
|
56 | 62 | var elements; |
|
57 | 63 | |
|
58 | 64 | if (node === null) { |
@@ -219,7 +219,6 b' function updateBumplimitProgress(postDel' | |||
|
219 | 219 | var newPostsToLimit = bumplimit - postCount; |
|
220 | 220 | if (newPostsToLimit <= 0) { |
|
221 | 221 | $('.bar-bg').remove(); |
|
222 | $('.thread').children('.post').addClass('dead_post'); | |
|
223 | 222 | } else { |
|
224 | 223 | postsToLimitElement.text(newPostsToLimit); |
|
225 | 224 | progressBar.width((100 - postCount / bumplimit * 100.0) + '%'); |
@@ -317,17 +316,19 b' function processNewPost(post) {' | |||
|
317 | 316 | |
|
318 | 317 | var form = $('#form'); |
|
319 | 318 | |
|
320 | var options = { | |
|
321 | beforeSubmit: function(arr, $form, options) { | |
|
322 | showAsErrors($('form'), gettext('Sending message...')); | |
|
323 | }, | |
|
324 | success: updateOnPost, | |
|
325 | url: '/api/add_post/' + threadId + '/' | |
|
326 | }; | |
|
319 | if (form.length > 0) { | |
|
320 | var options = { | |
|
321 | beforeSubmit: function(arr, $form, options) { | |
|
322 | showAsErrors($('form'), gettext('Sending message...')); | |
|
323 | }, | |
|
324 | success: updateOnPost, | |
|
325 | url: '/api/add_post/' + threadId + '/' | |
|
326 | }; | |
|
327 | 327 | |
|
328 | form.ajaxForm(options); | |
|
328 | form.ajaxForm(options); | |
|
329 | 329 | |
|
330 | resetForm(form); | |
|
330 | resetForm(form); | |
|
331 | } | |
|
331 | 332 | } |
|
332 | 333 | |
|
333 | 334 | $('#autoupdate').click(getThreadDiff); |
@@ -29,9 +29,7 b'' | |||
|
29 | 29 | <div class="navigation_panel header"> |
|
30 | 30 | <a class="link" href="{% url 'index' %}">{% trans "All threads" %}</a> |
|
31 | 31 | {% autoescape off %} |
|
32 |
{ |
|
|
33 | {{ tag.get_view }}, | |
|
34 | {% endfor %} | |
|
32 | {{ tags_str }}, | |
|
35 | 33 | {% endautoescape %} |
|
36 | 34 | <a href="{% url 'tags' %}" title="{% trans 'Tag management' %}" |
|
37 | 35 | >[...]</a>, |
@@ -26,7 +26,7 b'' | |||
|
26 | 26 | {% endcomment %} |
|
27 | 27 | {% if thread.archived %} |
|
28 | 28 | {% if is_opening %} |
|
29 | — {{ thread.bump_time }} | |
|
29 | — <time datetime="{{ thread.bump_time|date:'c' }}">{{ thread.bump_time|date:'r' }}</time> | |
|
30 | 30 | {% endif %} |
|
31 | 31 | {% endif %} |
|
32 | 32 | {% if is_opening and need_open_link %} |
@@ -92,9 +92,7 b'' | |||
|
92 | 92 | {% endif %} |
|
93 | 93 | <span class="tags"> |
|
94 | 94 | {% autoescape off %} |
|
95 |
{ |
|
|
96 | {{ tag.get_view }}{% if not forloop.last %},{% endif %} | |
|
97 | {% endfor %} | |
|
95 | {{ thread.get_tag_url_list }} | |
|
98 | 96 | {% endautoescape %} |
|
99 | 97 | </span> |
|
100 | 98 | </div> |
@@ -87,13 +87,15 b'' | |||
|
87 | 87 | {% if not thread.archived %} |
|
88 | 88 | {% with last_replies=thread.get_last_replies %} |
|
89 | 89 | {% if last_replies %} |
|
90 |
{% |
|
|
91 |
|
|
|
92 | <a href="{% url 'thread' thread.get_opening_post.id %}"> | |
|
93 | {% blocktrans with count=thread.get_skipped_replies_count %}Skipped {{ count }} replies. Open thread to see all replies.{% endblocktrans %} | |
|
94 | </a> | |
|
95 |
</ |
|
|
96 |
|
|
|
90 | {% with skipped_replies_count=thread.get_skipped_replies_count %} | |
|
91 | {% if skipped_replies_count %} | |
|
92 | <div class="skipped_replies"> | |
|
93 | <a href="{% url 'thread' thread.get_opening_post_id %}"> | |
|
94 | {% blocktrans with count=skipped_replies_count %}Skipped {{ count }} replies. Open thread to see all replies.{% endblocktrans %} | |
|
95 | </a> | |
|
96 | </div> | |
|
97 | {% endif %} | |
|
98 | {% endwith %} | |
|
97 | 99 | <div class="last-replies"> |
|
98 | 100 | {% for post in last_replies %} |
|
99 | 101 | {% post_view post is_opening=False moderator=moderator truncated=True %} |
@@ -61,12 +61,12 b'' | |||
|
61 | 61 | </div> |
|
62 | 62 | |
|
63 | 63 | <script src="{% static 'js/jquery.form.min.js' %}"></script> |
|
64 | <script src="{% static 'js/thread_update.js' %}"></script> | |
|
65 | <script src="{% static 'js/3party/centrifuge.js' %}"></script> | |
|
66 | 64 | {% endif %} |
|
67 | 65 | |
|
68 | 66 | <script src="{% static 'js/form.js' %}"></script> |
|
69 | 67 | <script src="{% static 'js/thread.js' %}"></script> |
|
68 | <script src="{% static 'js/thread_update.js' %}"></script> | |
|
69 | <script src="{% static 'js/3party/centrifuge.js' %}"></script> | |
|
70 | 70 | |
|
71 | 71 | {% endcache %} |
|
72 | 72 | {% endblock %} |
@@ -7,6 +7,8 b" ELLIPSIZER = '...'" | |||
|
7 | 7 | REGEX_LINES = re.compile(r'(<div class="br"></div>)', re.U | re.S) |
|
8 | 8 | REGEX_TAG = re.compile(r'<(/)?([^ ]+?)(?:(\s*/)| .*?)?>', re.S) |
|
9 | 9 | |
|
10 | IMG_ACTION_URL = '[<a href="{}">{}</a>]' | |
|
11 | ||
|
10 | 12 | |
|
11 | 13 | register = template.Library() |
|
12 | 14 | |
@@ -35,15 +37,10 b' def post_url(*args, **kwargs):' | |||
|
35 | 37 | def image_actions(*args, **kwargs): |
|
36 | 38 | image_link = args[0] |
|
37 | 39 | if len(args) > 1: |
|
38 | image_link = 'http://' + args[1] + image_link # TODO https? | |
|
39 | ||
|
40 | result = '' | |
|
40 | image_link = 'http://' + args[1] + image_link # TODO https? | |
|
41 | 41 | |
|
42 | for action in actions: | |
|
43 | result += '[<a href="' + action['link'] % image_link + '">' + \ | |
|
44 | action['name'] + '</a>]' | |
|
45 | ||
|
46 | return result | |
|
42 | return ', '.join([IMG_ACTION_URL.format( | |
|
43 | action['link'] % image_link, action['name'])for action in actions]) | |
|
47 | 44 | |
|
48 | 45 | |
|
49 | 46 | # TODO Use get_view of a post instead of this |
@@ -80,6 +77,7 b' def post_view(post, moderator=False, nee' | |||
|
80 | 77 | } |
|
81 | 78 | |
|
82 | 79 | |
|
80 | # TODO Fix or remove this method | |
|
83 | 81 | @register.filter(is_safe=True) |
|
84 | 82 | def truncate_lines(text, length): |
|
85 | 83 | if length <= 0: |
General Comments 0
You need to be logged in to leave comments.
Login now