##// END OF EJS Templates
Added index on post.opening. Use it when getting OP of a thread
neko259 -
r1381:197e4fa8 default
parent child Browse files
Show More
@@ -0,0 +1,19 b''
1 # -*- coding: utf-8 -*-
2 from __future__ import unicode_literals
3
4 from django.db import migrations, models
5
6
7 class Migration(migrations.Migration):
8
9 dependencies = [
10 ('boards', '0034_auto_20151014_2253'),
11 ]
12
13 operations = [
14 migrations.AlterField(
15 model_name='post',
16 name='opening',
17 field=models.BooleanField(db_index=True),
18 ),
19 ]
@@ -1,361 +1,361 b''
1 import logging
1 import logging
2 import re
2 import re
3 import uuid
3 import uuid
4
4
5 from django.core.exceptions import ObjectDoesNotExist
5 from django.core.exceptions import ObjectDoesNotExist
6 from django.core.urlresolvers import reverse
6 from django.core.urlresolvers import reverse
7 from django.db import models
7 from django.db import models
8 from django.db.models import TextField, QuerySet
8 from django.db.models import TextField, QuerySet
9 from django.template.defaultfilters import striptags, truncatewords
9 from django.template.defaultfilters import striptags, truncatewords
10 from django.template.loader import render_to_string
10 from django.template.loader import render_to_string
11 from django.utils import timezone
11 from django.utils import timezone
12
12
13 from boards import settings
13 from boards import settings
14 from boards.abstracts.tripcode import Tripcode
14 from boards.abstracts.tripcode import Tripcode
15 from boards.mdx_neboard import Parser
15 from boards.mdx_neboard import Parser
16 from boards.models import PostImage, Attachment
16 from boards.models import PostImage, Attachment
17 from boards.models.base import Viewable
17 from boards.models.base import Viewable
18 from boards.models.post.export import get_exporter, DIFF_TYPE_JSON
18 from boards.models.post.export import get_exporter, DIFF_TYPE_JSON
19 from boards.models.post.manager import PostManager
19 from boards.models.post.manager import PostManager
20 from boards.models.user import Notification
20 from boards.models.user import Notification
21
21
22 CSS_CLS_HIDDEN_POST = 'hidden_post'
22 CSS_CLS_HIDDEN_POST = 'hidden_post'
23 CSS_CLS_DEAD_POST = 'dead_post'
23 CSS_CLS_DEAD_POST = 'dead_post'
24 CSS_CLS_ARCHIVE_POST = 'archive_post'
24 CSS_CLS_ARCHIVE_POST = 'archive_post'
25 CSS_CLS_POST = 'post'
25 CSS_CLS_POST = 'post'
26
26
27 TITLE_MAX_WORDS = 10
27 TITLE_MAX_WORDS = 10
28
28
29 APP_LABEL_BOARDS = 'boards'
29 APP_LABEL_BOARDS = 'boards'
30
30
31 BAN_REASON_AUTO = 'Auto'
31 BAN_REASON_AUTO = 'Auto'
32
32
33 IMAGE_THUMB_SIZE = (200, 150)
33 IMAGE_THUMB_SIZE = (200, 150)
34
34
35 TITLE_MAX_LENGTH = 200
35 TITLE_MAX_LENGTH = 200
36
36
37 REGEX_REPLY = re.compile(r'\[post\](\d+)\[/post\]')
37 REGEX_REPLY = re.compile(r'\[post\](\d+)\[/post\]')
38 REGEX_NOTIFICATION = re.compile(r'\[user\](\w+)\[/user\]')
38 REGEX_NOTIFICATION = re.compile(r'\[user\](\w+)\[/user\]')
39
39
40 PARAMETER_TRUNCATED = 'truncated'
40 PARAMETER_TRUNCATED = 'truncated'
41 PARAMETER_TAG = 'tag'
41 PARAMETER_TAG = 'tag'
42 PARAMETER_OFFSET = 'offset'
42 PARAMETER_OFFSET = 'offset'
43 PARAMETER_DIFF_TYPE = 'type'
43 PARAMETER_DIFF_TYPE = 'type'
44 PARAMETER_CSS_CLASS = 'css_class'
44 PARAMETER_CSS_CLASS = 'css_class'
45 PARAMETER_THREAD = 'thread'
45 PARAMETER_THREAD = 'thread'
46 PARAMETER_IS_OPENING = 'is_opening'
46 PARAMETER_IS_OPENING = 'is_opening'
47 PARAMETER_MODERATOR = 'moderator'
47 PARAMETER_MODERATOR = 'moderator'
48 PARAMETER_POST = 'post'
48 PARAMETER_POST = 'post'
49 PARAMETER_OP_ID = 'opening_post_id'
49 PARAMETER_OP_ID = 'opening_post_id'
50 PARAMETER_NEED_OPEN_LINK = 'need_open_link'
50 PARAMETER_NEED_OPEN_LINK = 'need_open_link'
51 PARAMETER_REPLY_LINK = 'reply_link'
51 PARAMETER_REPLY_LINK = 'reply_link'
52 PARAMETER_NEED_OP_DATA = 'need_op_data'
52 PARAMETER_NEED_OP_DATA = 'need_op_data'
53
53
54 POST_VIEW_PARAMS = (
54 POST_VIEW_PARAMS = (
55 'need_op_data',
55 'need_op_data',
56 'reply_link',
56 'reply_link',
57 'moderator',
57 'moderator',
58 'need_open_link',
58 'need_open_link',
59 'truncated',
59 'truncated',
60 'mode_tree',
60 'mode_tree',
61 )
61 )
62
62
63
63
64 class Post(models.Model, Viewable):
64 class Post(models.Model, Viewable):
65 """A post is a message."""
65 """A post is a message."""
66
66
67 objects = PostManager()
67 objects = PostManager()
68
68
69 class Meta:
69 class Meta:
70 app_label = APP_LABEL_BOARDS
70 app_label = APP_LABEL_BOARDS
71 ordering = ('id',)
71 ordering = ('id',)
72
72
73 title = models.CharField(max_length=TITLE_MAX_LENGTH, null=True, blank=True)
73 title = models.CharField(max_length=TITLE_MAX_LENGTH, null=True, blank=True)
74 pub_time = models.DateTimeField()
74 pub_time = models.DateTimeField()
75 text = TextField(blank=True, null=True)
75 text = TextField(blank=True, null=True)
76 _text_rendered = TextField(blank=True, null=True, editable=False)
76 _text_rendered = TextField(blank=True, null=True, editable=False)
77
77
78 images = models.ManyToManyField(PostImage, null=True, blank=True,
78 images = models.ManyToManyField(PostImage, null=True, blank=True,
79 related_name='post_images', db_index=True)
79 related_name='post_images', db_index=True)
80 attachments = models.ManyToManyField(Attachment, null=True, blank=True,
80 attachments = models.ManyToManyField(Attachment, null=True, blank=True,
81 related_name='attachment_posts')
81 related_name='attachment_posts')
82
82
83 poster_ip = models.GenericIPAddressField()
83 poster_ip = models.GenericIPAddressField()
84
84
85 # TODO This field can be removed cause UID is used for update now
85 # TODO This field can be removed cause UID is used for update now
86 last_edit_time = models.DateTimeField()
86 last_edit_time = models.DateTimeField()
87
87
88 referenced_posts = models.ManyToManyField('Post', symmetrical=False,
88 referenced_posts = models.ManyToManyField('Post', symmetrical=False,
89 null=True,
89 null=True,
90 blank=True, related_name='refposts',
90 blank=True, related_name='refposts',
91 db_index=True)
91 db_index=True)
92 refmap = models.TextField(null=True, blank=True)
92 refmap = models.TextField(null=True, blank=True)
93 threads = models.ManyToManyField('Thread', db_index=True,
93 threads = models.ManyToManyField('Thread', db_index=True,
94 related_name='multi_replies')
94 related_name='multi_replies')
95 thread = models.ForeignKey('Thread', db_index=True, related_name='pt+')
95 thread = models.ForeignKey('Thread', db_index=True, related_name='pt+')
96
96
97 url = models.TextField()
97 url = models.TextField()
98 uid = models.TextField(db_index=True)
98 uid = models.TextField(db_index=True)
99
99
100 tripcode = models.CharField(max_length=50, blank=True, default='')
100 tripcode = models.CharField(max_length=50, blank=True, default='')
101 opening = models.BooleanField()
101 opening = models.BooleanField(db_index=True)
102 hidden = models.BooleanField(default=False)
102 hidden = models.BooleanField(default=False)
103
103
104 def __str__(self):
104 def __str__(self):
105 return 'P#{}/{}'.format(self.id, self.get_title())
105 return 'P#{}/{}'.format(self.id, self.get_title())
106
106
107 def get_referenced_posts(self):
107 def get_referenced_posts(self):
108 threads = self.get_threads().all()
108 threads = self.get_threads().all()
109 return self.referenced_posts.filter(threads__in=threads)\
109 return self.referenced_posts.filter(threads__in=threads)\
110 .order_by('pub_time').distinct().all()
110 .order_by('pub_time').distinct().all()
111
111
112 def get_title(self) -> str:
112 def get_title(self) -> str:
113 return self.title
113 return self.title
114
114
115 def get_title_or_text(self):
115 def get_title_or_text(self):
116 title = self.get_title()
116 title = self.get_title()
117 if not title:
117 if not title:
118 title = truncatewords(striptags(self.get_text()), TITLE_MAX_WORDS)
118 title = truncatewords(striptags(self.get_text()), TITLE_MAX_WORDS)
119
119
120 return title
120 return title
121
121
122 def build_refmap(self) -> None:
122 def build_refmap(self) -> None:
123 """
123 """
124 Builds a replies map string from replies list. This is a cache to stop
124 Builds a replies map string from replies list. This is a cache to stop
125 the server from recalculating the map on every post show.
125 the server from recalculating the map on every post show.
126 """
126 """
127
127
128 post_urls = [refpost.get_link_view()
128 post_urls = [refpost.get_link_view()
129 for refpost in self.referenced_posts.all()]
129 for refpost in self.referenced_posts.all()]
130
130
131 self.refmap = ', '.join(post_urls)
131 self.refmap = ', '.join(post_urls)
132
132
133 def is_referenced(self) -> bool:
133 def is_referenced(self) -> bool:
134 return self.refmap and len(self.refmap) > 0
134 return self.refmap and len(self.refmap) > 0
135
135
136 def is_opening(self) -> bool:
136 def is_opening(self) -> bool:
137 """
137 """
138 Checks if this is an opening post or just a reply.
138 Checks if this is an opening post or just a reply.
139 """
139 """
140
140
141 return self.opening
141 return self.opening
142
142
143 def get_absolute_url(self, thread=None):
143 def get_absolute_url(self, thread=None):
144 url = None
144 url = None
145
145
146 if thread is None:
146 if thread is None:
147 thread = self.get_thread()
147 thread = self.get_thread()
148
148
149 # Url is cached only for the "main" thread. When getting url
149 # Url is cached only for the "main" thread. When getting url
150 # for other threads, do it manually.
150 # for other threads, do it manually.
151 if self.url:
151 if self.url:
152 url = self.url
152 url = self.url
153
153
154 if url is None:
154 if url is None:
155 opening_id = thread.get_opening_post_id()
155 opening_id = thread.get_opening_post_id()
156 url = reverse('thread', kwargs={'post_id': opening_id})
156 url = reverse('thread', kwargs={'post_id': opening_id})
157 if self.id != opening_id:
157 if self.id != opening_id:
158 url += '#' + str(self.id)
158 url += '#' + str(self.id)
159
159
160 return url
160 return url
161
161
162 def get_thread(self):
162 def get_thread(self):
163 return self.thread
163 return self.thread
164
164
165 def get_threads(self) -> QuerySet:
165 def get_threads(self) -> QuerySet:
166 """
166 """
167 Gets post's thread.
167 Gets post's thread.
168 """
168 """
169
169
170 return self.threads
170 return self.threads
171
171
172 def get_view(self, *args, **kwargs) -> str:
172 def get_view(self, *args, **kwargs) -> str:
173 """
173 """
174 Renders post's HTML view. Some of the post params can be passed over
174 Renders post's HTML view. Some of the post params can be passed over
175 kwargs for the means of caching (if we view the thread, some params
175 kwargs for the means of caching (if we view the thread, some params
176 are same for every post and don't need to be computed over and over.
176 are same for every post and don't need to be computed over and over.
177 """
177 """
178
178
179 thread = self.get_thread()
179 thread = self.get_thread()
180
180
181 css_classes = [CSS_CLS_POST]
181 css_classes = [CSS_CLS_POST]
182 if thread.archived:
182 if thread.archived:
183 css_classes.append(CSS_CLS_ARCHIVE_POST)
183 css_classes.append(CSS_CLS_ARCHIVE_POST)
184 elif not thread.can_bump():
184 elif not thread.can_bump():
185 css_classes.append(CSS_CLS_DEAD_POST)
185 css_classes.append(CSS_CLS_DEAD_POST)
186 if self.is_hidden():
186 if self.is_hidden():
187 css_classes.append(CSS_CLS_HIDDEN_POST)
187 css_classes.append(CSS_CLS_HIDDEN_POST)
188
188
189 params = dict()
189 params = dict()
190 for param in POST_VIEW_PARAMS:
190 for param in POST_VIEW_PARAMS:
191 if param in kwargs:
191 if param in kwargs:
192 params[param] = kwargs[param]
192 params[param] = kwargs[param]
193
193
194 params.update({
194 params.update({
195 PARAMETER_POST: self,
195 PARAMETER_POST: self,
196 PARAMETER_IS_OPENING: self.is_opening(),
196 PARAMETER_IS_OPENING: self.is_opening(),
197 PARAMETER_THREAD: thread,
197 PARAMETER_THREAD: thread,
198 PARAMETER_CSS_CLASS: ' '.join(css_classes),
198 PARAMETER_CSS_CLASS: ' '.join(css_classes),
199 })
199 })
200
200
201 return render_to_string('boards/post.html', params)
201 return render_to_string('boards/post.html', params)
202
202
203 def get_search_view(self, *args, **kwargs):
203 def get_search_view(self, *args, **kwargs):
204 return self.get_view(need_op_data=True, *args, **kwargs)
204 return self.get_view(need_op_data=True, *args, **kwargs)
205
205
206 def get_first_image(self) -> PostImage:
206 def get_first_image(self) -> PostImage:
207 return self.images.earliest('id')
207 return self.images.earliest('id')
208
208
209 def delete(self, using=None):
209 def delete(self, using=None):
210 """
210 """
211 Deletes all post images and the post itself.
211 Deletes all post images and the post itself.
212 """
212 """
213
213
214 for image in self.images.all():
214 for image in self.images.all():
215 image_refs_count = image.post_images.count()
215 image_refs_count = image.post_images.count()
216 if image_refs_count == 1:
216 if image_refs_count == 1:
217 image.delete()
217 image.delete()
218
218
219 for attachment in self.attachments.all():
219 for attachment in self.attachments.all():
220 attachment_refs_count = attachment.attachment_posts.count()
220 attachment_refs_count = attachment.attachment_posts.count()
221 if attachment_refs_count == 1:
221 if attachment_refs_count == 1:
222 attachment.delete()
222 attachment.delete()
223
223
224 thread = self.get_thread()
224 thread = self.get_thread()
225 thread.last_edit_time = timezone.now()
225 thread.last_edit_time = timezone.now()
226 thread.save()
226 thread.save()
227
227
228 super(Post, self).delete(using)
228 super(Post, self).delete(using)
229
229
230 logging.getLogger('boards.post.delete').info(
230 logging.getLogger('boards.post.delete').info(
231 'Deleted post {}'.format(self))
231 'Deleted post {}'.format(self))
232
232
233 def get_post_data(self, format_type=DIFF_TYPE_JSON, request=None,
233 def get_post_data(self, format_type=DIFF_TYPE_JSON, request=None,
234 include_last_update=False) -> str:
234 include_last_update=False) -> str:
235 """
235 """
236 Gets post HTML or JSON data that can be rendered on a page or used by
236 Gets post HTML or JSON data that can be rendered on a page or used by
237 API.
237 API.
238 """
238 """
239
239
240 return get_exporter(format_type).export(self, request,
240 return get_exporter(format_type).export(self, request,
241 include_last_update)
241 include_last_update)
242
242
243 def notify_clients(self, recursive=True):
243 def notify_clients(self, recursive=True):
244 """
244 """
245 Sends post HTML data to the thread web socket.
245 Sends post HTML data to the thread web socket.
246 """
246 """
247
247
248 if not settings.get_bool('External', 'WebsocketsEnabled'):
248 if not settings.get_bool('External', 'WebsocketsEnabled'):
249 return
249 return
250
250
251 thread_ids = list()
251 thread_ids = list()
252 for thread in self.get_threads().all():
252 for thread in self.get_threads().all():
253 thread_ids.append(thread.id)
253 thread_ids.append(thread.id)
254
254
255 thread.notify_clients()
255 thread.notify_clients()
256
256
257 if recursive:
257 if recursive:
258 for reply_number in re.finditer(REGEX_REPLY, self.get_raw_text()):
258 for reply_number in re.finditer(REGEX_REPLY, self.get_raw_text()):
259 post_id = reply_number.group(1)
259 post_id = reply_number.group(1)
260
260
261 try:
261 try:
262 ref_post = Post.objects.get(id=post_id)
262 ref_post = Post.objects.get(id=post_id)
263
263
264 if ref_post.get_threads().exclude(id__in=thread_ids).exists():
264 if ref_post.get_threads().exclude(id__in=thread_ids).exists():
265 # If post is in this thread, its thread was already notified.
265 # If post is in this thread, its thread was already notified.
266 # Otherwise, notify its thread separately.
266 # Otherwise, notify its thread separately.
267 ref_post.notify_clients(recursive=False)
267 ref_post.notify_clients(recursive=False)
268 except ObjectDoesNotExist:
268 except ObjectDoesNotExist:
269 pass
269 pass
270
270
271 def build_url(self):
271 def build_url(self):
272 self.url = self.get_absolute_url()
272 self.url = self.get_absolute_url()
273 self.save(update_fields=['url'])
273 self.save(update_fields=['url'])
274
274
275 def save(self, force_insert=False, force_update=False, using=None,
275 def save(self, force_insert=False, force_update=False, using=None,
276 update_fields=None):
276 update_fields=None):
277 self._text_rendered = Parser().parse(self.get_raw_text())
277 self._text_rendered = Parser().parse(self.get_raw_text())
278
278
279 self.uid = str(uuid.uuid4())
279 self.uid = str(uuid.uuid4())
280 if update_fields is not None and 'uid' not in update_fields:
280 if update_fields is not None and 'uid' not in update_fields:
281 update_fields += ['uid']
281 update_fields += ['uid']
282
282
283 if self.id:
283 if self.id:
284 for thread in self.get_threads().all():
284 for thread in self.get_threads().all():
285 thread.last_edit_time = self.last_edit_time
285 thread.last_edit_time = self.last_edit_time
286
286
287 thread.save(update_fields=['last_edit_time', 'bumpable'])
287 thread.save(update_fields=['last_edit_time', 'bumpable'])
288
288
289 super().save(force_insert, force_update, using, update_fields)
289 super().save(force_insert, force_update, using, update_fields)
290
290
291 def get_text(self) -> str:
291 def get_text(self) -> str:
292 return self._text_rendered
292 return self._text_rendered
293
293
294 def get_raw_text(self) -> str:
294 def get_raw_text(self) -> str:
295 return self.text
295 return self.text
296
296
297 def get_absolute_id(self) -> str:
297 def get_absolute_id(self) -> str:
298 """
298 """
299 If the post has many threads, shows its main thread OP id in the post
299 If the post has many threads, shows its main thread OP id in the post
300 ID.
300 ID.
301 """
301 """
302
302
303 if self.get_threads().count() > 1:
303 if self.get_threads().count() > 1:
304 return '{}/{}'.format(self.get_thread().get_opening_post_id(), self.id)
304 return '{}/{}'.format(self.get_thread().get_opening_post_id(), self.id)
305 else:
305 else:
306 return str(self.id)
306 return str(self.id)
307
307
308 def connect_notifications(self):
308 def connect_notifications(self):
309 for reply_number in re.finditer(REGEX_NOTIFICATION, self.get_raw_text()):
309 for reply_number in re.finditer(REGEX_NOTIFICATION, self.get_raw_text()):
310 user_name = reply_number.group(1).lower()
310 user_name = reply_number.group(1).lower()
311 Notification.objects.get_or_create(name=user_name, post=self)
311 Notification.objects.get_or_create(name=user_name, post=self)
312
312
313 def connect_replies(self):
313 def connect_replies(self):
314 """
314 """
315 Connects replies to a post to show them as a reflink map
315 Connects replies to a post to show them as a reflink map
316 """
316 """
317
317
318 for reply_number in re.finditer(REGEX_REPLY, self.get_raw_text()):
318 for reply_number in re.finditer(REGEX_REPLY, self.get_raw_text()):
319 post_id = reply_number.group(1)
319 post_id = reply_number.group(1)
320
320
321 try:
321 try:
322 referenced_post = Post.objects.get(id=post_id)
322 referenced_post = Post.objects.get(id=post_id)
323
323
324 referenced_post.referenced_posts.add(self)
324 referenced_post.referenced_posts.add(self)
325 referenced_post.last_edit_time = self.pub_time
325 referenced_post.last_edit_time = self.pub_time
326 referenced_post.build_refmap()
326 referenced_post.build_refmap()
327 referenced_post.save(update_fields=['refmap', 'last_edit_time'])
327 referenced_post.save(update_fields=['refmap', 'last_edit_time'])
328 except ObjectDoesNotExist:
328 except ObjectDoesNotExist:
329 pass
329 pass
330
330
331 def connect_threads(self, opening_posts):
331 def connect_threads(self, opening_posts):
332 for opening_post in opening_posts:
332 for opening_post in opening_posts:
333 threads = opening_post.get_threads().all()
333 threads = opening_post.get_threads().all()
334 for thread in threads:
334 for thread in threads:
335 if thread.can_bump():
335 if thread.can_bump():
336 thread.update_bump_status()
336 thread.update_bump_status()
337
337
338 thread.last_edit_time = self.last_edit_time
338 thread.last_edit_time = self.last_edit_time
339 thread.save(update_fields=['last_edit_time', 'bumpable'])
339 thread.save(update_fields=['last_edit_time', 'bumpable'])
340 self.threads.add(opening_post.get_thread())
340 self.threads.add(opening_post.get_thread())
341
341
342 def get_tripcode(self):
342 def get_tripcode(self):
343 if self.tripcode:
343 if self.tripcode:
344 return Tripcode(self.tripcode)
344 return Tripcode(self.tripcode)
345
345
346 def get_link_view(self):
346 def get_link_view(self):
347 """
347 """
348 Gets view of a reflink to the post.
348 Gets view of a reflink to the post.
349 """
349 """
350 result = '<a href="{}">&gt;&gt;{}</a>'.format(self.get_absolute_url(),
350 result = '<a href="{}">&gt;&gt;{}</a>'.format(self.get_absolute_url(),
351 self.id)
351 self.id)
352 if self.is_opening():
352 if self.is_opening():
353 result = '<b>{}</b>'.format(result)
353 result = '<b>{}</b>'.format(result)
354
354
355 return result
355 return result
356
356
357 def is_hidden(self) -> bool:
357 def is_hidden(self) -> bool:
358 return self.hidden
358 return self.hidden
359
359
360 def set_hidden(self, hidden):
360 def set_hidden(self, hidden):
361 self.hidden = hidden
361 self.hidden = hidden
@@ -1,258 +1,258 b''
1 import logging
1 import logging
2 from adjacent import Client
2 from adjacent import Client
3
3
4 from django.db.models import Count, Sum, QuerySet, Q
4 from django.db.models import Count, Sum, QuerySet, Q
5 from django.utils import timezone
5 from django.utils import timezone
6 from django.db import models
6 from django.db import models
7
7
8 from boards import settings
8 from boards import settings
9 import boards
9 import boards
10 from boards.utils import cached_result, datetime_to_epoch
10 from boards.utils import cached_result, datetime_to_epoch
11 from boards.models.post import Post
11 from boards.models.post import Post
12 from boards.models.tag import Tag
12 from boards.models.tag import Tag
13
13
14 FAV_THREAD_NO_UPDATES = -1
14 FAV_THREAD_NO_UPDATES = -1
15
15
16
16
17 __author__ = 'neko259'
17 __author__ = 'neko259'
18
18
19
19
20 logger = logging.getLogger(__name__)
20 logger = logging.getLogger(__name__)
21
21
22
22
23 WS_NOTIFICATION_TYPE_NEW_POST = 'new_post'
23 WS_NOTIFICATION_TYPE_NEW_POST = 'new_post'
24 WS_NOTIFICATION_TYPE = 'notification_type'
24 WS_NOTIFICATION_TYPE = 'notification_type'
25
25
26 WS_CHANNEL_THREAD = "thread:"
26 WS_CHANNEL_THREAD = "thread:"
27
27
28
28
29 class ThreadManager(models.Manager):
29 class ThreadManager(models.Manager):
30 def process_oldest_threads(self):
30 def process_oldest_threads(self):
31 """
31 """
32 Preserves maximum thread count. If there are too many threads,
32 Preserves maximum thread count. If there are too many threads,
33 archive or delete the old ones.
33 archive or delete the old ones.
34 """
34 """
35
35
36 threads = Thread.objects.filter(archived=False).order_by('-bump_time')
36 threads = Thread.objects.filter(archived=False).order_by('-bump_time')
37 thread_count = threads.count()
37 thread_count = threads.count()
38
38
39 max_thread_count = settings.get_int('Messages', 'MaxThreadCount')
39 max_thread_count = settings.get_int('Messages', 'MaxThreadCount')
40 if thread_count > max_thread_count:
40 if thread_count > max_thread_count:
41 num_threads_to_delete = thread_count - max_thread_count
41 num_threads_to_delete = thread_count - max_thread_count
42 old_threads = threads[thread_count - num_threads_to_delete:]
42 old_threads = threads[thread_count - num_threads_to_delete:]
43
43
44 for thread in old_threads:
44 for thread in old_threads:
45 if settings.get_bool('Storage', 'ArchiveThreads'):
45 if settings.get_bool('Storage', 'ArchiveThreads'):
46 self._archive_thread(thread)
46 self._archive_thread(thread)
47 else:
47 else:
48 thread.delete()
48 thread.delete()
49
49
50 logger.info('Processed %d old threads' % num_threads_to_delete)
50 logger.info('Processed %d old threads' % num_threads_to_delete)
51
51
52 def _archive_thread(self, thread):
52 def _archive_thread(self, thread):
53 thread.archived = True
53 thread.archived = True
54 thread.bumpable = False
54 thread.bumpable = False
55 thread.last_edit_time = timezone.now()
55 thread.last_edit_time = timezone.now()
56 thread.update_posts_time()
56 thread.update_posts_time()
57 thread.save(update_fields=['archived', 'last_edit_time', 'bumpable'])
57 thread.save(update_fields=['archived', 'last_edit_time', 'bumpable'])
58
58
59 def get_new_posts(self, datas):
59 def get_new_posts(self, datas):
60 query = None
60 query = None
61 # TODO Use classes instead of dicts
61 # TODO Use classes instead of dicts
62 for data in datas:
62 for data in datas:
63 if data['last_id'] != FAV_THREAD_NO_UPDATES:
63 if data['last_id'] != FAV_THREAD_NO_UPDATES:
64 q = (Q(id=data['op'].get_thread().id)
64 q = (Q(id=data['op'].get_thread().id)
65 & Q(multi_replies__id__gt=data['last_id']))
65 & Q(multi_replies__id__gt=data['last_id']))
66 if query is None:
66 if query is None:
67 query = q
67 query = q
68 else:
68 else:
69 query = query | q
69 query = query | q
70 if query is not None:
70 if query is not None:
71 return self.filter(query).annotate(
71 return self.filter(query).annotate(
72 new_post_count=Count('multi_replies'))
72 new_post_count=Count('multi_replies'))
73
73
74 def get_new_post_count(self, datas):
74 def get_new_post_count(self, datas):
75 new_posts = self.get_new_posts(datas)
75 new_posts = self.get_new_posts(datas)
76 return new_posts.aggregate(total_count=Count('multi_replies'))\
76 return new_posts.aggregate(total_count=Count('multi_replies'))\
77 ['total_count'] if new_posts else 0
77 ['total_count'] if new_posts else 0
78
78
79
79
80 def get_thread_max_posts():
80 def get_thread_max_posts():
81 return settings.get_int('Messages', 'MaxPostsPerThread')
81 return settings.get_int('Messages', 'MaxPostsPerThread')
82
82
83
83
84 class Thread(models.Model):
84 class Thread(models.Model):
85 objects = ThreadManager()
85 objects = ThreadManager()
86
86
87 class Meta:
87 class Meta:
88 app_label = 'boards'
88 app_label = 'boards'
89
89
90 tags = models.ManyToManyField('Tag', related_name='thread_tags')
90 tags = models.ManyToManyField('Tag', related_name='thread_tags')
91 bump_time = models.DateTimeField(db_index=True)
91 bump_time = models.DateTimeField(db_index=True)
92 last_edit_time = models.DateTimeField()
92 last_edit_time = models.DateTimeField()
93 archived = models.BooleanField(default=False)
93 archived = models.BooleanField(default=False)
94 bumpable = models.BooleanField(default=True)
94 bumpable = models.BooleanField(default=True)
95 max_posts = models.IntegerField(default=get_thread_max_posts)
95 max_posts = models.IntegerField(default=get_thread_max_posts)
96
96
97 def get_tags(self) -> QuerySet:
97 def get_tags(self) -> QuerySet:
98 """
98 """
99 Gets a sorted tag list.
99 Gets a sorted tag list.
100 """
100 """
101
101
102 return self.tags.order_by('name')
102 return self.tags.order_by('name')
103
103
104 def bump(self):
104 def bump(self):
105 """
105 """
106 Bumps (moves to up) thread if possible.
106 Bumps (moves to up) thread if possible.
107 """
107 """
108
108
109 if self.can_bump():
109 if self.can_bump():
110 self.bump_time = self.last_edit_time
110 self.bump_time = self.last_edit_time
111
111
112 self.update_bump_status()
112 self.update_bump_status()
113
113
114 logger.info('Bumped thread %d' % self.id)
114 logger.info('Bumped thread %d' % self.id)
115
115
116 def has_post_limit(self) -> bool:
116 def has_post_limit(self) -> bool:
117 return self.max_posts > 0
117 return self.max_posts > 0
118
118
119 def update_bump_status(self, exclude_posts=None):
119 def update_bump_status(self, exclude_posts=None):
120 if self.has_post_limit() and self.get_reply_count() >= self.max_posts:
120 if self.has_post_limit() and self.get_reply_count() >= self.max_posts:
121 self.bumpable = False
121 self.bumpable = False
122 self.update_posts_time(exclude_posts=exclude_posts)
122 self.update_posts_time(exclude_posts=exclude_posts)
123
123
124 def _get_cache_key(self):
124 def _get_cache_key(self):
125 return [datetime_to_epoch(self.last_edit_time)]
125 return [datetime_to_epoch(self.last_edit_time)]
126
126
127 @cached_result(key_method=_get_cache_key)
127 @cached_result(key_method=_get_cache_key)
128 def get_reply_count(self) -> int:
128 def get_reply_count(self) -> int:
129 return self.get_replies().count()
129 return self.get_replies().count()
130
130
131 @cached_result(key_method=_get_cache_key)
131 @cached_result(key_method=_get_cache_key)
132 def get_images_count(self) -> int:
132 def get_images_count(self) -> int:
133 return self.get_replies().annotate(images_count=Count(
133 return self.get_replies().annotate(images_count=Count(
134 'images')).aggregate(Sum('images_count'))['images_count__sum']
134 'images')).aggregate(Sum('images_count'))['images_count__sum']
135
135
136 def can_bump(self) -> bool:
136 def can_bump(self) -> bool:
137 """
137 """
138 Checks if the thread can be bumped by replying to it.
138 Checks if the thread can be bumped by replying to it.
139 """
139 """
140
140
141 return self.bumpable and not self.is_archived()
141 return self.bumpable and not self.is_archived()
142
142
143 def get_last_replies(self) -> QuerySet:
143 def get_last_replies(self) -> QuerySet:
144 """
144 """
145 Gets several last replies, not including opening post
145 Gets several last replies, not including opening post
146 """
146 """
147
147
148 last_replies_count = settings.get_int('View', 'LastRepliesCount')
148 last_replies_count = settings.get_int('View', 'LastRepliesCount')
149
149
150 if last_replies_count > 0:
150 if last_replies_count > 0:
151 reply_count = self.get_reply_count()
151 reply_count = self.get_reply_count()
152
152
153 if reply_count > 0:
153 if reply_count > 0:
154 reply_count_to_show = min(last_replies_count,
154 reply_count_to_show = min(last_replies_count,
155 reply_count - 1)
155 reply_count - 1)
156 replies = self.get_replies()
156 replies = self.get_replies()
157 last_replies = replies[reply_count - reply_count_to_show:]
157 last_replies = replies[reply_count - reply_count_to_show:]
158
158
159 return last_replies
159 return last_replies
160
160
161 def get_skipped_replies_count(self) -> int:
161 def get_skipped_replies_count(self) -> int:
162 """
162 """
163 Gets number of posts between opening post and last replies.
163 Gets number of posts between opening post and last replies.
164 """
164 """
165 reply_count = self.get_reply_count()
165 reply_count = self.get_reply_count()
166 last_replies_count = min(settings.get_int('View', 'LastRepliesCount'),
166 last_replies_count = min(settings.get_int('View', 'LastRepliesCount'),
167 reply_count - 1)
167 reply_count - 1)
168 return reply_count - last_replies_count - 1
168 return reply_count - last_replies_count - 1
169
169
170 def get_replies(self, view_fields_only=False) -> QuerySet:
170 def get_replies(self, view_fields_only=False) -> QuerySet:
171 """
171 """
172 Gets sorted thread posts
172 Gets sorted thread posts
173 """
173 """
174
174
175 query = self.multi_replies.order_by('pub_time').prefetch_related(
175 query = self.multi_replies.order_by('pub_time').prefetch_related(
176 'images', 'thread', 'threads', 'attachments')
176 'images', 'thread', 'threads', 'attachments')
177 if view_fields_only:
177 if view_fields_only:
178 query = query.defer('poster_ip')
178 query = query.defer('poster_ip')
179 return query.all()
179 return query.all()
180
180
181 def get_top_level_replies(self) -> QuerySet:
181 def get_top_level_replies(self) -> QuerySet:
182 return self.get_replies().exclude(refposts__threads__in=[self])
182 return self.get_replies().exclude(refposts__threads__in=[self])
183
183
184 def get_replies_with_images(self, view_fields_only=False) -> QuerySet:
184 def get_replies_with_images(self, view_fields_only=False) -> QuerySet:
185 """
185 """
186 Gets replies that have at least one image attached
186 Gets replies that have at least one image attached
187 """
187 """
188
188
189 return self.get_replies(view_fields_only).annotate(images_count=Count(
189 return self.get_replies(view_fields_only).annotate(images_count=Count(
190 'images')).filter(images_count__gt=0)
190 'images')).filter(images_count__gt=0)
191
191
192 def get_opening_post(self, only_id=False) -> Post:
192 def get_opening_post(self, only_id=False) -> Post:
193 """
193 """
194 Gets the first post of the thread
194 Gets the first post of the thread
195 """
195 """
196
196
197 query = self.get_replies().order_by('pub_time')
197 query = self.get_replies().filter(opening=True)
198 if only_id:
198 if only_id:
199 query = query.only('id')
199 query = query.only('id')
200 opening_post = query.first()
200 opening_post = query.first()
201
201
202 return opening_post
202 return opening_post
203
203
204 @cached_result()
204 @cached_result()
205 def get_opening_post_id(self) -> int:
205 def get_opening_post_id(self) -> int:
206 """
206 """
207 Gets ID of the first thread post.
207 Gets ID of the first thread post.
208 """
208 """
209
209
210 return self.get_opening_post(only_id=True).id
210 return self.get_opening_post(only_id=True).id
211
211
212 def get_pub_time(self):
212 def get_pub_time(self):
213 """
213 """
214 Gets opening post's pub time because thread does not have its own one.
214 Gets opening post's pub time because thread does not have its own one.
215 """
215 """
216
216
217 return self.get_opening_post().pub_time
217 return self.get_opening_post().pub_time
218
218
219 def __str__(self):
219 def __str__(self):
220 return 'T#{}/{}'.format(self.id, self.get_opening_post_id())
220 return 'T#{}/{}'.format(self.id, self.get_opening_post_id())
221
221
222 def get_tag_url_list(self) -> list:
222 def get_tag_url_list(self) -> list:
223 return boards.models.Tag.objects.get_tag_url_list(self.get_tags())
223 return boards.models.Tag.objects.get_tag_url_list(self.get_tags())
224
224
225 def update_posts_time(self, exclude_posts=None):
225 def update_posts_time(self, exclude_posts=None):
226 last_edit_time = self.last_edit_time
226 last_edit_time = self.last_edit_time
227
227
228 for post in self.multi_replies.all():
228 for post in self.multi_replies.all():
229 if exclude_posts is None or post not in exclude_posts:
229 if exclude_posts is None or post not in exclude_posts:
230 # Manual update is required because uids are generated on save
230 # Manual update is required because uids are generated on save
231 post.last_edit_time = last_edit_time
231 post.last_edit_time = last_edit_time
232 post.save(update_fields=['last_edit_time'])
232 post.save(update_fields=['last_edit_time'])
233
233
234 post.get_threads().update(last_edit_time=last_edit_time)
234 post.get_threads().update(last_edit_time=last_edit_time)
235
235
236 def notify_clients(self):
236 def notify_clients(self):
237 if not settings.get_bool('External', 'WebsocketsEnabled'):
237 if not settings.get_bool('External', 'WebsocketsEnabled'):
238 return
238 return
239
239
240 client = Client()
240 client = Client()
241
241
242 channel_name = WS_CHANNEL_THREAD + str(self.get_opening_post_id())
242 channel_name = WS_CHANNEL_THREAD + str(self.get_opening_post_id())
243 client.publish(channel_name, {
243 client.publish(channel_name, {
244 WS_NOTIFICATION_TYPE: WS_NOTIFICATION_TYPE_NEW_POST,
244 WS_NOTIFICATION_TYPE: WS_NOTIFICATION_TYPE_NEW_POST,
245 })
245 })
246 client.send()
246 client.send()
247
247
248 def get_absolute_url(self):
248 def get_absolute_url(self):
249 return self.get_opening_post().get_absolute_url()
249 return self.get_opening_post().get_absolute_url()
250
250
251 def get_required_tags(self):
251 def get_required_tags(self):
252 return self.get_tags().filter(required=True)
252 return self.get_tags().filter(required=True)
253
253
254 def get_replies_newer(self, post_id):
254 def get_replies_newer(self, post_id):
255 return self.get_replies().filter(id__gt=post_id)
255 return self.get_replies().filter(id__gt=post_id)
256
256
257 def is_archived(self):
257 def is_archived(self):
258 return self.archived
258 return self.archived
@@ -1,239 +1,240 b''
1 # Django settings for neboard project.
1 # Django settings for neboard project.
2 import os
2 import os
3
3
4 DEBUG = True
4 DEBUG = True
5 TEMPLATE_DEBUG = DEBUG
5 TEMPLATE_DEBUG = DEBUG
6
6
7 ADMINS = (
7 ADMINS = (
8 # ('Your Name', 'your_email@example.com'),
8 # ('Your Name', 'your_email@example.com'),
9 ('admin', 'admin@example.com')
9 ('admin', 'admin@example.com')
10 )
10 )
11
11
12 MANAGERS = ADMINS
12 MANAGERS = ADMINS
13
13
14 DATABASES = {
14 DATABASES = {
15 'default': {
15 'default': {
16 'ENGINE': 'django.db.backends.sqlite3', # Add 'postgresql_psycopg2', 'mysql', 'sqlite3' or 'oracle'.
16 'ENGINE': 'django.db.backends.sqlite3', # Add 'postgresql_psycopg2', 'mysql', 'sqlite3' or 'oracle'.
17 'NAME': 'database.db', # Or path to database file if using sqlite3.
17 'NAME': 'database.db', # Or path to database file if using sqlite3.
18 'USER': '', # Not used with sqlite3.
18 'USER': '', # Not used with sqlite3.
19 'PASSWORD': '', # Not used with sqlite3.
19 'PASSWORD': '', # Not used with sqlite3.
20 'HOST': '', # Set to empty string for localhost. Not used with sqlite3.
20 'HOST': '', # Set to empty string for localhost. Not used with sqlite3.
21 'PORT': '', # Set to empty string for default. Not used with sqlite3.
21 'PORT': '', # Set to empty string for default. Not used with sqlite3.
22 'CONN_MAX_AGE': None,
22 'CONN_MAX_AGE': None,
23 }
23 }
24 }
24 }
25
25
26 # Local time zone for this installation. Choices can be found here:
26 # Local time zone for this installation. Choices can be found here:
27 # http://en.wikipedia.org/wiki/List_of_tz_zones_by_name
27 # http://en.wikipedia.org/wiki/List_of_tz_zones_by_name
28 # although not all choices may be available on all operating systems.
28 # although not all choices may be available on all operating systems.
29 # In a Windows environment this must be set to your system time zone.
29 # In a Windows environment this must be set to your system time zone.
30 TIME_ZONE = 'Europe/Kiev'
30 TIME_ZONE = 'Europe/Kiev'
31
31
32 # Language code for this installation. All choices can be found here:
32 # Language code for this installation. All choices can be found here:
33 # http://www.i18nguy.com/unicode/language-identifiers.html
33 # http://www.i18nguy.com/unicode/language-identifiers.html
34 LANGUAGE_CODE = 'en'
34 LANGUAGE_CODE = 'en'
35
35
36 SITE_ID = 1
36 SITE_ID = 1
37
37
38 # If you set this to False, Django will make some optimizations so as not
38 # If you set this to False, Django will make some optimizations so as not
39 # to load the internationalization machinery.
39 # to load the internationalization machinery.
40 USE_I18N = True
40 USE_I18N = True
41
41
42 # If you set this to False, Django will not format dates, numbers and
42 # If you set this to False, Django will not format dates, numbers and
43 # calendars according to the current locale.
43 # calendars according to the current locale.
44 USE_L10N = True
44 USE_L10N = True
45
45
46 # If you set this to False, Django will not use timezone-aware datetimes.
46 # If you set this to False, Django will not use timezone-aware datetimes.
47 USE_TZ = True
47 USE_TZ = True
48
48
49 USE_ETAGS = True
49 USE_ETAGS = True
50
50
51 # Absolute filesystem path to the directory that will hold user-uploaded files.
51 # Absolute filesystem path to the directory that will hold user-uploaded files.
52 # Example: "/home/media/media.lawrence.com/media/"
52 # Example: "/home/media/media.lawrence.com/media/"
53 MEDIA_ROOT = './media/'
53 MEDIA_ROOT = './media/'
54
54
55 # URL that handles the media served from MEDIA_ROOT. Make sure to use a
55 # URL that handles the media served from MEDIA_ROOT. Make sure to use a
56 # trailing slash.
56 # trailing slash.
57 # Examples: "http://media.lawrence.com/media/", "http://example.com/media/"
57 # Examples: "http://media.lawrence.com/media/", "http://example.com/media/"
58 MEDIA_URL = '/media/'
58 MEDIA_URL = '/media/'
59
59
60 # Absolute path to the directory static files should be collected to.
60 # Absolute path to the directory static files should be collected to.
61 # Don't put anything in this directory yourself; store your static files
61 # Don't put anything in this directory yourself; store your static files
62 # in apps' "static/" subdirectories and in STATICFILES_DIRS.
62 # in apps' "static/" subdirectories and in STATICFILES_DIRS.
63 # Example: "/home/media/media.lawrence.com/static/"
63 # Example: "/home/media/media.lawrence.com/static/"
64 STATIC_ROOT = ''
64 STATIC_ROOT = ''
65
65
66 # URL prefix for static files.
66 # URL prefix for static files.
67 # Example: "http://media.lawrence.com/static/"
67 # Example: "http://media.lawrence.com/static/"
68 STATIC_URL = '/static/'
68 STATIC_URL = '/static/'
69
69
70 # Additional locations of static files
70 # Additional locations of static files
71 # It is really a hack, put real paths, not related
71 # It is really a hack, put real paths, not related
72 STATICFILES_DIRS = (
72 STATICFILES_DIRS = (
73 os.path.dirname(__file__) + '/boards/static',
73 os.path.dirname(__file__) + '/boards/static',
74
74
75 # '/d/work/python/django/neboard/neboard/boards/static',
75 # '/d/work/python/django/neboard/neboard/boards/static',
76 # Put strings here, like "/home/html/static" or "C:/www/django/static".
76 # Put strings here, like "/home/html/static" or "C:/www/django/static".
77 # Always use forward slashes, even on Windows.
77 # Always use forward slashes, even on Windows.
78 # Don't forget to use absolute paths, not relative paths.
78 # Don't forget to use absolute paths, not relative paths.
79 )
79 )
80
80
81 # List of finder classes that know how to find static files in
81 # List of finder classes that know how to find static files in
82 # various locations.
82 # various locations.
83 STATICFILES_FINDERS = (
83 STATICFILES_FINDERS = (
84 'django.contrib.staticfiles.finders.FileSystemFinder',
84 'django.contrib.staticfiles.finders.FileSystemFinder',
85 'django.contrib.staticfiles.finders.AppDirectoriesFinder',
85 'django.contrib.staticfiles.finders.AppDirectoriesFinder',
86 )
86 )
87
87
88 if DEBUG:
88 if DEBUG:
89 STATICFILES_STORAGE = \
89 STATICFILES_STORAGE = \
90 'django.contrib.staticfiles.storage.StaticFilesStorage'
90 'django.contrib.staticfiles.storage.StaticFilesStorage'
91 else:
91 else:
92 STATICFILES_STORAGE = \
92 STATICFILES_STORAGE = \
93 'django.contrib.staticfiles.storage.CachedStaticFilesStorage'
93 'django.contrib.staticfiles.storage.CachedStaticFilesStorage'
94
94
95 # Make this unique, and don't share it with anybody.
95 # Make this unique, and don't share it with anybody.
96 SECRET_KEY = '@1rc$o(7=tt#kd+4s$u6wchm**z^)4x90)7f6z(i&amp;55@o11*8o'
96 SECRET_KEY = '@1rc$o(7=tt#kd+4s$u6wchm**z^)4x90)7f6z(i&amp;55@o11*8o'
97
97
98 # List of callables that know how to import templates from various sources.
98 # List of callables that know how to import templates from various sources.
99 TEMPLATE_LOADERS = (
99 TEMPLATE_LOADERS = (
100 'django.template.loaders.filesystem.Loader',
100 'django.template.loaders.filesystem.Loader',
101 'django.template.loaders.app_directories.Loader',
101 'django.template.loaders.app_directories.Loader',
102 )
102 )
103
103
104 TEMPLATE_CONTEXT_PROCESSORS = (
104 TEMPLATE_CONTEXT_PROCESSORS = (
105 'django.core.context_processors.media',
105 'django.core.context_processors.media',
106 'django.core.context_processors.static',
106 'django.core.context_processors.static',
107 'django.core.context_processors.request',
107 'django.core.context_processors.request',
108 'django.contrib.auth.context_processors.auth',
108 'django.contrib.auth.context_processors.auth',
109 'boards.context_processors.user_and_ui_processor',
109 'boards.context_processors.user_and_ui_processor',
110 )
110 )
111
111
112 MIDDLEWARE_CLASSES = (
112 MIDDLEWARE_CLASSES = [
113 'django.middleware.http.ConditionalGetMiddleware',
113 'django.middleware.http.ConditionalGetMiddleware',
114 'django.contrib.sessions.middleware.SessionMiddleware',
114 'django.contrib.sessions.middleware.SessionMiddleware',
115 'django.middleware.locale.LocaleMiddleware',
115 'django.middleware.locale.LocaleMiddleware',
116 'django.middleware.common.CommonMiddleware',
116 'django.middleware.common.CommonMiddleware',
117 'django.contrib.auth.middleware.AuthenticationMiddleware',
117 'django.contrib.auth.middleware.AuthenticationMiddleware',
118 'django.contrib.messages.middleware.MessageMiddleware',
118 'django.contrib.messages.middleware.MessageMiddleware',
119 'boards.middlewares.BanMiddleware',
119 'boards.middlewares.BanMiddleware',
120 'boards.middlewares.TimezoneMiddleware',
120 'boards.middlewares.TimezoneMiddleware',
121 )
121 ]
122
122
123 ROOT_URLCONF = 'neboard.urls'
123 ROOT_URLCONF = 'neboard.urls'
124
124
125 # Python dotted path to the WSGI application used by Django's runserver.
125 # Python dotted path to the WSGI application used by Django's runserver.
126 WSGI_APPLICATION = 'neboard.wsgi.application'
126 WSGI_APPLICATION = 'neboard.wsgi.application'
127
127
128 TEMPLATE_DIRS = (
128 TEMPLATE_DIRS = (
129 # Put strings here, like "/home/html/django_templates" or "C:/www/django/templates".
129 # Put strings here, like "/home/html/django_templates" or "C:/www/django/templates".
130 # Always use forward slashes, even on Windows.
130 # Always use forward slashes, even on Windows.
131 # Don't forget to use absolute paths, not relative paths.
131 # Don't forget to use absolute paths, not relative paths.
132 'templates',
132 'templates',
133 )
133 )
134
134
135 INSTALLED_APPS = (
135 INSTALLED_APPS = (
136 'django.contrib.auth',
136 'django.contrib.auth',
137 'django.contrib.contenttypes',
137 'django.contrib.contenttypes',
138 'django.contrib.sessions',
138 'django.contrib.sessions',
139 # 'django.contrib.sites',
139 # 'django.contrib.sites',
140 'django.contrib.messages',
140 'django.contrib.messages',
141 'django.contrib.staticfiles',
141 'django.contrib.staticfiles',
142 # Uncomment the next line to enable the admin:
142 # Uncomment the next line to enable the admin:
143 'django.contrib.admin',
143 'django.contrib.admin',
144 # Uncomment the next line to enable admin documentation:
144 # Uncomment the next line to enable admin documentation:
145 # 'django.contrib.admindocs',
145 # 'django.contrib.admindocs',
146 #'django.contrib.humanize',
146 #'django.contrib.humanize',
147
147
148 'debug_toolbar',
148 'debug_toolbar',
149
149
150 # Search
150 # Search
151 'haystack',
151 'haystack',
152
152
153 'boards',
153 'boards',
154 )
154 )
155
155
156 # A sample logging configuration. The only tangible logging
156 # A sample logging configuration. The only tangible logging
157 # performed by this configuration is to send an email to
157 # performed by this configuration is to send an email to
158 # the site admins on every HTTP 500 error when DEBUG=False.
158 # the site admins on every HTTP 500 error when DEBUG=False.
159 # See http://docs.djangoproject.com/en/dev/topics/logging for
159 # See http://docs.djangoproject.com/en/dev/topics/logging for
160 # more details on how to customize your logging configuration.
160 # more details on how to customize your logging configuration.
161 LOGGING = {
161 LOGGING = {
162 'version': 1,
162 'version': 1,
163 'disable_existing_loggers': False,
163 'disable_existing_loggers': False,
164 'formatters': {
164 'formatters': {
165 'verbose': {
165 'verbose': {
166 'format': '%(levelname)s %(asctime)s %(name)s %(process)d %(thread)d %(message)s'
166 'format': '%(levelname)s %(asctime)s %(name)s %(process)d %(thread)d %(message)s'
167 },
167 },
168 'simple': {
168 'simple': {
169 'format': '%(levelname)s %(asctime)s [%(name)s] %(message)s'
169 'format': '%(levelname)s %(asctime)s [%(name)s] %(message)s'
170 },
170 },
171 },
171 },
172 'filters': {
172 'filters': {
173 'require_debug_false': {
173 'require_debug_false': {
174 '()': 'django.utils.log.RequireDebugFalse'
174 '()': 'django.utils.log.RequireDebugFalse'
175 }
175 }
176 },
176 },
177 'handlers': {
177 'handlers': {
178 'console': {
178 'console': {
179 'level': 'DEBUG',
179 'level': 'DEBUG',
180 'class': 'logging.StreamHandler',
180 'class': 'logging.StreamHandler',
181 'formatter': 'simple'
181 'formatter': 'simple'
182 },
182 },
183 },
183 },
184 'loggers': {
184 'loggers': {
185 'boards': {
185 'boards': {
186 'handlers': ['console'],
186 'handlers': ['console'],
187 'level': 'DEBUG',
187 'level': 'DEBUG',
188 }
188 }
189 },
189 },
190 }
190 }
191
191
192 HAYSTACK_CONNECTIONS = {
192 HAYSTACK_CONNECTIONS = {
193 'default': {
193 'default': {
194 'ENGINE': 'haystack.backends.whoosh_backend.WhooshEngine',
194 'ENGINE': 'haystack.backends.whoosh_backend.WhooshEngine',
195 'PATH': os.path.join(os.path.dirname(__file__), 'whoosh_index'),
195 'PATH': os.path.join(os.path.dirname(__file__), 'whoosh_index'),
196 },
196 },
197 }
197 }
198
198
199 THEMES = [
199 THEMES = [
200 ('md', 'Mystic Dark'),
200 ('md', 'Mystic Dark'),
201 ('md_centered', 'Mystic Dark (centered)'),
201 ('md_centered', 'Mystic Dark (centered)'),
202 ('sw', 'Snow White'),
202 ('sw', 'Snow White'),
203 ('pg', 'Photon Gray'),
203 ('pg', 'Photon Gray'),
204 ]
204 ]
205
205
206 IMAGE_VIEWERS = [
206 IMAGE_VIEWERS = [
207 ('simple', 'Simple'),
207 ('simple', 'Simple'),
208 ('popup', 'Popup'),
208 ('popup', 'Popup'),
209 ]
209 ]
210
210
211 ALLOWED_HOSTS = ['*']
212
211 POSTING_DELAY = 20 # seconds
213 POSTING_DELAY = 20 # seconds
212
214
213 # Websocket settins
215 # Websocket settins
214 CENTRIFUGE_HOST = 'localhost'
216 CENTRIFUGE_HOST = 'localhost'
215 CENTRIFUGE_PORT = '9090'
217 CENTRIFUGE_PORT = '9090'
216
218
217 CENTRIFUGE_ADDRESS = 'http://{}:{}'.format(CENTRIFUGE_HOST, CENTRIFUGE_PORT)
219 CENTRIFUGE_ADDRESS = 'http://{}:{}'.format(CENTRIFUGE_HOST, CENTRIFUGE_PORT)
218 CENTRIFUGE_PROJECT_ID = '<project id here>'
220 CENTRIFUGE_PROJECT_ID = '<project id here>'
219 CENTRIFUGE_PROJECT_SECRET = '<project secret here>'
221 CENTRIFUGE_PROJECT_SECRET = '<project secret here>'
220 CENTRIFUGE_TIMEOUT = 5
222 CENTRIFUGE_TIMEOUT = 5
221
223
222 # Debug mode middlewares
224 # Debug middlewares
223 if DEBUG:
225 MIDDLEWARE_CLASSES += [
224 MIDDLEWARE_CLASSES += (
226 'debug_toolbar.middleware.DebugToolbarMiddleware',
225 'debug_toolbar.middleware.DebugToolbarMiddleware',
227 ]
226 )
227
228
228 def custom_show_toolbar(request):
229 def custom_show_toolbar(request):
229 return True
230 return request.user.has_perm('admin.debug')
230
231
231 DEBUG_TOOLBAR_CONFIG = {
232 DEBUG_TOOLBAR_CONFIG = {
232 'ENABLE_STACKTRACES': True,
233 'ENABLE_STACKTRACES': True,
233 'SHOW_TOOLBAR_CALLBACK': 'neboard.settings.custom_show_toolbar',
234 'SHOW_TOOLBAR_CALLBACK': 'neboard.settings.custom_show_toolbar',
234 }
235 }
235
236
236 # FIXME Uncommenting this fails somehow. Need to investigate this
237 # FIXME Uncommenting this fails somehow. Need to investigate this
237 #DEBUG_TOOLBAR_PANELS += (
238 #DEBUG_TOOLBAR_PANELS += (
238 # 'debug_toolbar.panels.profiling.ProfilingDebugPanel',
239 # 'debug_toolbar.panels.profiling.ProfilingDebugPanel',
239 #)
240 #)
General Comments 0
You need to be logged in to leave comments. Login now