##// END OF EJS Templates
Fixed issue in rendering post without having a request
neko259 -
r1117:899891b1 default
parent child Browse files
Show More
@@ -1,426 +1,426
1 1 from datetime import datetime, timedelta, date
2 2 from datetime import time as dtime
3 3 import logging
4 4 import re
5 5
6 6 from django.core.exceptions import ObjectDoesNotExist
7 7 from django.core.urlresolvers import reverse
8 8 from django.db import models, transaction
9 9 from django.db.models import TextField
10 10 from django.template.loader import render_to_string
11 11 from django.utils import timezone
12 12
13 13 from boards import settings
14 14 from boards.mdx_neboard import Parser
15 15 from boards.models import PostImage
16 16 from boards.models.base import Viewable
17 17 from boards import utils
18 18 from boards.models.user import Notification, Ban
19 19 import boards.models.thread
20 20
21 21
22 22 APP_LABEL_BOARDS = 'boards'
23 23
24 24 POSTS_PER_DAY_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 = 200
31 31
32 32 # TODO This should be removed
33 33 NO_IP = '0.0.0.0'
34 34
35 35 REGEX_REPLY = re.compile(r'\[post\](\d+)\[/post\]')
36 36 REGEX_NOTIFICATION = re.compile(r'\[user\](\w+)\[/user\]')
37 37
38 38 PARAMETER_TRUNCATED = 'truncated'
39 39 PARAMETER_TAG = 'tag'
40 40 PARAMETER_OFFSET = 'offset'
41 41 PARAMETER_DIFF_TYPE = 'type'
42 42 PARAMETER_CSS_CLASS = 'css_class'
43 43 PARAMETER_THREAD = 'thread'
44 44 PARAMETER_IS_OPENING = 'is_opening'
45 45 PARAMETER_MODERATOR = 'moderator'
46 46 PARAMETER_POST = 'post'
47 47 PARAMETER_OP_ID = 'opening_post_id'
48 48 PARAMETER_NEED_OPEN_LINK = 'need_open_link'
49 49 PARAMETER_REPLY_LINK = 'reply_link'
50 50
51 51 DIFF_TYPE_HTML = 'html'
52 52 DIFF_TYPE_JSON = 'json'
53 53
54 54 REFMAP_STR = '<a href="{}">&gt;&gt;{}</a>'
55 55
56 56
57 57 class PostManager(models.Manager):
58 58 @transaction.atomic
59 59 def create_post(self, title: str, text: str, image=None, thread=None,
60 60 ip=NO_IP, tags: list=None, threads: list=None):
61 61 """
62 62 Creates new post
63 63 """
64 64
65 65 is_banned = Ban.objects.filter(ip=ip).exists()
66 66
67 67 # TODO Raise specific exception and catch it in the views
68 68 if is_banned:
69 69 raise Exception("This user is banned")
70 70
71 71 if not tags:
72 72 tags = []
73 73 if not threads:
74 74 threads = []
75 75
76 76 posting_time = timezone.now()
77 77 if not thread:
78 78 thread = boards.models.thread.Thread.objects.create(
79 79 bump_time=posting_time, last_edit_time=posting_time)
80 80 new_thread = True
81 81 else:
82 82 new_thread = False
83 83
84 84 pre_text = Parser().preparse(text)
85 85
86 86 post = self.create(title=title,
87 87 text=pre_text,
88 88 pub_time=posting_time,
89 89 poster_ip=ip,
90 90 thread=thread,
91 91 last_edit_time=posting_time)
92 92 post.threads.add(thread)
93 93
94 94 logger = logging.getLogger('boards.post.create')
95 95
96 96 logger.info('Created post {} by {}'.format(post, post.poster_ip))
97 97
98 98 if image:
99 99 post.images.add(PostImage.objects.create_with_hash(image))
100 100
101 101 list(map(thread.add_tag, tags))
102 102
103 103 if new_thread:
104 104 boards.models.thread.Thread.objects.process_oldest_threads()
105 105 else:
106 106 thread.last_edit_time = posting_time
107 107 thread.bump()
108 108 thread.save()
109 109
110 110 post.connect_replies()
111 111 post.connect_threads(threads)
112 112 post.connect_notifications()
113 113
114 114 post.build_url()
115 115
116 116 return post
117 117
118 118 def delete_posts_by_ip(self, ip):
119 119 """
120 120 Deletes all posts of the author with same IP
121 121 """
122 122
123 123 posts = self.filter(poster_ip=ip)
124 124 for post in posts:
125 125 post.delete()
126 126
127 127 @utils.cached_result()
128 128 def get_posts_per_day(self) -> float:
129 129 """
130 130 Gets average count of posts per day for the last 7 days
131 131 """
132 132
133 133 day_end = date.today()
134 134 day_start = day_end - timedelta(POSTS_PER_DAY_RANGE)
135 135
136 136 day_time_start = timezone.make_aware(datetime.combine(
137 137 day_start, dtime()), timezone.get_current_timezone())
138 138 day_time_end = timezone.make_aware(datetime.combine(
139 139 day_end, dtime()), timezone.get_current_timezone())
140 140
141 141 posts_per_period = float(self.filter(
142 142 pub_time__lte=day_time_end,
143 143 pub_time__gte=day_time_start).count())
144 144
145 145 ppd = posts_per_period / POSTS_PER_DAY_RANGE
146 146
147 147 return ppd
148 148
149 149
150 150 class Post(models.Model, Viewable):
151 151 """A post is a message."""
152 152
153 153 objects = PostManager()
154 154
155 155 class Meta:
156 156 app_label = APP_LABEL_BOARDS
157 157 ordering = ('id',)
158 158
159 159 title = models.CharField(max_length=TITLE_MAX_LENGTH, null=True, blank=True)
160 160 pub_time = models.DateTimeField()
161 161 text = TextField(blank=True, null=True)
162 162 _text_rendered = TextField(blank=True, null=True, editable=False)
163 163
164 164 images = models.ManyToManyField(PostImage, null=True, blank=True,
165 165 related_name='ip+', db_index=True)
166 166
167 167 poster_ip = models.GenericIPAddressField()
168 168
169 169 last_edit_time = models.DateTimeField()
170 170
171 171 referenced_posts = models.ManyToManyField('Post', symmetrical=False,
172 172 null=True,
173 173 blank=True, related_name='rfp+',
174 174 db_index=True)
175 175 refmap = models.TextField(null=True, blank=True)
176 176 threads = models.ManyToManyField('Thread', db_index=True)
177 177 thread = models.ForeignKey('Thread', db_index=True, related_name='pt+')
178 178 url = models.TextField()
179 179
180 180 def __str__(self):
181 181 return 'P#{}/{}'.format(self.id, self.title)
182 182
183 183 def get_title(self) -> str:
184 184 """
185 185 Gets original post title or part of its text.
186 186 """
187 187
188 188 title = self.title
189 189 if not title:
190 190 title = self.get_text()
191 191
192 192 return title
193 193
194 194 def build_refmap(self) -> None:
195 195 """
196 196 Builds a replies map string from replies list. This is a cache to stop
197 197 the server from recalculating the map on every post show.
198 198 """
199 199
200 200 post_urls = [REFMAP_STR.format(refpost.get_url(), refpost.id)
201 201 for refpost in self.referenced_posts.all()]
202 202
203 203 self.refmap = ', '.join(post_urls)
204 204
205 205 def is_referenced(self) -> bool:
206 206 return self.refmap and len(self.refmap) > 0
207 207
208 208 def is_opening(self) -> bool:
209 209 """
210 210 Checks if this is an opening post or just a reply.
211 211 """
212 212
213 213 return self.get_thread().get_opening_post_id() == self.id
214 214
215 215 def get_url(self):
216 216 return self.url
217 217
218 218 def get_thread(self):
219 219 return self.thread
220 220
221 221 def get_threads(self) -> list:
222 222 """
223 223 Gets post's thread.
224 224 """
225 225
226 226 return self.threads
227 227
228 228 def get_view(self, moderator=False, need_open_link=False,
229 229 truncated=False, reply_link=False, *args, **kwargs) -> str:
230 230 """
231 231 Renders post's HTML view. Some of the post params can be passed over
232 232 kwargs for the means of caching (if we view the thread, some params
233 233 are same for every post and don't need to be computed over and over.
234 234 """
235 235
236 236 thread = self.get_thread()
237 237 is_opening = kwargs.get(PARAMETER_IS_OPENING, self.is_opening())
238 238
239 239 if is_opening:
240 240 opening_post_id = self.id
241 241 else:
242 242 opening_post_id = thread.get_opening_post_id()
243 243
244 244 css_class = 'post'
245 245 if thread.archived:
246 246 css_class += ' archive_post'
247 247 elif not thread.can_bump():
248 248 css_class += ' dead_post'
249 249
250 250 return render_to_string('boards/post.html', {
251 251 PARAMETER_POST: self,
252 252 PARAMETER_MODERATOR: moderator,
253 253 PARAMETER_IS_OPENING: is_opening,
254 254 PARAMETER_THREAD: thread,
255 255 PARAMETER_CSS_CLASS: css_class,
256 256 PARAMETER_NEED_OPEN_LINK: need_open_link,
257 257 PARAMETER_TRUNCATED: truncated,
258 258 PARAMETER_OP_ID: opening_post_id,
259 259 PARAMETER_REPLY_LINK: reply_link,
260 260 })
261 261
262 262 def get_search_view(self, *args, **kwargs):
263 263 return self.get_view(args, kwargs)
264 264
265 265 def get_first_image(self) -> PostImage:
266 266 return self.images.earliest('id')
267 267
268 268 def delete(self, using=None):
269 269 """
270 270 Deletes all post images and the post itself.
271 271 """
272 272
273 273 for image in self.images.all():
274 274 image_refs_count = Post.objects.filter(images__in=[image]).count()
275 275 if image_refs_count == 1:
276 276 image.delete()
277 277
278 278 thread = self.get_thread()
279 279 thread.last_edit_time = timezone.now()
280 280 thread.save()
281 281
282 282 super(Post, self).delete(using)
283 283
284 284 logging.getLogger('boards.post.delete').info(
285 285 'Deleted post {}'.format(self))
286 286
287 287 # TODO Implement this with OOP, e.g. use the factory and HtmlPostData class
288 288 def get_post_data(self, format_type=DIFF_TYPE_JSON, request=None,
289 289 include_last_update=False) -> str:
290 290 """
291 291 Gets post HTML or JSON data that can be rendered on a page or used by
292 292 API.
293 293 """
294 294
295 295 if format_type == DIFF_TYPE_HTML:
296 if PARAMETER_TRUNCATED in request.GET:
296 if request is not None and PARAMETER_TRUNCATED in request.GET:
297 297 truncated = True
298 298 reply_link = False
299 299 else:
300 300 truncated = False
301 301 reply_link = True
302 302
303 303 return self.get_view(truncated=truncated, reply_link=reply_link,
304 304 moderator=utils.is_moderator(request))
305 305 elif format_type == DIFF_TYPE_JSON:
306 306 post_json = {
307 307 'id': self.id,
308 308 'title': self.title,
309 309 'text': self._text_rendered,
310 310 }
311 311 if self.images.exists():
312 312 post_image = self.get_first_image()
313 313 post_json['image'] = post_image.image.url
314 314 post_json['image_preview'] = post_image.image.url_200x150
315 315 if include_last_update:
316 316 post_json['bump_time'] = utils.datetime_to_epoch(
317 317 self.get_thread().bump_time)
318 318 return post_json
319 319
320 320 def notify_clients(self, recursive=True):
321 321 """
322 322 Sends post HTML data to the thread web socket.
323 323 """
324 324
325 325 if not settings.WEBSOCKETS_ENABLED:
326 326 return
327 327
328 328 thread_ids = list()
329 329 for thread in self.get_threads().all():
330 330 thread_ids.append(thread.id)
331 331
332 332 thread.notify_clients()
333 333
334 334 if recursive:
335 335 for reply_number in re.finditer(REGEX_REPLY, self.get_raw_text()):
336 336 post_id = reply_number.group(1)
337 337
338 338 try:
339 339 ref_post = Post.objects.get(id=post_id)
340 340
341 341 if ref_post.get_threads().exclude(id__in=thread_ids).exists():
342 342 # If post is in this thread, its thread was already notified.
343 343 # Otherwise, notify its thread separately.
344 344 ref_post.notify_clients(recursive=False)
345 345 except ObjectDoesNotExist:
346 346 pass
347 347
348 348 def build_url(self):
349 349 thread = self.get_thread()
350 350 opening_id = thread.get_opening_post_id()
351 351 post_url = reverse('thread', kwargs={'post_id': opening_id})
352 352 if self.id != opening_id:
353 353 post_url += '#' + str(self.id)
354 354 self.url = post_url
355 355 self.save(update_fields=['url'])
356 356
357 357 def save(self, force_insert=False, force_update=False, using=None,
358 358 update_fields=None):
359 359 self._text_rendered = Parser().parse(self.get_raw_text())
360 360
361 361 if self.id:
362 362 for thread in self.get_threads().all():
363 363 if thread.can_bump():
364 364 thread.update_bump_status()
365 365 thread.last_edit_time = self.last_edit_time
366 366
367 367 thread.save(update_fields=['last_edit_time', 'bumpable'])
368 368
369 369 super().save(force_insert, force_update, using, update_fields)
370 370
371 371 def get_text(self) -> str:
372 372 return self._text_rendered
373 373
374 374 def get_raw_text(self) -> str:
375 375 return self.text
376 376
377 377 def get_absolute_id(self) -> str:
378 378 """
379 379 If the post has many threads, shows its main thread OP id in the post
380 380 ID.
381 381 """
382 382
383 383 if self.get_threads().count() > 1:
384 384 return '{}/{}'.format(self.get_thread().get_opening_post_id(), self.id)
385 385 else:
386 386 return str(self.id)
387 387
388 388 def connect_notifications(self):
389 389 for reply_number in re.finditer(REGEX_NOTIFICATION, self.get_raw_text()):
390 390 user_name = reply_number.group(1).lower()
391 391 Notification.objects.get_or_create(name=user_name, post=self)
392 392
393 393 def connect_replies(self):
394 394 """
395 395 Connects replies to a post to show them as a reflink map
396 396 """
397 397
398 398 for reply_number in re.finditer(REGEX_REPLY, self.get_raw_text()):
399 399 post_id = reply_number.group(1)
400 400
401 401 try:
402 402 referenced_post = Post.objects.get(id=post_id)
403 403
404 404 referenced_post.referenced_posts.add(self)
405 405 referenced_post.last_edit_time = self.pub_time
406 406 referenced_post.build_refmap()
407 407 referenced_post.save(update_fields=['refmap', 'last_edit_time'])
408 408 except ObjectDoesNotExist:
409 409 pass
410 410
411 411 def connect_threads(self, opening_posts):
412 412 """
413 413 If the referenced post is an OP in another thread,
414 414 make this post multi-thread.
415 415 """
416 416
417 417 for opening_post in opening_posts:
418 418 threads = opening_post.get_threads().all()
419 419 for thread in threads:
420 420 if thread.can_bump():
421 421 thread.update_bump_status()
422 422
423 423 thread.last_edit_time = self.last_edit_time
424 424 thread.save(update_fields=['last_edit_time', 'bumpable'])
425 425
426 426 self.threads.add(thread)
@@ -1,240 +1,238
1 from datetime import datetime
2 1 import json
3 2 import logging
3
4 4 from django.db import transaction
5 5 from django.http import HttpResponse
6 from django.shortcuts import get_object_or_404, render
7 from django.template import RequestContext
8 from django.utils import timezone
6 from django.shortcuts import get_object_or_404
9 7 from django.core import serializers
10 8
11 9 from boards.forms import PostForm, PlainErrorList
12 10 from boards.models import Post, Thread, Tag
13 11 from boards.utils import datetime_to_epoch
14 12 from boards.views.thread import ThreadView
15 13 from boards.models.user import Notification
16 14
15
17 16 __author__ = 'neko259'
18 17
19 18 PARAMETER_TRUNCATED = 'truncated'
20 19 PARAMETER_TAG = 'tag'
21 20 PARAMETER_OFFSET = 'offset'
22 21 PARAMETER_DIFF_TYPE = 'type'
23 22 PARAMETER_POST = 'post'
24 23 PARAMETER_ADDED = 'added'
25 24 PARAMETER_UPDATED = 'updated'
26 25 PARAMETER_LAST_UPDATE = 'last_update'
27 26
28 27 DIFF_TYPE_HTML = 'html'
29 28 DIFF_TYPE_JSON = 'json'
30 29
31 30 STATUS_OK = 'ok'
32 31 STATUS_ERROR = 'error'
33 32
34 33 logger = logging.getLogger(__name__)
35 34
36 35
37 36 @transaction.atomic
38 37 def api_get_threaddiff(request):
39 38 """
40 39 Gets posts that were changed or added since time
41 40 """
42 41
43 42 thread_id = request.GET.get('thread')
44 43 last_update_time = request.GET.get('last_update')
45 44 last_post = request.GET.get('last_post')
46 45
47 46 thread = get_object_or_404(Post, id=thread_id).get_thread()
48 47
49 48 json_data = {
50 49 PARAMETER_ADDED: [],
51 50 PARAMETER_UPDATED: [],
52 51 'last_update': None,
53 52 }
54 53 added_posts = Post.objects.filter(threads__in=[thread],
55 54 id__gt=int(last_post)) \
56 55 .order_by('pub_time')
57 56 updated_posts = Post.objects.filter(threads__in=[thread],
58 57 pub_time__lte=last_update_time,
59 58 last_edit_time__gt=last_update_time)
60 59
61 60 diff_type = request.GET.get(PARAMETER_DIFF_TYPE, DIFF_TYPE_HTML)
62 61
63 62 for post in added_posts:
64 63 json_data[PARAMETER_ADDED].append(get_post_data(post.id, diff_type, request))
65 64 for post in updated_posts:
66 65 json_data[PARAMETER_UPDATED].append(get_post_data(post.id, diff_type, request))
67 66 json_data[PARAMETER_LAST_UPDATE] = str(thread.last_edit_time)
68 67
69 68 return HttpResponse(content=json.dumps(json_data))
70 69
71 70
72 71 def api_add_post(request, opening_post_id):
73 72 """
74 73 Adds a post and return the JSON response for it
75 74 """
76 75
77 76 opening_post = get_object_or_404(Post, id=opening_post_id)
78 77
79 78 logger.info('Adding post via api...')
80 79
81 80 status = STATUS_OK
82 81 errors = []
83 82
84 83 if request.method == 'POST':
85 84 form = PostForm(request.POST, request.FILES, error_class=PlainErrorList)
86 85 form.session = request.session
87 86
88 87 if form.need_to_ban:
89 88 # Ban user because he is suspected to be a bot
90 89 # _ban_current_user(request)
91 90 status = STATUS_ERROR
92 91 if form.is_valid():
93 92 post = ThreadView().new_post(request, form, opening_post,
94 93 html_response=False)
95 94 if not post:
96 95 status = STATUS_ERROR
97 96 else:
98 97 logger.info('Added post #%d via api.' % post.id)
99 98 else:
100 99 status = STATUS_ERROR
101 100 errors = form.as_json_errors()
102 101
103 102 response = {
104 103 'status': status,
105 104 'errors': errors,
106 105 }
107 106
108 107 return HttpResponse(content=json.dumps(response))
109 108
110 109
111 110 def get_post(request, post_id):
112 111 """
113 112 Gets the html of a post. Used for popups. Post can be truncated if used
114 113 in threads list with 'truncated' get parameter.
115 114 """
116 115
117 116 post = get_object_or_404(Post, id=post_id)
118 117 truncated = PARAMETER_TRUNCATED in request.GET
119 118
120 119 return HttpResponse(content=post.get_view(truncated=truncated))
121 120
122 121
123 122 def api_get_threads(request, count):
124 123 """
125 124 Gets the JSON thread opening posts list.
126 125 Parameters that can be used for filtering:
127 126 tag, offset (from which thread to get results)
128 127 """
129 128
130 129 if PARAMETER_TAG in request.GET:
131 130 tag_name = request.GET[PARAMETER_TAG]
132 131 if tag_name is not None:
133 132 tag = get_object_or_404(Tag, name=tag_name)
134 133 threads = tag.get_threads().filter(archived=False)
135 134 else:
136 135 threads = Thread.objects.filter(archived=False)
137 136
138 137 if PARAMETER_OFFSET in request.GET:
139 138 offset = request.GET[PARAMETER_OFFSET]
140 139 offset = int(offset) if offset is not None else 0
141 140 else:
142 141 offset = 0
143 142
144 143 threads = threads.order_by('-bump_time')
145 144 threads = threads[offset:offset + int(count)]
146 145
147 146 opening_posts = []
148 147 for thread in threads:
149 148 opening_post = thread.get_opening_post()
150 149
151 150 # TODO Add tags, replies and images count
152 151 post_data = get_post_data(opening_post.id, include_last_update=True)
153 152 post_data['bumpable'] = thread.can_bump()
154 153 post_data['archived'] = thread.archived
155 154
156 155 opening_posts.append(post_data)
157 156
158 157 return HttpResponse(content=json.dumps(opening_posts))
159 158
160 159
161 160 # TODO Test this
162 161 def api_get_tags(request):
163 162 """
164 163 Gets all tags or user tags.
165 164 """
166 165
167 166 # TODO Get favorite tags for the given user ID
168 167
169 168 tags = Tag.objects.get_not_empty_tags()
170 169
171 170 term = request.GET.get('term')
172 171 if term is not None:
173 172 tags = tags.filter(name__contains=term)
174 173
175 174 tag_names = [tag.name for tag in tags]
176 175
177 176 return HttpResponse(content=json.dumps(tag_names))
178 177
179 178
180 179 # TODO The result can be cached by the thread last update time
181 180 # TODO Test this
182 181 def api_get_thread_posts(request, opening_post_id):
183 182 """
184 183 Gets the JSON array of thread posts
185 184 """
186 185
187 186 opening_post = get_object_or_404(Post, id=opening_post_id)
188 187 thread = opening_post.get_thread()
189 188 posts = thread.get_replies()
190 189
191 190 json_data = {
192 191 'posts': [],
193 192 'last_update': None,
194 193 }
195 194 json_post_list = []
196 195
197 196 for post in posts:
198 197 json_post_list.append(get_post_data(post.id))
199 198 json_data['last_update'] = datetime_to_epoch(thread.last_edit_time)
200 199 json_data['posts'] = json_post_list
201 200
202 201 return HttpResponse(content=json.dumps(json_data))
203 202
204 203
205 204 def api_get_notifications(request, username):
206 205 last_notification_id_str = request.GET.get('last', None)
207 206 last_id = int(last_notification_id_str) if last_notification_id_str is not None else None
208 207
209 208 posts = Notification.objects.get_notification_posts(username=username,
210 last=last_id)
209 last=last_id)
211 210
212 211 json_post_list = []
213 212 for post in posts:
214 213 json_post_list.append(get_post_data(post.id))
215 214 return HttpResponse(content=json.dumps(json_post_list))
216 215
217 216
218
219 217 def api_get_post(request, post_id):
220 218 """
221 219 Gets the JSON of a post. This can be
222 220 used as and API for external clients.
223 221 """
224 222
225 223 post = get_object_or_404(Post, id=post_id)
226 224
227 225 json = serializers.serialize("json", [post], fields=(
228 226 "pub_time", "_text_rendered", "title", "text", "image",
229 227 "image_width", "image_height", "replies", "tags"
230 228 ))
231 229
232 230 return HttpResponse(content=json)
233 231
234 232
235 233 # TODO Remove this method and use post method directly
236 234 def get_post_data(post_id, format_type=DIFF_TYPE_JSON, request=None,
237 235 include_last_update=False):
238 236 post = get_object_or_404(Post, id=post_id)
239 237 return post.get_post_data(format_type=format_type, request=request,
240 238 include_last_update=include_last_update)
General Comments 0
You need to be logged in to leave comments. Login now