##// END OF EJS Templates
Updating last update time of the thread when archiving it
neko259 -
r492:62014008 1.6-dev
parent child Browse files
Show More
@@ -1,392 +1,393 b''
1 1 from datetime import datetime, timedelta
2 2 from datetime import time as dtime
3 3 import os
4 4 from random import random
5 5 import time
6 6 import math
7 7 import re
8 8 from django.core.cache import cache
9 9
10 10 from django.db import models
11 11 from django.http import Http404
12 12 from django.utils import timezone
13 13 from markupfield.fields import MarkupField
14 14
15 15 from neboard import settings
16 16 from boards import thumbs
17 17
18 18 MAX_TITLE_LENGTH = 50
19 19
20 20 APP_LABEL_BOARDS = 'boards'
21 21
22 22 CACHE_KEY_PPD = 'ppd'
23 23
24 24 POSTS_PER_DAY_RANGE = range(7)
25 25
26 26 BAN_REASON_AUTO = 'Auto'
27 27
28 28 IMAGE_THUMB_SIZE = (200, 150)
29 29
30 30 TITLE_MAX_LENGTH = 50
31 31
32 32 DEFAULT_MARKUP_TYPE = 'markdown'
33 33
34 34 NO_PARENT = -1
35 35 NO_IP = '0.0.0.0'
36 36 UNKNOWN_UA = ''
37 37 ALL_PAGES = -1
38 38 IMAGES_DIRECTORY = 'images/'
39 39 FILE_EXTENSION_DELIMITER = '.'
40 40
41 41 SETTING_MODERATE = "moderate"
42 42
43 43 REGEX_REPLY = re.compile('>>(\d+)')
44 44
45 45
46 46 class PostManager(models.Manager):
47 47
48 48 def create_post(self, title, text, image=None, thread=None,
49 49 ip=NO_IP, tags=None, user=None):
50 50 """
51 51 Create new post
52 52 """
53 53
54 54 posting_time = timezone.now()
55 55 if not thread:
56 56 thread = Thread.objects.create(bump_time=posting_time,
57 57 last_edit_time=posting_time)
58 58 else:
59 59 thread.bump()
60 60 thread.last_edit_time = posting_time
61 61 thread.save()
62 62
63 63 post = self.create(title=title,
64 64 text=text,
65 65 pub_time=posting_time,
66 66 thread_new=thread,
67 67 image=image,
68 68 poster_ip=ip,
69 69 poster_user_agent=UNKNOWN_UA, # TODO Get UA at
70 70 # last!
71 71 last_edit_time=posting_time,
72 72 user=user)
73 73
74 74 thread.replies.add(post)
75 75 if tags:
76 76 linked_tags = []
77 77 for tag in tags:
78 78 tag_linked_tags = tag.get_linked_tags()
79 79 if len(tag_linked_tags) > 0:
80 80 linked_tags.extend(tag_linked_tags)
81 81
82 82 tags.extend(linked_tags)
83 83 map(thread.add_tag, tags)
84 84
85 85 self._delete_old_threads()
86 86 self.connect_replies(post)
87 87
88 88 return post
89 89
90 90 def delete_post(self, post):
91 91 """
92 92 Delete post and update or delete its thread
93 93 """
94 94
95 95 thread = post.thread_new
96 96
97 97 if thread.get_opening_post() == self:
98 98 thread.replies.delete()
99 99
100 100 thread.delete()
101 101 else:
102 102 thread.last_edit_time = timezone.now()
103 103 thread.save()
104 104
105 105 post.delete()
106 106
107 107 def delete_posts_by_ip(self, ip):
108 108 """
109 109 Delete all posts of the author with same IP
110 110 """
111 111
112 112 posts = self.filter(poster_ip=ip)
113 113 map(self.delete_post, posts)
114 114
115 115 # TODO Move this method to thread manager
116 116 def get_threads(self, tag=None, page=ALL_PAGES,
117 117 order_by='-bump_time', archived=False):
118 118 if tag:
119 119 threads = tag.threads
120 120
121 121 if not threads.exists():
122 122 raise Http404
123 123 else:
124 124 threads = Thread.objects.all()
125 125
126 126 threads = threads.filter(archived=archived).order_by(order_by)
127 127
128 128 if page != ALL_PAGES:
129 129 thread_count = threads.count()
130 130
131 131 if page < self._get_page_count(thread_count):
132 132 start_thread = page * settings.THREADS_PER_PAGE
133 133 end_thread = min(start_thread + settings.THREADS_PER_PAGE,
134 134 thread_count)
135 135 threads = threads[start_thread:end_thread]
136 136
137 137 return threads
138 138
139 139 # TODO Move this method to thread manager
140 140 def get_thread_page_count(self, tag=None, archived=False):
141 141 if tag:
142 142 threads = Thread.objects.filter(tags=tag)
143 143 else:
144 144 threads = Thread.objects.all()
145 145
146 146 threads = threads.filter(archived=archived)
147 147
148 148 return self._get_page_count(threads.count())
149 149
150 150 # TODO Move this method to thread manager
151 151 def _delete_old_threads(self):
152 152 """
153 153 Preserves maximum thread count. If there are too many threads,
154 154 archive the old ones.
155 155 """
156 156
157 157 threads = self.get_threads()
158 158 thread_count = threads.count()
159 159
160 160 if thread_count > settings.MAX_THREAD_COUNT:
161 161 num_threads_to_delete = thread_count - settings.MAX_THREAD_COUNT
162 162 old_threads = threads[thread_count - num_threads_to_delete:]
163 163
164 164 for thread in old_threads:
165 165 thread.archived = True
166 thread.last_edit_time = timezone.now()
166 167 thread.save()
167 168
168 169 def connect_replies(self, post):
169 170 """
170 171 Connect replies to a post to show them as a reflink map
171 172 """
172 173
173 174 for reply_number in re.finditer(REGEX_REPLY, post.text.raw):
174 175 post_id = reply_number.group(1)
175 176 ref_post = self.filter(id=post_id)
176 177 if ref_post.count() > 0:
177 178 referenced_post = ref_post[0]
178 179 referenced_post.referenced_posts.add(post)
179 180 referenced_post.last_edit_time = post.pub_time
180 181 referenced_post.save()
181 182
182 183 def _get_page_count(self, thread_count):
183 184 """
184 185 Get number of pages that will be needed for all threads
185 186 """
186 187
187 188 return int(math.ceil(thread_count / float(settings.THREADS_PER_PAGE)))
188 189
189 190 def get_posts_per_day(self):
190 191 """
191 192 Get average count of posts per day for the last 7 days
192 193 """
193 194
194 195 today = datetime.now().date()
195 196 ppd = cache.get(CACHE_KEY_PPD + str(today))
196 197 if ppd:
197 198 return ppd
198 199
199 200 posts_per_days = []
200 201 for i in POSTS_PER_DAY_RANGE:
201 202 day_end = today - timedelta(i + 1)
202 203 day_start = today - timedelta(i + 2)
203 204
204 205 day_time_start = timezone.make_aware(datetime.combine(day_start,
205 206 dtime()), timezone.get_current_timezone())
206 207 day_time_end = timezone.make_aware(datetime.combine(day_end,
207 208 dtime()), timezone.get_current_timezone())
208 209
209 210 posts_per_days.append(float(self.filter(
210 211 pub_time__lte=day_time_end,
211 212 pub_time__gte=day_time_start).count()))
212 213
213 214 ppd = (sum(posts_per_day for posts_per_day in posts_per_days) /
214 215 len(posts_per_days))
215 216 cache.set(CACHE_KEY_PPD, ppd)
216 217 return ppd
217 218
218 219
219 220 class Post(models.Model):
220 221 """A post is a message."""
221 222
222 223 objects = PostManager()
223 224
224 225 class Meta:
225 226 app_label = APP_LABEL_BOARDS
226 227
227 228 # TODO Save original file name to some field
228 229 def _update_image_filename(self, filename):
229 230 """Get unique image filename"""
230 231
231 232 path = IMAGES_DIRECTORY
232 233 new_name = str(int(time.mktime(time.gmtime())))
233 234 new_name += str(int(random() * 1000))
234 235 new_name += FILE_EXTENSION_DELIMITER
235 236 new_name += filename.split(FILE_EXTENSION_DELIMITER)[-1:][0]
236 237
237 238 return os.path.join(path, new_name)
238 239
239 240 title = models.CharField(max_length=TITLE_MAX_LENGTH)
240 241 pub_time = models.DateTimeField()
241 242 text = MarkupField(default_markup_type=DEFAULT_MARKUP_TYPE,
242 243 escape_html=False)
243 244
244 245 image_width = models.IntegerField(default=0)
245 246 image_height = models.IntegerField(default=0)
246 247
247 248 image_pre_width = models.IntegerField(default=0)
248 249 image_pre_height = models.IntegerField(default=0)
249 250
250 251 image = thumbs.ImageWithThumbsField(upload_to=_update_image_filename,
251 252 blank=True, sizes=(IMAGE_THUMB_SIZE,),
252 253 width_field='image_width',
253 254 height_field='image_height',
254 255 preview_width_field='image_pre_width',
255 256 preview_height_field='image_pre_height')
256 257
257 258 poster_ip = models.GenericIPAddressField()
258 259 poster_user_agent = models.TextField()
259 260
260 261 thread = models.ForeignKey('Post', null=True, default=None)
261 262 thread_new = models.ForeignKey('Thread', null=True, default=None)
262 263 last_edit_time = models.DateTimeField()
263 264 user = models.ForeignKey('User', null=True, default=None)
264 265
265 266 referenced_posts = models.ManyToManyField('Post', symmetrical=False,
266 267 null=True,
267 268 blank=True, related_name='rfp+')
268 269
269 270 def __unicode__(self):
270 271 return '#' + str(self.id) + ' ' + self.title + ' (' + \
271 272 self.text.raw[:50] + ')'
272 273
273 274 def get_title(self):
274 275 title = self.title
275 276 if len(title) == 0:
276 277 title = self.text.rendered[:MAX_TITLE_LENGTH]
277 278
278 279 return title
279 280
280 281 def get_sorted_referenced_posts(self):
281 282 return self.referenced_posts.order_by('id')
282 283
283 284 def is_referenced(self):
284 285 return self.referenced_posts.all().exists()
285 286
286 287 def is_opening(self):
287 288 return self.thread_new.get_replies()[0] == self
288 289
289 290
290 291 class Thread(models.Model):
291 292
292 293 class Meta:
293 294 app_label = APP_LABEL_BOARDS
294 295
295 296 tags = models.ManyToManyField('Tag')
296 297 bump_time = models.DateTimeField()
297 298 last_edit_time = models.DateTimeField()
298 299 replies = models.ManyToManyField('Post', symmetrical=False, null=True,
299 300 blank=True, related_name='tre+')
300 301 archived = models.BooleanField(default=False)
301 302
302 303 def get_tags(self):
303 304 """
304 305 Get a sorted tag list
305 306 """
306 307
307 308 return self.tags.order_by('name')
308 309
309 310 def bump(self):
310 311 """
311 312 Bump (move to up) thread
312 313 """
313 314
314 315 if self.can_bump():
315 316 self.bump_time = timezone.now()
316 317
317 318 def get_reply_count(self):
318 319 return self.replies.count()
319 320
320 321 def get_images_count(self):
321 322 return self.replies.filter(image_width__gt=0).count()
322 323
323 324 def can_bump(self):
324 325 """
325 326 Check if the thread can be bumped by replying
326 327 """
327 328
328 329 if self.archived:
329 330 return False
330 331
331 332 post_count = self.get_reply_count()
332 333
333 334 return post_count < settings.MAX_POSTS_PER_THREAD
334 335
335 336 def delete_with_posts(self):
336 337 """
337 338 Completely delete thread and all its posts
338 339 """
339 340
340 341 if self.replies.count() > 0:
341 342 self.replies.all().delete()
342 343
343 344 self.delete()
344 345
345 346 def get_last_replies(self):
346 347 """
347 348 Get last replies, not including opening post
348 349 """
349 350
350 351 if settings.LAST_REPLIES_COUNT > 0:
351 352 reply_count = self.get_reply_count()
352 353
353 354 if reply_count > 0:
354 355 reply_count_to_show = min(settings.LAST_REPLIES_COUNT,
355 356 reply_count - 1)
356 357 last_replies = self.replies.all().order_by('pub_time')[
357 358 reply_count - reply_count_to_show:]
358 359
359 360 return last_replies
360 361
361 362 def get_replies(self):
362 363 """
363 364 Get sorted thread posts
364 365 """
365 366
366 367 return self.replies.all().order_by('pub_time')
367 368
368 369 def add_tag(self, tag):
369 370 """
370 371 Connect thread to a tag and tag to a thread
371 372 """
372 373
373 374 self.tags.add(tag)
374 375 tag.threads.add(self)
375 376
376 377 def get_opening_post(self):
377 378 """
378 379 Get first post of the thread
379 380 """
380 381
381 382 return self.get_replies()[0]
382 383
383 384 def __unicode__(self):
384 385 return str(self.get_replies()[0].id)
385 386
386 387 def get_pub_time(self):
387 388 """
388 389 Thread does not have its own pub time, so we need to get it from
389 390 the opening post
390 391 """
391 392
392 393 return self.get_opening_post().pub_time
@@ -1,145 +1,145 b''
1 1 {% extends "boards/base.html" %}
2 2
3 3 {% load i18n %}
4 4 {% load cache %}
5 5 {% load board %}
6 6 {% load static %}
7 7
8 8 {% block head %}
9 9 <title>Neboard</title>
10 10
11 11 {% if prev_page %}
12 12 <link rel="next" href="
13 13 {% if tag %}
14 14 {% url "tag" tag_name=tag page=prev_page %}
15 15 {% else %}
16 16 {% url "index" page=prev_page %}
17 17 {% endif %}
18 18 " />
19 19 {% endif %}
20 20 {% if next_page %}
21 21 <link rel="next" href="
22 22 {% if tag %}
23 23 {% url "tag" tag_name=tag page=next_page %}
24 24 {% else %}
25 25 {% url "index" page=next_page %}
26 26 {% endif %}
27 27 " />
28 28 {% endif %}
29 29
30 30 {% endblock %}
31 31
32 32 {% block content %}
33 33
34 34 {% get_current_language as LANGUAGE_CODE %}
35 35
36 36 {% if threads %}
37 37 {% if prev_page %}
38 38 <div class="page_link">
39 39 <a href="{% url "archive" page=prev_page %}">{% trans "Previous page" %}</a>
40 40 </div>
41 41 {% endif %}
42 42
43 43 {% for thread in threads %}
44 44 {% cache 600 thread_short thread.id thread.thread.last_edit_time moderator LANGUAGE_CODE %}
45 45 <div class="thread">
46 46 <div class="post archive_post" id="{{ thread.op.id }}">
47 47 {% if thread.op.image %}
48 48 <div class="image">
49 49 <a class="thumb"
50 50 href="{{ thread.op.image.url }}"><img
51 51 src="{{ thread.op.image.url_200x150 }}"
52 52 alt="{{ thread.op.id }}"
53 53 width="{{ thread.op.image_pre_width }}"
54 54 height="{{ thread.op.image_pre_height }}"
55 55 data-width="{{ thread.op.image_width }}"
56 56 data-height="{{ thread.op.image_height }}"/>
57 57 </a>
58 58 </div>
59 59 {% endif %}
60 60 <div class="message">
61 61 <div class="post-info">
62 62 <span class="title">{{ thread.op.title }}</span>
63 63 <a class="post_id" href="{% url 'thread' thread.op.id %}"
64 64 > ({{ thread.op.id }})</a>
65 [{{ thread.op.pub_time }}] β€” [{{ thread.thread.bump_time }}]
65 [{{ thread.op.pub_time }}] β€” [{{ thread.thread.last_edit_time }}]
66 66
67 67 [<a class="link" href="
68 68 {% url 'thread' thread.op.id %}">{% trans "Open" %}</a>]
69 69
70 70 {% if moderator %}
71 71 <span class="moderator_info">
72 72 [<a href="
73 73 {% url 'delete' post_id=thread.op.id %}?next={{ request.path }}"
74 74 >{% trans 'Delete' %}</a>]
75 75 ({{ thread.op.poster_ip }})
76 76 [<a href="
77 77 {% url 'ban' post_id=thread.op.id %}?next={{ request.path }}"
78 78 >{% trans 'Ban IP' %}</a>]
79 79 </span>
80 80 {% endif %}
81 81 </div>
82 82 {% autoescape off %}
83 83 {{ thread.op.text.rendered|truncatewords_html:50 }}
84 84 {% endautoescape %}
85 85 {% if thread.op.is_referenced %}
86 86 <div class="refmap">
87 87 {% trans "Replies" %}:
88 88 {% for ref_post in thread.op.get_sorted_referenced_posts %}
89 89 <a href="{% post_url ref_post.id %}">&gt;&gt;{{ ref_post.id }}</a
90 90 >{% if not forloop.last %},{% endif %}
91 91 {% endfor %}
92 92 </div>
93 93 {% endif %}
94 94 </div>
95 95 <div class="metadata">
96 96 {{ thread.thread.get_images_count }} {% trans 'images' %},
97 97 {{ thread.thread.get_reply_count }} {% trans 'replies' %}.
98 98 {% if thread.thread.tags %}
99 99 <span class="tags">
100 100 {% for tag in thread.thread.get_tags %}
101 101 <a class="tag" href="
102 102 {% url 'tag' tag_name=tag.name %}">
103 103 #{{ tag.name }}</a
104 104 >{% if not forloop.last %},{% endif %}
105 105 {% endfor %}
106 106 </span>
107 107 {% endif %}
108 108 </div>
109 109 </div>
110 110 </div>
111 111 {% endcache %}
112 112 {% endfor %}
113 113
114 114 {% if next_page %}
115 115 <div class="page_link">
116 116 <a href="{% url "archive" page=next_page %}">{% trans "Next page" %}</a>
117 117 </div>
118 118 {% endif %}
119 119 {% else %}
120 120 <div class="post">
121 121 {% trans 'No threads exist. Create the first one!' %}</div>
122 122 {% endif %}
123 123
124 124 {% endblock %}
125 125
126 126 {% block metapanel %}
127 127
128 128 <span class="metapanel">
129 129 <b><a href="{% url "authors" %}">Neboard</a> 1.5 Aker</b>
130 130 {% trans "Pages:" %}[
131 131 {% for page in pages %}
132 132 <a
133 133 {% ifequal page current_page %}
134 134 class="current_page"
135 135 {% endifequal %}
136 136 href="
137 137 {% url "archive" page=page %}
138 138 ">{{ page }}</a>
139 139 {% if not forloop.last %},{% endif %}
140 140 {% endfor %}
141 141 ]
142 142 [<a href="rss/">RSS</a>]
143 143 </span>
144 144
145 145 {% endblock %}
General Comments 0
You need to be logged in to leave comments. Login now