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