##// END OF EJS Templates
Added notification API
neko259 -
r994:e93bc5ac default
parent child Browse files
Show More
@@ -1,69 +1,65
1 1 from boards.abstracts.settingsmanager import get_settings_manager, \
2 2 SETTING_USERNAME, SETTING_LAST_NOTIFICATION_ID
3 3 from boards.models.user import Notification
4 4
5 5 __author__ = 'neko259'
6 6
7 7 from boards import settings
8 8 from boards.models import Post
9 9
10 10 CONTEXT_SITE_NAME = 'site_name'
11 11 CONTEXT_VERSION = 'version'
12 12 CONTEXT_MODERATOR = 'moderator'
13 13 CONTEXT_THEME_CSS = 'theme_css'
14 14 CONTEXT_THEME = 'theme'
15 15 CONTEXT_PPD = 'posts_per_day'
16 16 CONTEXT_TAGS = 'tags'
17 17 CONTEXT_USER = 'user'
18 18 CONTEXT_NEW_NOTIFICATIONS_COUNT = 'new_notifications_count'
19 19 CONTEXT_USERNAME = 'username'
20 20
21 21 PERMISSION_MODERATE = 'moderation'
22 22
23 23
24 24 def get_notifications(context, request):
25 25 settings_manager = get_settings_manager(request)
26 26 username = settings_manager.get_setting(SETTING_USERNAME)
27 27 new_notifications_count = 0
28 28 if username is not None and len(username) > 0:
29 29 last_notification_id = settings_manager.get_setting(
30 30 SETTING_LAST_NOTIFICATION_ID)
31 if last_notification_id is not None:
32 new_notifications_count = Notification.objects.filter(
33 id__gt=last_notification_id).filter(
34 name=username).count()
35 else:
36 new_notifications_count = Notification.objects.filter(
37 name=username).count()
31
32 new_notifications_count = Notification.objects.get_notification_posts(
33 username=username, last=last_notification_id).count()
38 34 context[CONTEXT_NEW_NOTIFICATIONS_COUNT] = new_notifications_count
39 35 context[CONTEXT_USERNAME] = username
40 36
41 37
42 38 def get_moderator_permissions(context, request):
43 39 try:
44 40 moderate = request.user.has_perm(PERMISSION_MODERATE)
45 41 except AttributeError:
46 42 moderate = False
47 43 context[CONTEXT_MODERATOR] = moderate
48 44
49 45
50 46 def user_and_ui_processor(request):
51 47 context = dict()
52 48
53 49 context[CONTEXT_PPD] = float(Post.objects.get_posts_per_day())
54 50
55 51 settings_manager = get_settings_manager(request)
56 52 context[CONTEXT_TAGS] = settings_manager.get_fav_tags()
57 53 theme = settings_manager.get_theme()
58 54 context[CONTEXT_THEME] = theme
59 55 context[CONTEXT_THEME_CSS] = 'css/' + theme + '/base_page.css'
60 56
61 57 # This shows the moderator panel
62 58 get_moderator_permissions(context, request)
63 59
64 60 context[CONTEXT_VERSION] = settings.VERSION
65 61 context[CONTEXT_SITE_NAME] = settings.SITE_NAME
66 62
67 63 get_notifications(context, request)
68 64
69 65 return context
@@ -1,465 +1,465
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 urllib.parse import unquote
7 7
8 8 from adjacent import Client
9 9 from django.core.urlresolvers import reverse
10 10 from django.db import models, transaction
11 11 from django.db.models import TextField
12 12 from django.template.loader import render_to_string
13 13 from django.utils import timezone
14 14
15 15 from boards import settings
16 16 from boards.mdx_neboard import bbcode_extended
17 17 from boards.models import PostImage
18 18 from boards.models.base import Viewable
19 from boards.utils import datetime_to_epoch, cached_result
19 20 from boards.models.user import Notification
20 from boards.utils import datetime_to_epoch, cached_result
21 21 import boards.models.thread
22 22
23 23
24 24 WS_NOTIFICATION_TYPE_NEW_POST = 'new_post'
25 25 WS_NOTIFICATION_TYPE = 'notification_type'
26 26
27 27 WS_CHANNEL_THREAD = "thread:"
28 28
29 29 APP_LABEL_BOARDS = 'boards'
30 30
31 31 POSTS_PER_DAY_RANGE = 7
32 32
33 33 BAN_REASON_AUTO = 'Auto'
34 34
35 35 IMAGE_THUMB_SIZE = (200, 150)
36 36
37 37 TITLE_MAX_LENGTH = 200
38 38
39 39 # TODO This should be removed
40 40 NO_IP = '0.0.0.0'
41 41
42 42 # TODO Real user agent should be saved instead of this
43 43 UNKNOWN_UA = ''
44 44
45 45 REGEX_REPLY = re.compile(r'\[post\](\d+)\[/post\]')
46 46 REGEX_URL = re.compile(r'https?\://[a-zA-Z0-9\-\.]+\.[a-zA-Z]{2,3}(/\S*)?')
47 47 REGEX_NOTIFICATION = re.compile(r'\[user\](\w+)\[/user\]')
48 48
49 49 PARAMETER_TRUNCATED = 'truncated'
50 50 PARAMETER_TAG = 'tag'
51 51 PARAMETER_OFFSET = 'offset'
52 52 PARAMETER_DIFF_TYPE = 'type'
53 53 PARAMETER_BUMPABLE = 'bumpable'
54 54 PARAMETER_THREAD = 'thread'
55 55 PARAMETER_IS_OPENING = 'is_opening'
56 56 PARAMETER_MODERATOR = 'moderator'
57 57 PARAMETER_POST = 'post'
58 58 PARAMETER_OP_ID = 'opening_post_id'
59 59 PARAMETER_NEED_OPEN_LINK = 'need_open_link'
60 60
61 61 DIFF_TYPE_HTML = 'html'
62 62 DIFF_TYPE_JSON = 'json'
63 63
64 64 PREPARSE_PATTERNS = {
65 65 r'>>(\d+)': r'[post]\1[/post]', # Reflink ">>123"
66 66 r'^>([^>].+)': r'[quote]\1[/quote]', # Quote ">text"
67 67 r'^//(.+)': r'[comment]\1[/comment]', # Comment "//text"
68 68 r'@(\w+)': r'[user]\1[/user]', # User notification "@user"
69 69 }
70 70
71 71
72 72 class PostManager(models.Manager):
73 73 @transaction.atomic
74 74 def create_post(self, title: str, text: str, image=None, thread=None,
75 75 ip=NO_IP, tags: list=None):
76 76 """
77 77 Creates new post
78 78 """
79 79
80 80 if not tags:
81 81 tags = []
82 82
83 83 posting_time = timezone.now()
84 84 if not thread:
85 85 thread = boards.models.thread.Thread.objects.create(
86 86 bump_time=posting_time, last_edit_time=posting_time)
87 87 new_thread = True
88 88 else:
89 89 new_thread = False
90 90
91 91 pre_text = self._preparse_text(text)
92 92
93 93 post = self.create(title=title,
94 94 text=pre_text,
95 95 pub_time=posting_time,
96 96 poster_ip=ip,
97 97 thread=thread,
98 98 poster_user_agent=UNKNOWN_UA, # TODO Get UA at
99 99 # last!
100 100 last_edit_time=posting_time)
101 101 post.threads.add(thread)
102 102
103 103 logger = logging.getLogger('boards.post.create')
104 104
105 105 logger.info('Created post {} by {}'.format(
106 106 post, post.poster_ip))
107 107
108 108 if image:
109 109 # Try to find existing image. If it exists, assign it to the post
110 110 # instead of createing the new one
111 111 image_hash = PostImage.get_hash(image)
112 112 existing = PostImage.objects.filter(hash=image_hash)
113 113 if len(existing) > 0:
114 114 post_image = existing[0]
115 115 else:
116 116 post_image = PostImage.objects.create(image=image)
117 117 logger.info('Created new image #{} for post #{}'.format(
118 118 post_image.id, post.id))
119 119 post.images.add(post_image)
120 120
121 121 list(map(thread.add_tag, tags))
122 122
123 123 if new_thread:
124 124 boards.models.thread.Thread.objects.process_oldest_threads()
125 125 else:
126 126 thread.bump()
127 127 thread.last_edit_time = posting_time
128 128 thread.save()
129 129
130 130 self.connect_replies(post)
131 131 post.connect_notifications()
132 132
133 133 return post
134 134
135 135 def delete_posts_by_ip(self, ip):
136 136 """
137 137 Deletes all posts of the author with same IP
138 138 """
139 139
140 140 posts = self.filter(poster_ip=ip)
141 141 for post in posts:
142 142 post.delete()
143 143
144 144 # TODO This may be a method in the post
145 145 def connect_replies(self, post):
146 146 """
147 147 Connects replies to a post to show them as a reflink map
148 148 """
149 149
150 150 for reply_number in re.finditer(REGEX_REPLY, post.get_raw_text()):
151 151 post_id = reply_number.group(1)
152 152 ref_post = self.filter(id=post_id)
153 153 if ref_post.count() > 0:
154 154 referenced_post = ref_post[0]
155 155 referenced_post.referenced_posts.add(post)
156 156 referenced_post.last_edit_time = post.pub_time
157 157 referenced_post.build_refmap()
158 158 referenced_post.save(update_fields=['refmap', 'last_edit_time'])
159 159
160 160 referenced_threads = referenced_post.get_threads().all()
161 161 for thread in referenced_threads:
162 162 thread.last_edit_time = post.pub_time
163 163 thread.save(update_fields=['last_edit_time'])
164 164
165 165 post.threads.add(thread)
166 166
167 167 @cached_result
168 168 def get_posts_per_day(self):
169 169 """
170 170 Gets average count of posts per day for the last 7 days
171 171 """
172 172
173 173 day_end = date.today()
174 174 day_start = day_end - timedelta(POSTS_PER_DAY_RANGE)
175 175
176 176 day_time_start = timezone.make_aware(datetime.combine(
177 177 day_start, dtime()), timezone.get_current_timezone())
178 178 day_time_end = timezone.make_aware(datetime.combine(
179 179 day_end, dtime()), timezone.get_current_timezone())
180 180
181 181 posts_per_period = float(self.filter(
182 182 pub_time__lte=day_time_end,
183 183 pub_time__gte=day_time_start).count())
184 184
185 185 ppd = posts_per_period / POSTS_PER_DAY_RANGE
186 186
187 187 return ppd
188 188
189 189 def _preparse_text(self, text: str) -> str:
190 190 """
191 191 Preparses text to change patterns like '>>' to a proper bbcode
192 192 tags.
193 193 """
194 194
195 195 for key, value in PREPARSE_PATTERNS.items():
196 196 text = re.sub(key, value, text, flags=re.MULTILINE)
197 197
198 198 for link in REGEX_URL.findall(text):
199 199 text = text.replace(link, unquote(link))
200 200
201 201 return text
202 202
203 203
204 204 class Post(models.Model, Viewable):
205 205 """A post is a message."""
206 206
207 207 objects = PostManager()
208 208
209 209 class Meta:
210 210 app_label = APP_LABEL_BOARDS
211 211 ordering = ('id',)
212 212
213 213 title = models.CharField(max_length=TITLE_MAX_LENGTH)
214 214 pub_time = models.DateTimeField()
215 215 text = TextField(blank=True, null=True)
216 216 _text_rendered = TextField(blank=True, null=True, editable=False)
217 217
218 218 images = models.ManyToManyField(PostImage, null=True, blank=True,
219 219 related_name='ip+', db_index=True)
220 220
221 221 poster_ip = models.GenericIPAddressField()
222 222 poster_user_agent = models.TextField()
223 223
224 224 last_edit_time = models.DateTimeField()
225 225
226 226 referenced_posts = models.ManyToManyField('Post', symmetrical=False,
227 227 null=True,
228 228 blank=True, related_name='rfp+',
229 229 db_index=True)
230 230 refmap = models.TextField(null=True, blank=True)
231 231 threads = models.ManyToManyField('Thread', db_index=True)
232 232 thread = models.ForeignKey('Thread', db_index=True, related_name='pt+')
233 233
234 234 def __str__(self):
235 235 return 'P#{}/{}'.format(self.id, self.title)
236 236
237 237 def get_title(self) -> str:
238 238 """
239 239 Gets original post title or part of its text.
240 240 """
241 241
242 242 title = self.title
243 243 if not title:
244 244 title = self.get_text()
245 245
246 246 return title
247 247
248 248 def build_refmap(self) -> None:
249 249 """
250 250 Builds a replies map string from replies list. This is a cache to stop
251 251 the server from recalculating the map on every post show.
252 252 """
253 253 map_string = ''
254 254
255 255 first = True
256 256 for refpost in self.referenced_posts.all():
257 257 if not first:
258 258 map_string += ', '
259 259 map_string += '<a href="%s">&gt;&gt;%s</a>' % (refpost.get_url(),
260 260 refpost.id)
261 261 first = False
262 262
263 263 self.refmap = map_string
264 264
265 265 def get_sorted_referenced_posts(self):
266 266 return self.refmap
267 267
268 268 def is_referenced(self) -> bool:
269 269 if not self.refmap:
270 270 return False
271 271 else:
272 272 return len(self.refmap) > 0
273 273
274 274 def is_opening(self) -> bool:
275 275 """
276 276 Checks if this is an opening post or just a reply.
277 277 """
278 278
279 279 return self.get_thread().get_opening_post_id() == self.id
280 280
281 281 @transaction.atomic
282 282 def add_tag(self, tag):
283 283 edit_time = timezone.now()
284 284
285 285 thread = self.get_thread()
286 286 thread.add_tag(tag)
287 287 self.last_edit_time = edit_time
288 288 self.save(update_fields=['last_edit_time'])
289 289
290 290 thread.last_edit_time = edit_time
291 291 thread.save(update_fields=['last_edit_time'])
292 292
293 293 @cached_result
294 294 def get_url(self):
295 295 """
296 296 Gets full url to the post.
297 297 """
298 298
299 299 thread = self.get_thread()
300 300
301 301 opening_id = thread.get_opening_post_id()
302 302
303 303 if self.id != opening_id:
304 304 link = reverse('thread', kwargs={
305 305 'post_id': opening_id}) + '#' + str(self.id)
306 306 else:
307 307 link = reverse('thread', kwargs={'post_id': self.id})
308 308
309 309 return link
310 310
311 311 def get_thread(self):
312 312 return self.thread
313 313
314 314 def get_threads(self):
315 315 """
316 316 Gets post's thread.
317 317 """
318 318
319 319 return self.threads
320 320
321 321 def get_referenced_posts(self):
322 322 return self.referenced_posts.only('id', 'threads')
323 323
324 324 def get_view(self, moderator=False, need_open_link=False,
325 325 truncated=False, *args, **kwargs):
326 326 """
327 327 Renders post's HTML view. Some of the post params can be passed over
328 328 kwargs for the means of caching (if we view the thread, some params
329 329 are same for every post and don't need to be computed over and over.
330 330 """
331 331
332 332 thread = self.get_thread()
333 333 is_opening = kwargs.get(PARAMETER_IS_OPENING, self.is_opening())
334 334 can_bump = kwargs.get(PARAMETER_BUMPABLE, thread.can_bump())
335 335
336 336 if is_opening:
337 337 opening_post_id = self.id
338 338 else:
339 339 opening_post_id = thread.get_opening_post_id()
340 340
341 341 return render_to_string('boards/post.html', {
342 342 PARAMETER_POST: self,
343 343 PARAMETER_MODERATOR: moderator,
344 344 PARAMETER_IS_OPENING: is_opening,
345 345 PARAMETER_THREAD: thread,
346 346 PARAMETER_BUMPABLE: can_bump,
347 347 PARAMETER_NEED_OPEN_LINK: need_open_link,
348 348 PARAMETER_TRUNCATED: truncated,
349 349 PARAMETER_OP_ID: opening_post_id,
350 350 })
351 351
352 352 def get_search_view(self, *args, **kwargs):
353 353 return self.get_view(args, kwargs)
354 354
355 355 def get_first_image(self) -> PostImage:
356 356 return self.images.earliest('id')
357 357
358 358 def delete(self, using=None):
359 359 """
360 360 Deletes all post images and the post itself.
361 361 """
362 362
363 363 for image in self.images.all():
364 364 image_refs_count = Post.objects.filter(images__in=[image]).count()
365 365 if image_refs_count == 1:
366 366 image.delete()
367 367
368 368 thread = self.get_thread()
369 369 thread.last_edit_time = timezone.now()
370 370 thread.save()
371 371
372 372 super(Post, self).delete(using)
373 373
374 374 logging.getLogger('boards.post.delete').info(
375 375 'Deleted post {}'.format(self))
376 376
377 377 def get_post_data(self, format_type=DIFF_TYPE_JSON, request=None,
378 378 include_last_update=False):
379 379 """
380 380 Gets post HTML or JSON data that can be rendered on a page or used by
381 381 API.
382 382 """
383 383
384 384 if format_type == DIFF_TYPE_HTML:
385 385 params = dict()
386 386 params['post'] = self
387 387 if PARAMETER_TRUNCATED in request.GET:
388 388 params[PARAMETER_TRUNCATED] = True
389 389
390 390 return render_to_string('boards/api_post.html', params)
391 391 elif format_type == DIFF_TYPE_JSON:
392 392 post_json = {
393 393 'id': self.id,
394 394 'title': self.title,
395 395 'text': self._text_rendered,
396 396 }
397 397 if self.images.exists():
398 398 post_image = self.get_first_image()
399 399 post_json['image'] = post_image.image.url
400 400 post_json['image_preview'] = post_image.image.url_200x150
401 401 if include_last_update:
402 402 post_json['bump_time'] = datetime_to_epoch(
403 403 self.get_thread().bump_time)
404 404 return post_json
405 405
406 406 def send_to_websocket(self, request, recursive=True):
407 407 """
408 408 Sends post HTML data to the thread web socket.
409 409 """
410 410
411 411 if not settings.WEBSOCKETS_ENABLED:
412 412 return
413 413
414 414 client = Client()
415 415
416 416 thread = self.get_thread()
417 417 thread_id = thread.id
418 418 channel_name = WS_CHANNEL_THREAD + str(thread.get_opening_post_id())
419 419 client.publish(channel_name, {
420 420 WS_NOTIFICATION_TYPE: WS_NOTIFICATION_TYPE_NEW_POST,
421 421 })
422 422 client.send()
423 423
424 424 logger = logging.getLogger('boards.post.websocket')
425 425
426 426 logger.info('Sent notification from post #{} to channel {}'.format(
427 427 self.id, channel_name))
428 428
429 429 if recursive:
430 430 for reply_number in re.finditer(REGEX_REPLY, self.get_raw_text()):
431 431 post_id = reply_number.group(1)
432 432 ref_post = Post.objects.filter(id=post_id)[0]
433 433
434 434 # If post is in this thread, its thread was already notified.
435 435 # Otherwise, notify its thread separately.
436 436 if ref_post.get_thread().id != thread_id:
437 437 ref_post.send_to_websocket(request, recursive=False)
438 438
439 439 def save(self, force_insert=False, force_update=False, using=None,
440 440 update_fields=None):
441 441 self._text_rendered = bbcode_extended(self.get_raw_text())
442 442
443 443 super().save(force_insert, force_update, using, update_fields)
444 444
445 445 def get_text(self) -> str:
446 446 return self._text_rendered
447 447
448 448 def get_raw_text(self) -> str:
449 449 return self.text
450 450
451 451 def get_absolute_id(self) -> str:
452 452 """
453 453 If the post has many threads, shows its main thread OP id in the post
454 454 ID.
455 455 """
456 456
457 457 if self.get_threads().count() > 1:
458 458 return '{}/{}'.format(self.get_thread().get_opening_post_id(), self.id)
459 459 else:
460 460 return str(self.id)
461 461
462 462 def connect_notifications(self):
463 463 for reply_number in re.finditer(REGEX_NOTIFICATION, self.get_raw_text()):
464 464 user_name = reply_number.group(1)
465 465 Notification.objects.get_or_create(name=user_name, post=self)
@@ -1,30 +1,44
1 1 from django.db import models
2 2
3 import boards.models.post
4
3 5 __author__ = 'neko259'
4 6
5 7 BAN_REASON_AUTO = 'Auto'
6 8 BAN_REASON_MAX_LENGTH = 200
7 9
8 10
9 11 class Ban(models.Model):
10 12
11 13 class Meta:
12 14 app_label = 'boards'
13 15
14 16 ip = models.GenericIPAddressField()
15 17 reason = models.CharField(default=BAN_REASON_AUTO,
16 18 max_length=BAN_REASON_MAX_LENGTH)
17 19 can_read = models.BooleanField(default=True)
18 20
19 21 def __str__(self):
20 22 return self.ip
21 23
22 24
25 class NotificationManager(models.Manager):
26 def get_notification_posts(self, username: str, last: int = None):
27 posts = boards.models.post.Post.objects.filter(notification__name=username)
28 if last is not None:
29 posts = posts.filter(id__gt=last)
30 posts = posts.order_by('-id')
31
32 return posts
33
34
23 35 class Notification(models.Model):
24 36
25 37 class Meta:
26 38 app_label = 'boards'
27 39
40 objects = NotificationManager()
41
28 42 post = models.ForeignKey('Post')
29 43 name = models.TextField()
30 44
@@ -1,79 +1,81
1 1 from django.conf.urls import patterns, url, include
2 2 from django.contrib import admin
3 3 from boards import views
4 4 from boards.rss import AllThreadsFeed, TagThreadsFeed, ThreadPostsFeed
5 5 from boards.views import api, tag_threads, all_threads, \
6 6 settings, all_tags
7 7 from boards.views.authors import AuthorsView
8 8 from boards.views.ban import BanUserView
9 9 from boards.views.notifications import NotificationView
10 10 from boards.views.search import BoardSearchView
11 11 from boards.views.static import StaticPageView
12 12 from boards.views.preview import PostPreviewView
13 13
14 14 js_info_dict = {
15 15 'packages': ('boards',),
16 16 }
17 17
18 18 urlpatterns = patterns('',
19 19 # /boards/
20 20 url(r'^$', all_threads.AllThreadsView.as_view(), name='index'),
21 21 # /boards/page/
22 22 url(r'^page/(?P<page>\w+)/$', all_threads.AllThreadsView.as_view(),
23 23 name='index'),
24 24
25 25 # /boards/tag/tag_name/
26 26 url(r'^tag/(?P<tag_name>\w+)/$', tag_threads.TagView.as_view(),
27 27 name='tag'),
28 28 # /boards/tag/tag_id/page/
29 29 url(r'^tag/(?P<tag_name>\w+)/page/(?P<page>\w+)/$',
30 30 tag_threads.TagView.as_view(), name='tag'),
31 31
32 32 # /boards/thread/
33 33 url(r'^thread/(?P<post_id>\w+)/$', views.thread.normal.NormalThreadView.as_view(),
34 34 name='thread'),
35 35 url(r'^thread/(?P<post_id>\w+)/mode/gallery/$', views.thread.gallery.GalleryThreadView.as_view(),
36 36 name='thread_gallery'),
37 37
38 38 url(r'^settings/$', settings.SettingsView.as_view(), name='settings'),
39 39 url(r'^tags/$', all_tags.AllTagsView.as_view(), name='tags'),
40 40 url(r'^authors/$', AuthorsView.as_view(), name='authors'),
41 41 url(r'^ban/(?P<post_id>\w+)/$', BanUserView.as_view(), name='ban'),
42 42
43 43 url(r'^banned/$', views.banned.BannedView.as_view(), name='banned'),
44 44 url(r'^staticpage/(?P<name>\w+)/$', StaticPageView.as_view(),
45 45 name='staticpage'),
46 46
47 47 # RSS feeds
48 48 url(r'^rss/$', AllThreadsFeed()),
49 49 url(r'^page/(?P<page>\w+)/rss/$', AllThreadsFeed()),
50 50 url(r'^tag/(?P<tag_name>\w+)/rss/$', TagThreadsFeed()),
51 51 url(r'^tag/(?P<tag_name>\w+)/page/(?P<page>\w+)/rss/$', TagThreadsFeed()),
52 52 url(r'^thread/(?P<post_id>\w+)/rss/$', ThreadPostsFeed()),
53 53
54 54 # i18n
55 55 url(r'^jsi18n/$', 'boards.views.cached_js_catalog', js_info_dict,
56 56 name='js_info_dict'),
57 57
58 58 # API
59 59 url(r'^api/post/(?P<post_id>\w+)/$', api.get_post, name="get_post"),
60 60 url(r'^api/diff_thread/(?P<thread_id>\w+)/(?P<last_update_time>\w+)/$',
61 61 api.api_get_threaddiff, name="get_thread_diff"),
62 62 url(r'^api/threads/(?P<count>\w+)/$', api.api_get_threads,
63 63 name='get_threads'),
64 64 url(r'^api/tags/$', api.api_get_tags, name='get_tags'),
65 65 url(r'^api/thread/(?P<opening_post_id>\w+)/$', api.api_get_thread_posts,
66 66 name='get_thread'),
67 67 url(r'^api/add_post/(?P<opening_post_id>\w+)/$', api.api_add_post,
68 68 name='add_post'),
69 url(r'^api/notifications/(?P<username>\w+)/$', api.api_get_notifications,
70 name='api_notifications'),
69 71
70 72 # Search
71 73 url(r'^search/$', BoardSearchView.as_view(), name='search'),
72 74
73 75 # Notifications
74 76 url(r'^notifications/(?P<username>\w+)$', NotificationView.as_view(), name='notifications'),
75 77
76 78 # Post preview
77 79 url(r'^preview/$', PostPreviewView.as_view(), name='preview')
78 80
79 81 )
@@ -1,225 +1,240
1 1 from datetime import datetime
2 2 import json
3 3 import logging
4 4 from django.db import transaction
5 5 from django.http import HttpResponse
6 6 from django.shortcuts import get_object_or_404, render
7 7 from django.template import RequestContext
8 8 from django.utils import timezone
9 9 from django.core import serializers
10 10
11 11 from boards.forms import PostForm, PlainErrorList
12 12 from boards.models import Post, Thread, Tag
13 13 from boards.utils import datetime_to_epoch
14 14 from boards.views.thread import ThreadView
15 from boards.models.user import Notification
15 16
16 17 __author__ = 'neko259'
17 18
18 19 PARAMETER_TRUNCATED = 'truncated'
19 20 PARAMETER_TAG = 'tag'
20 21 PARAMETER_OFFSET = 'offset'
21 22 PARAMETER_DIFF_TYPE = 'type'
22 23
23 24 DIFF_TYPE_HTML = 'html'
24 25 DIFF_TYPE_JSON = 'json'
25 26
26 27 STATUS_OK = 'ok'
27 28 STATUS_ERROR = 'error'
28 29
29 30 logger = logging.getLogger(__name__)
30 31
31 32
32 33 @transaction.atomic
33 34 def api_get_threaddiff(request, thread_id, last_update_time):
34 35 """
35 36 Gets posts that were changed or added since time
36 37 """
37 38
38 39 thread = get_object_or_404(Post, id=thread_id).get_thread()
39 40
40 41 # Add 1 to ensure we don't load the same post over and over
41 42 last_update_timestamp = float(last_update_time) + 1
42 43
43 44 filter_time = datetime.fromtimestamp(last_update_timestamp / 1000000,
44 45 timezone.get_current_timezone())
45 46
46 47 json_data = {
47 48 'added': [],
48 49 'updated': [],
49 50 'last_update': None,
50 51 }
51 52 added_posts = Post.objects.filter(threads__in=[thread],
52 53 pub_time__gt=filter_time) \
53 54 .order_by('pub_time')
54 55 updated_posts = Post.objects.filter(threads__in=[thread],
55 56 pub_time__lte=filter_time,
56 57 last_edit_time__gt=filter_time)
57 58
58 59 diff_type = request.GET.get(PARAMETER_DIFF_TYPE, DIFF_TYPE_HTML)
59 60
60 61 for post in added_posts:
61 62 json_data['added'].append(get_post_data(post.id, diff_type, request))
62 63 for post in updated_posts:
63 64 json_data['updated'].append(get_post_data(post.id, diff_type, request))
64 65 json_data['last_update'] = datetime_to_epoch(thread.last_edit_time)
65 66
66 67 return HttpResponse(content=json.dumps(json_data))
67 68
68 69
69 70 def api_add_post(request, opening_post_id):
70 71 """
71 72 Adds a post and return the JSON response for it
72 73 """
73 74
74 75 opening_post = get_object_or_404(Post, id=opening_post_id)
75 76
76 77 logger.info('Adding post via api...')
77 78
78 79 status = STATUS_OK
79 80 errors = []
80 81
81 82 if request.method == 'POST':
82 83 form = PostForm(request.POST, request.FILES, error_class=PlainErrorList)
83 84 form.session = request.session
84 85
85 86 if form.need_to_ban:
86 87 # Ban user because he is suspected to be a bot
87 88 # _ban_current_user(request)
88 89 status = STATUS_ERROR
89 90 if form.is_valid():
90 91 post = ThreadView().new_post(request, form, opening_post,
91 92 html_response=False)
92 93 if not post:
93 94 status = STATUS_ERROR
94 95 else:
95 96 logger.info('Added post #%d via api.' % post.id)
96 97 else:
97 98 status = STATUS_ERROR
98 99 errors = form.as_json_errors()
99 100
100 101 response = {
101 102 'status': status,
102 103 'errors': errors,
103 104 }
104 105
105 106 return HttpResponse(content=json.dumps(response))
106 107
107 108
108 109 def get_post(request, post_id):
109 110 """
110 111 Gets the html of a post. Used for popups. Post can be truncated if used
111 112 in threads list with 'truncated' get parameter.
112 113 """
113 114
114 115 post = get_object_or_404(Post, id=post_id)
115 116
116 117 context = RequestContext(request)
117 118 context['post'] = post
118 119 if PARAMETER_TRUNCATED in request.GET:
119 120 context[PARAMETER_TRUNCATED] = True
120 121
121 122 # TODO Use dict here
122 123 return render(request, 'boards/api_post.html', context_instance=context)
123 124
124 125
125 126 def api_get_threads(request, count):
126 127 """
127 128 Gets the JSON thread opening posts list.
128 129 Parameters that can be used for filtering:
129 130 tag, offset (from which thread to get results)
130 131 """
131 132
132 133 if PARAMETER_TAG in request.GET:
133 134 tag_name = request.GET[PARAMETER_TAG]
134 135 if tag_name is not None:
135 136 tag = get_object_or_404(Tag, name=tag_name)
136 137 threads = tag.get_threads().filter(archived=False)
137 138 else:
138 139 threads = Thread.objects.filter(archived=False)
139 140
140 141 if PARAMETER_OFFSET in request.GET:
141 142 offset = request.GET[PARAMETER_OFFSET]
142 143 offset = int(offset) if offset is not None else 0
143 144 else:
144 145 offset = 0
145 146
146 147 threads = threads.order_by('-bump_time')
147 148 threads = threads[offset:offset + int(count)]
148 149
149 150 opening_posts = []
150 151 for thread in threads:
151 152 opening_post = thread.get_opening_post()
152 153
153 154 # TODO Add tags, replies and images count
154 155 post_data = get_post_data(opening_post.id, include_last_update=True)
155 156 post_data['bumpable'] = thread.can_bump()
156 157 post_data['archived'] = thread.archived
157 158
158 159 opening_posts.append(post_data)
159 160
160 161 return HttpResponse(content=json.dumps(opening_posts))
161 162
162 163
163 164 # TODO Test this
164 165 def api_get_tags(request):
165 166 """
166 167 Gets all tags or user tags.
167 168 """
168 169
169 170 # TODO Get favorite tags for the given user ID
170 171
171 172 tags = Tag.objects.get_not_empty_tags()
172 173 tag_names = []
173 174 for tag in tags:
174 175 tag_names.append(tag.name)
175 176
176 177 return HttpResponse(content=json.dumps(tag_names))
177 178
178 179
179 180 # TODO The result can be cached by the thread last update time
180 181 # TODO Test this
181 182 def api_get_thread_posts(request, opening_post_id):
182 183 """
183 184 Gets the JSON array of thread posts
184 185 """
185 186
186 187 opening_post = get_object_or_404(Post, id=opening_post_id)
187 188 thread = opening_post.get_thread()
188 189 posts = thread.get_replies()
189 190
190 191 json_data = {
191 192 'posts': [],
192 193 'last_update': None,
193 194 }
194 195 json_post_list = []
195 196
196 197 for post in posts:
197 198 json_post_list.append(get_post_data(post.id))
198 199 json_data['last_update'] = datetime_to_epoch(thread.last_edit_time)
199 200 json_data['posts'] = json_post_list
200 201
201 202 return HttpResponse(content=json.dumps(json_data))
202 203
203 204
205 def api_get_notifications(request, username):
206 last_notification_id_str = request.GET.get('last', None)
207 last_id = int(last_notification_id_str) if last_notification_id_str is not None else None
208
209 posts = Notification.objects.get_notification_posts(username=username,
210 last=last_id)
211
212 json_post_list = []
213 for post in posts:
214 json_post_list.append(get_post_data(post.id))
215 return HttpResponse(content=json.dumps(json_post_list))
216
217
218
204 219 def api_get_post(request, post_id):
205 220 """
206 221 Gets the JSON of a post. This can be
207 222 used as and API for external clients.
208 223 """
209 224
210 225 post = get_object_or_404(Post, id=post_id)
211 226
212 227 json = serializers.serialize("json", [post], fields=(
213 228 "pub_time", "_text_rendered", "title", "text", "image",
214 229 "image_width", "image_height", "replies", "tags"
215 230 ))
216 231
217 232 return HttpResponse(content=json)
218 233
219 234
220 235 # TODO Remove this method and use post method directly
221 236 def get_post_data(post_id, format_type=DIFF_TYPE_JSON, request=None,
222 237 include_last_update=False):
223 238 post = get_object_or_404(Post, id=post_id)
224 239 return post.get_post_data(format_type=format_type, request=request,
225 240 include_last_update=include_last_update)
@@ -1,41 +1,41
1 1 from django.shortcuts import render
2 2 from boards.abstracts.paginator import get_paginator
3 3 from boards.abstracts.settingsmanager import get_settings_manager, \
4 4 SETTING_USERNAME, SETTING_LAST_NOTIFICATION_ID
5 5 from boards.models import Post
6 6 from boards.models.user import Notification
7 7 from boards.views.base import BaseBoardView
8 8
9 9 TEMPLATE = 'boards/notifications.html'
10 10 PARAM_PAGE = 'page'
11 11 PARAM_USERNAME = 'notification_username'
12 12 REQUEST_PAGE = 'page'
13 13 RESULTS_PER_PAGE = 10
14 14
15 15
16 16 class NotificationView(BaseBoardView):
17 17
18 18 def get(self, request, username):
19 19 params = self.get_context_data()
20 20
21 21 settings_manager = get_settings_manager(request)
22 22
23 23 # If we open our notifications, reset the "new" count
24 24 my_username = settings_manager.get_setting(SETTING_USERNAME)
25
26 posts = Notification.objects.get_notification_posts(username=username)
25 27 if username == my_username:
26 last = Notification.objects.filter(name=username).order_by(
27 'id').last()
28 last = posts.first()
28 29 if last is not None:
29 30 last_id = last.id
30 31 settings_manager.set_setting(SETTING_LAST_NOTIFICATION_ID,
31 32 last_id)
32 33
33 posts = Post.objects.filter(notification__name=username).order_by('-id')
34 34 paginator = get_paginator(posts, RESULTS_PER_PAGE)
35 35
36 36 page = int(request.GET.get(REQUEST_PAGE, '1'))
37 37
38 38 params[PARAM_PAGE] = paginator.page(page)
39 39 params[PARAM_USERNAME] = username
40 40
41 41 return render(request, TEMPLATE, params)
@@ -1,66 +1,75
1 1 # INTRO #
2 2
3 3 The API is provided to query the data from a neaboard server by any client
4 4 application.
5 5
6 6 Tha data is returned in the json format and got by an http query.
7 7
8 8 # METHODS #
9 9
10 10 ## Threads ##
11 11
12 12 /api/threads/N/?offset=M&tag=O
13 13
14 14 Get a thread list. You will get ``N`` threads (required parameter) starting from
15 15 ``M``th one (optional parameter, default is 0) with the tag ``O`` (optional parameter,
16 16 threads with any tags are shown by default).
17 17
18 18 ## Tags ##
19 19
20 20 /api/tags/
21 21
22 22 Get all active tag list. Active tag is a tag that has at least 1 active thread
23 23 associated with it.
24 24
25 25 ## Thread ##
26 26
27 27 /api/thread/N/
28 28
29 29 Get all ``N``th thread post. ``N`` is an opening post ID for the thread.
30 30
31 31 Output format:
32 32
33 33 * ``posts``: list of posts
34 34 * ``last_update``: last update timestamp
35 35
36 36 ## Thread diff ##
37 37
38 38 /api/diff_thread/N/M/?type=O
39 39
40 40 Get the diff of the thread with id=``N`` from the ``M`` timestamp in the ``O``
41 41 format. 2 formats are available: ``html`` (used in AJAX thread update) and
42 42 ``json``. The default format is ``html``. Return list format:
43 43
44 44 * ``added``: list of added posts
45 45 * ``updated``: list of updated posts
46 46 * ``last_update``: last update timestamp
47 47
48 ## Notifications ##
49
50 /api/notifications/<username>/[?last=<id>]
51
52 Get user notifications for user starting from the post ID.
53
54 * ``username``: name of the notified user
55 * ``id``: ID of a last notification post
56
48 57 ## General info ##
49 58
50 59 In case of incorrect request you can get http error 404.
51 60
52 61 Response JSON for a post or thread contains:
53 62
54 63 * ``id``
55 64 * ``title``
56 65 * ``text``
57 66 * ``image`` (if image available)
58 67 * ``image_preview`` (if image available)
59 68 * ``bump_time`` (for threads)
60 69
61 70 In future, it will also contain:
62 71
63 72 * tags list (for thread)
64 73 * publishing time
65 74 * bump time
66 75 * reply IDs (if available)
General Comments 0
You need to be logged in to leave comments. Login now