##// 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 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 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 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 Yasako'
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._validate_image(image)
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 = timezone.now()
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 {% for tag in tags %}
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 {% for tag in thread.get_tags %}
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 {% if thread.get_skipped_replies_count %}
91 <div class="skipped_replies">
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 </div>
96 {% endif %}
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