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