##// END OF EJS Templates
Moved imageboard settings to the boards settings module. Added setting to disable archive
neko259 -
r716:a6b9dd95 1.8.1 default
parent child Browse files
Show More
@@ -0,0 +1,1 b''
1 import settings No newline at end of file
@@ -1,31 +1,30 b''
1 from boards import utils
1 from boards import utils, settings
2 from boards.models import Post
2 from boards.models import Post
3 from boards.models.post import SETTING_MODERATE
3 from boards.models.post import SETTING_MODERATE
4 import neboard
5
4
6 __author__ = 'neko259'
5 __author__ = 'neko259'
7
6
8
7
9 def user_and_ui_processor(request):
8 def user_and_ui_processor(request):
10 context = {}
9 context = {}
11
10
12 user = utils.get_user(request)
11 user = utils.get_user(request)
13 context['user'] = user
12 context['user'] = user
14 context['tags'] = user.fav_tags.all()
13 context['tags'] = user.fav_tags.all()
15 context['posts_per_day'] = float(Post.objects.get_posts_per_day())
14 context['posts_per_day'] = float(Post.objects.get_posts_per_day())
16
15
17 theme = utils.get_theme(request, user)
16 theme = utils.get_theme(request, user)
18 context['theme'] = theme
17 context['theme'] = theme
19 context['theme_css'] = 'css/' + theme + '/base_page.css'
18 context['theme_css'] = 'css/' + theme + '/base_page.css'
20
19
21 # This shows the moderator panel
20 # This shows the moderator panel
22 moderate = user.get_setting(SETTING_MODERATE)
21 moderate = user.get_setting(SETTING_MODERATE)
23 if moderate == 'True':
22 if moderate == 'True':
24 context['moderator'] = user.is_moderator()
23 context['moderator'] = user.is_moderator()
25 else:
24 else:
26 context['moderator'] = False
25 context['moderator'] = False
27
26
28 context['version'] = neboard.settings.VERSION
27 context['version'] = settings.VERSION
29 context['site_name'] = neboard.settings.SITE_NAME
28 context['site_name'] = settings.SITE_NAME
30
29
31 return context No newline at end of file
30 return context
@@ -1,351 +1,351 b''
1 import re
1 import re
2 import time
2 import time
3 import hashlib
3 import hashlib
4
4
5 from captcha.fields import CaptchaField
5 from captcha.fields import CaptchaField
6 from django import forms
6 from django import forms
7 from django.forms.util import ErrorList
7 from django.forms.util import ErrorList
8 from django.utils.translation import ugettext_lazy as _
8 from django.utils.translation import ugettext_lazy as _
9
9
10 from boards.mdx_neboard import formatters
10 from boards.mdx_neboard import formatters
11 from boards.models.post import TITLE_MAX_LENGTH
11 from boards.models.post import TITLE_MAX_LENGTH
12 from boards.models import User, Post, PostImage
12 from boards.models import User, PostImage
13 from neboard import settings
13 from neboard import settings
14 from boards import utils
14 from boards import utils
15 import boards.settings as board_settings
15 import boards.settings as board_settings
16
16
17 VETERAN_POSTING_DELAY = 5
17 VETERAN_POSTING_DELAY = 5
18
18
19 ATTRIBUTE_PLACEHOLDER = 'placeholder'
19 ATTRIBUTE_PLACEHOLDER = 'placeholder'
20
20
21 LAST_POST_TIME = 'last_post_time'
21 LAST_POST_TIME = 'last_post_time'
22 LAST_LOGIN_TIME = 'last_login_time'
22 LAST_LOGIN_TIME = 'last_login_time'
23 TEXT_PLACEHOLDER = _('''Type message here. You can reply to message >>123 like
23 TEXT_PLACEHOLDER = _('''Type message here. You can reply to message >>123 like
24 this. 2 new lines are required to start new paragraph.''')
24 this. 2 new lines are required to start new paragraph.''')
25 TAGS_PLACEHOLDER = _('tag1 several_words_tag')
25 TAGS_PLACEHOLDER = _('tag1 several_words_tag')
26
26
27 ERROR_IMAGE_DUPLICATE = _('Such image was already posted')
27 ERROR_IMAGE_DUPLICATE = _('Such image was already posted')
28
28
29 LABEL_TITLE = _('Title')
29 LABEL_TITLE = _('Title')
30 LABEL_TEXT = _('Text')
30 LABEL_TEXT = _('Text')
31 LABEL_TAG = _('Tag')
31 LABEL_TAG = _('Tag')
32
32
33 TAG_MAX_LENGTH = 20
33 TAG_MAX_LENGTH = 20
34
34
35 REGEX_TAG = ur'^[\w\d]+$'
35 REGEX_TAG = ur'^[\w\d]+$'
36
36
37
37
38 class FormatPanel(forms.Textarea):
38 class FormatPanel(forms.Textarea):
39 def render(self, name, value, attrs=None):
39 def render(self, name, value, attrs=None):
40 output = '<div id="mark-panel">'
40 output = '<div id="mark-panel">'
41 for formatter in formatters:
41 for formatter in formatters:
42 output += u'<span class="mark_btn"' + \
42 output += u'<span class="mark_btn"' + \
43 u' onClick="addMarkToMsg(\'' + formatter.format_left + \
43 u' onClick="addMarkToMsg(\'' + formatter.format_left + \
44 '\', \'' + formatter.format_right + '\')">' + \
44 '\', \'' + formatter.format_right + '\')">' + \
45 formatter.preview_left + formatter.name + \
45 formatter.preview_left + formatter.name + \
46 formatter.preview_right + u'</span>'
46 formatter.preview_right + u'</span>'
47
47
48 output += '</div>'
48 output += '</div>'
49 output += super(FormatPanel, self).render(name, value, attrs=None)
49 output += super(FormatPanel, self).render(name, value, attrs=None)
50
50
51 return output
51 return output
52
52
53
53
54 class PlainErrorList(ErrorList):
54 class PlainErrorList(ErrorList):
55 def __unicode__(self):
55 def __unicode__(self):
56 return self.as_text()
56 return self.as_text()
57
57
58 def as_text(self):
58 def as_text(self):
59 return ''.join([u'(!) %s ' % e for e in self])
59 return ''.join([u'(!) %s ' % e for e in self])
60
60
61
61
62 class NeboardForm(forms.Form):
62 class NeboardForm(forms.Form):
63
63
64 def as_div(self):
64 def as_div(self):
65 """
65 """
66 Returns this form rendered as HTML <as_div>s.
66 Returns this form rendered as HTML <as_div>s.
67 """
67 """
68
68
69 return self._html_output(
69 return self._html_output(
70 # TODO Do not show hidden rows in the list here
70 # TODO Do not show hidden rows in the list here
71 normal_row='<div class="form-row"><div class="form-label">'
71 normal_row='<div class="form-row"><div class="form-label">'
72 '%(label)s'
72 '%(label)s'
73 '</div></div>'
73 '</div></div>'
74 '<div class="form-row"><div class="form-input">'
74 '<div class="form-row"><div class="form-input">'
75 '%(field)s'
75 '%(field)s'
76 '</div></div>'
76 '</div></div>'
77 '<div class="form-row">'
77 '<div class="form-row">'
78 '%(help_text)s'
78 '%(help_text)s'
79 '</div>',
79 '</div>',
80 error_row='<div class="form-row">'
80 error_row='<div class="form-row">'
81 '<div class="form-label"></div>'
81 '<div class="form-label"></div>'
82 '<div class="form-errors">%s</div>'
82 '<div class="form-errors">%s</div>'
83 '</div>',
83 '</div>',
84 row_ender='</div>',
84 row_ender='</div>',
85 help_text_html='%s',
85 help_text_html='%s',
86 errors_on_separate_row=True)
86 errors_on_separate_row=True)
87
87
88 def as_json_errors(self):
88 def as_json_errors(self):
89 errors = []
89 errors = []
90
90
91 for name, field in self.fields.items():
91 for name, field in self.fields.items():
92 if self[name].errors:
92 if self[name].errors:
93 errors.append({
93 errors.append({
94 'field': name,
94 'field': name,
95 'errors': self[name].errors.as_text(),
95 'errors': self[name].errors.as_text(),
96 })
96 })
97
97
98 return errors
98 return errors
99
99
100
100
101 class PostForm(NeboardForm):
101 class PostForm(NeboardForm):
102
102
103 title = forms.CharField(max_length=TITLE_MAX_LENGTH, required=False,
103 title = forms.CharField(max_length=TITLE_MAX_LENGTH, required=False,
104 label=LABEL_TITLE)
104 label=LABEL_TITLE)
105 text = forms.CharField(
105 text = forms.CharField(
106 widget=FormatPanel(attrs={ATTRIBUTE_PLACEHOLDER: TEXT_PLACEHOLDER}),
106 widget=FormatPanel(attrs={ATTRIBUTE_PLACEHOLDER: TEXT_PLACEHOLDER}),
107 required=False, label=LABEL_TEXT)
107 required=False, label=LABEL_TEXT)
108 image = forms.ImageField(required=False, label=_('Image'),
108 image = forms.ImageField(required=False, label=_('Image'),
109 widget=forms.ClearableFileInput(attrs={'accept': 'image/*'}))
109 widget=forms.ClearableFileInput(attrs={'accept': 'image/*'}))
110
110
111 # This field is for spam prevention only
111 # This field is for spam prevention only
112 email = forms.CharField(max_length=100, required=False, label=_('e-mail'),
112 email = forms.CharField(max_length=100, required=False, label=_('e-mail'),
113 widget=forms.TextInput(attrs={
113 widget=forms.TextInput(attrs={
114 'class': 'form-email'}))
114 'class': 'form-email'}))
115
115
116 session = None
116 session = None
117 need_to_ban = False
117 need_to_ban = False
118
118
119 def clean_title(self):
119 def clean_title(self):
120 title = self.cleaned_data['title']
120 title = self.cleaned_data['title']
121 if title:
121 if title:
122 if len(title) > TITLE_MAX_LENGTH:
122 if len(title) > TITLE_MAX_LENGTH:
123 raise forms.ValidationError(_('Title must have less than %s '
123 raise forms.ValidationError(_('Title must have less than %s '
124 'characters') %
124 'characters') %
125 str(TITLE_MAX_LENGTH))
125 str(TITLE_MAX_LENGTH))
126 return title
126 return title
127
127
128 def clean_text(self):
128 def clean_text(self):
129 text = self.cleaned_data['text'].strip()
129 text = self.cleaned_data['text'].strip()
130 if text:
130 if text:
131 if len(text) > board_settings.MAX_TEXT_LENGTH:
131 if len(text) > board_settings.MAX_TEXT_LENGTH:
132 raise forms.ValidationError(_('Text must have less than %s '
132 raise forms.ValidationError(_('Text must have less than %s '
133 'characters') %
133 'characters') %
134 str(board_settings
134 str(board_settings
135 .MAX_TEXT_LENGTH))
135 .MAX_TEXT_LENGTH))
136 return text
136 return text
137
137
138 def clean_image(self):
138 def clean_image(self):
139 image = self.cleaned_data['image']
139 image = self.cleaned_data['image']
140 if image:
140 if image:
141 if image._size > board_settings.MAX_IMAGE_SIZE:
141 if image._size > board_settings.MAX_IMAGE_SIZE:
142 raise forms.ValidationError(
142 raise forms.ValidationError(
143 _('Image must be less than %s bytes')
143 _('Image must be less than %s bytes')
144 % str(board_settings.MAX_IMAGE_SIZE))
144 % str(board_settings.MAX_IMAGE_SIZE))
145
145
146 md5 = hashlib.md5()
146 md5 = hashlib.md5()
147 for chunk in image.chunks():
147 for chunk in image.chunks():
148 md5.update(chunk)
148 md5.update(chunk)
149 image_hash = md5.hexdigest()
149 image_hash = md5.hexdigest()
150 if PostImage.objects.filter(hash=image_hash).exists():
150 if PostImage.objects.filter(hash=image_hash).exists():
151 raise forms.ValidationError(ERROR_IMAGE_DUPLICATE)
151 raise forms.ValidationError(ERROR_IMAGE_DUPLICATE)
152
152
153 return image
153 return image
154
154
155 def clean(self):
155 def clean(self):
156 cleaned_data = super(PostForm, self).clean()
156 cleaned_data = super(PostForm, self).clean()
157
157
158 if not self.session:
158 if not self.session:
159 raise forms.ValidationError('Humans have sessions')
159 raise forms.ValidationError('Humans have sessions')
160
160
161 if cleaned_data['email']:
161 if cleaned_data['email']:
162 self.need_to_ban = True
162 self.need_to_ban = True
163 raise forms.ValidationError('A human cannot enter a hidden field')
163 raise forms.ValidationError('A human cannot enter a hidden field')
164
164
165 if not self.errors:
165 if not self.errors:
166 self._clean_text_image()
166 self._clean_text_image()
167
167
168 if not self.errors and self.session:
168 if not self.errors and self.session:
169 self._validate_posting_speed()
169 self._validate_posting_speed()
170
170
171 return cleaned_data
171 return cleaned_data
172
172
173 def _clean_text_image(self):
173 def _clean_text_image(self):
174 text = self.cleaned_data.get('text')
174 text = self.cleaned_data.get('text')
175 image = self.cleaned_data.get('image')
175 image = self.cleaned_data.get('image')
176
176
177 if (not text) and (not image):
177 if (not text) and (not image):
178 error_message = _('Either text or image must be entered.')
178 error_message = _('Either text or image must be entered.')
179 self._errors['text'] = self.error_class([error_message])
179 self._errors['text'] = self.error_class([error_message])
180
180
181 def _validate_posting_speed(self):
181 def _validate_posting_speed(self):
182 can_post = True
182 can_post = True
183
183
184 # TODO Remove this, it's only for test
184 # TODO Remove this, it's only for test
185 if not 'user_id' in self.session:
185 if not 'user_id' in self.session:
186 return
186 return
187
187
188 user = User.objects.get(id=self.session['user_id'])
188 user = User.objects.get(id=self.session['user_id'])
189 if user.is_veteran():
189 if user.is_veteran():
190 posting_delay = VETERAN_POSTING_DELAY
190 posting_delay = VETERAN_POSTING_DELAY
191 else:
191 else:
192 posting_delay = settings.POSTING_DELAY
192 posting_delay = settings.POSTING_DELAY
193
193
194 if LAST_POST_TIME in self.session:
194 if LAST_POST_TIME in self.session:
195 now = time.time()
195 now = time.time()
196 last_post_time = self.session[LAST_POST_TIME]
196 last_post_time = self.session[LAST_POST_TIME]
197
197
198 current_delay = int(now - last_post_time)
198 current_delay = int(now - last_post_time)
199
199
200 if current_delay < posting_delay:
200 if current_delay < posting_delay:
201 error_message = _('Wait %s seconds after last posting') % str(
201 error_message = _('Wait %s seconds after last posting') % str(
202 posting_delay - current_delay)
202 posting_delay - current_delay)
203 self._errors['text'] = self.error_class([error_message])
203 self._errors['text'] = self.error_class([error_message])
204
204
205 can_post = False
205 can_post = False
206
206
207 if can_post:
207 if can_post:
208 self.session[LAST_POST_TIME] = time.time()
208 self.session[LAST_POST_TIME] = time.time()
209
209
210
210
211 class ThreadForm(PostForm):
211 class ThreadForm(PostForm):
212
212
213 regex_tags = re.compile(ur'^[\w\s\d]+$', re.UNICODE)
213 regex_tags = re.compile(ur'^[\w\s\d]+$', re.UNICODE)
214
214
215 tags = forms.CharField(
215 tags = forms.CharField(
216 widget=forms.TextInput(attrs={ATTRIBUTE_PLACEHOLDER: TAGS_PLACEHOLDER}),
216 widget=forms.TextInput(attrs={ATTRIBUTE_PLACEHOLDER: TAGS_PLACEHOLDER}),
217 max_length=100, label=_('Tags'), required=True)
217 max_length=100, label=_('Tags'), required=True)
218
218
219 def clean_tags(self):
219 def clean_tags(self):
220 tags = self.cleaned_data['tags'].strip()
220 tags = self.cleaned_data['tags'].strip()
221
221
222 if not tags or not self.regex_tags.match(tags):
222 if not tags or not self.regex_tags.match(tags):
223 raise forms.ValidationError(
223 raise forms.ValidationError(
224 _('Inappropriate characters in tags.'))
224 _('Inappropriate characters in tags.'))
225
225
226 return tags
226 return tags
227
227
228 def clean(self):
228 def clean(self):
229 cleaned_data = super(ThreadForm, self).clean()
229 cleaned_data = super(ThreadForm, self).clean()
230
230
231 return cleaned_data
231 return cleaned_data
232
232
233
233
234 class PostCaptchaForm(PostForm):
234 class PostCaptchaForm(PostForm):
235 captcha = CaptchaField()
235 captcha = CaptchaField()
236
236
237 def __init__(self, *args, **kwargs):
237 def __init__(self, *args, **kwargs):
238 self.request = kwargs['request']
238 self.request = kwargs['request']
239 del kwargs['request']
239 del kwargs['request']
240
240
241 super(PostCaptchaForm, self).__init__(*args, **kwargs)
241 super(PostCaptchaForm, self).__init__(*args, **kwargs)
242
242
243 def clean(self):
243 def clean(self):
244 cleaned_data = super(PostCaptchaForm, self).clean()
244 cleaned_data = super(PostCaptchaForm, self).clean()
245
245
246 success = self.is_valid()
246 success = self.is_valid()
247 utils.update_captcha_access(self.request, success)
247 utils.update_captcha_access(self.request, success)
248
248
249 if success:
249 if success:
250 return cleaned_data
250 return cleaned_data
251 else:
251 else:
252 raise forms.ValidationError(_("Captcha validation failed"))
252 raise forms.ValidationError(_("Captcha validation failed"))
253
253
254
254
255 class ThreadCaptchaForm(ThreadForm):
255 class ThreadCaptchaForm(ThreadForm):
256 captcha = CaptchaField()
256 captcha = CaptchaField()
257
257
258 def __init__(self, *args, **kwargs):
258 def __init__(self, *args, **kwargs):
259 self.request = kwargs['request']
259 self.request = kwargs['request']
260 del kwargs['request']
260 del kwargs['request']
261
261
262 super(ThreadCaptchaForm, self).__init__(*args, **kwargs)
262 super(ThreadCaptchaForm, self).__init__(*args, **kwargs)
263
263
264 def clean(self):
264 def clean(self):
265 cleaned_data = super(ThreadCaptchaForm, self).clean()
265 cleaned_data = super(ThreadCaptchaForm, self).clean()
266
266
267 success = self.is_valid()
267 success = self.is_valid()
268 utils.update_captcha_access(self.request, success)
268 utils.update_captcha_access(self.request, success)
269
269
270 if success:
270 if success:
271 return cleaned_data
271 return cleaned_data
272 else:
272 else:
273 raise forms.ValidationError(_("Captcha validation failed"))
273 raise forms.ValidationError(_("Captcha validation failed"))
274
274
275
275
276 class SettingsForm(NeboardForm):
276 class SettingsForm(NeboardForm):
277
277
278 theme = forms.ChoiceField(choices=settings.THEMES,
278 theme = forms.ChoiceField(choices=settings.THEMES,
279 label=_('Theme'))
279 label=_('Theme'))
280
280
281
281
282 class ModeratorSettingsForm(SettingsForm):
282 class ModeratorSettingsForm(SettingsForm):
283
283
284 moderate = forms.BooleanField(required=False, label=_('Enable moderation '
284 moderate = forms.BooleanField(required=False, label=_('Enable moderation '
285 'panel'))
285 'panel'))
286
286
287
287
288 class LoginForm(NeboardForm):
288 class LoginForm(NeboardForm):
289
289
290 user_id = forms.CharField()
290 user_id = forms.CharField()
291
291
292 session = None
292 session = None
293
293
294 def clean_user_id(self):
294 def clean_user_id(self):
295 user_id = self.cleaned_data['user_id']
295 user_id = self.cleaned_data['user_id']
296 if user_id:
296 if user_id:
297 users = User.objects.filter(user_id=user_id)
297 users = User.objects.filter(user_id=user_id)
298 if len(users) == 0:
298 if len(users) == 0:
299 raise forms.ValidationError(_('No such user found'))
299 raise forms.ValidationError(_('No such user found'))
300
300
301 return user_id
301 return user_id
302
302
303 def _validate_login_speed(self):
303 def _validate_login_speed(self):
304 can_post = True
304 can_post = True
305
305
306 if LAST_LOGIN_TIME in self.session:
306 if LAST_LOGIN_TIME in self.session:
307 now = time.time()
307 now = time.time()
308 last_login_time = self.session[LAST_LOGIN_TIME]
308 last_login_time = self.session[LAST_LOGIN_TIME]
309
309
310 current_delay = int(now - last_login_time)
310 current_delay = int(now - last_login_time)
311
311
312 if current_delay < board_settings.LOGIN_TIMEOUT:
312 if current_delay < board_settings.LOGIN_TIMEOUT:
313 error_message = _('Wait %s minutes after last login') % str(
313 error_message = _('Wait %s minutes after last login') % str(
314 (board_settings.LOGIN_TIMEOUT - current_delay) / 60)
314 (board_settings.LOGIN_TIMEOUT - current_delay) / 60)
315 self._errors['user_id'] = self.error_class([error_message])
315 self._errors['user_id'] = self.error_class([error_message])
316
316
317 can_post = False
317 can_post = False
318
318
319 if can_post:
319 if can_post:
320 self.session[LAST_LOGIN_TIME] = time.time()
320 self.session[LAST_LOGIN_TIME] = time.time()
321
321
322 def clean(self):
322 def clean(self):
323 if not self.session:
323 if not self.session:
324 raise forms.ValidationError('Humans have sessions')
324 raise forms.ValidationError('Humans have sessions')
325
325
326 self._validate_login_speed()
326 self._validate_login_speed()
327
327
328 cleaned_data = super(LoginForm, self).clean()
328 cleaned_data = super(LoginForm, self).clean()
329
329
330 return cleaned_data
330 return cleaned_data
331
331
332
332
333 class AddTagForm(NeboardForm):
333 class AddTagForm(NeboardForm):
334
334
335 tag = forms.CharField(max_length=TAG_MAX_LENGTH, label=LABEL_TAG)
335 tag = forms.CharField(max_length=TAG_MAX_LENGTH, label=LABEL_TAG)
336 method = forms.CharField(widget=forms.HiddenInput(), initial='add_tag')
336 method = forms.CharField(widget=forms.HiddenInput(), initial='add_tag')
337
337
338 def clean_tag(self):
338 def clean_tag(self):
339 tag = self.cleaned_data['tag']
339 tag = self.cleaned_data['tag']
340
340
341 regex_tag = re.compile(REGEX_TAG, re.UNICODE)
341 regex_tag = re.compile(REGEX_TAG, re.UNICODE)
342 if not regex_tag.match(tag):
342 if not regex_tag.match(tag):
343 raise forms.ValidationError(_('Inappropriate characters in tags.'))
343 raise forms.ValidationError(_('Inappropriate characters in tags.'))
344
344
345 return tag
345 return tag
346
346
347 def clean(self):
347 def clean(self):
348 cleaned_data = super(AddTagForm, self).clean()
348 cleaned_data = super(AddTagForm, self).clean()
349
349
350 return cleaned_data
350 return cleaned_data
351
351
@@ -1,354 +1,354 b''
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 os
4 import os
5 from random import random
5 from random import random
6 import time
6 import time
7 import re
7 import re
8 import hashlib
8 import hashlib
9
9
10 from django.core.cache import cache
10 from django.core.cache import cache
11 from django.core.urlresolvers import reverse
11 from django.core.urlresolvers import reverse
12 from django.db import models, transaction
12 from django.db import models, transaction
13 from django.template.loader import render_to_string
13 from django.template.loader import render_to_string
14 from django.utils import timezone
14 from django.utils import timezone
15 from markupfield.fields import MarkupField
15 from markupfield.fields import MarkupField
16 from boards.models import PostImage
16 from boards.models import PostImage
17 from boards.models.base import Viewable
17 from boards.models.base import Viewable
18
18
19 from boards.models.thread import Thread
19 from boards.models.thread import Thread
20 from neboard import settings
20 from neboard import settings
21 from boards import thumbs
21 from boards import thumbs
22
22
23
23
24 APP_LABEL_BOARDS = 'boards'
24 APP_LABEL_BOARDS = 'boards'
25
25
26 CACHE_KEY_PPD = 'ppd'
26 CACHE_KEY_PPD = 'ppd'
27 CACHE_KEY_POST_URL = 'post_url'
27 CACHE_KEY_POST_URL = 'post_url'
28
28
29 POSTS_PER_DAY_RANGE = range(7)
29 POSTS_PER_DAY_RANGE = range(7)
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 DEFAULT_MARKUP_TYPE = 'markdown'
37 DEFAULT_MARKUP_TYPE = 'markdown'
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 SETTING_MODERATE = "moderate"
45 SETTING_MODERATE = "moderate"
46
46
47 REGEX_REPLY = re.compile('>>(\d+)')
47 REGEX_REPLY = re.compile('>>(\d+)')
48
48
49 logger = logging.getLogger(__name__)
49 logger = logging.getLogger(__name__)
50
50
51
51
52 class PostManager(models.Manager):
52 class PostManager(models.Manager):
53
53
54 def create_post(self, title, text, image=None, thread=None,
54 def create_post(self, title, text, image=None, thread=None,
55 ip=NO_IP, tags=None, user=None):
55 ip=NO_IP, tags=None, user=None):
56 """
56 """
57 Creates new post
57 Creates new post
58 """
58 """
59
59
60 posting_time = timezone.now()
60 posting_time = timezone.now()
61 if not thread:
61 if not thread:
62 thread = Thread.objects.create(bump_time=posting_time,
62 thread = Thread.objects.create(bump_time=posting_time,
63 last_edit_time=posting_time)
63 last_edit_time=posting_time)
64 new_thread = True
64 new_thread = True
65 else:
65 else:
66 thread.bump()
66 thread.bump()
67 thread.last_edit_time = posting_time
67 thread.last_edit_time = posting_time
68 thread.save()
68 thread.save()
69 new_thread = False
69 new_thread = False
70
70
71 post = self.create(title=title,
71 post = self.create(title=title,
72 text=text,
72 text=text,
73 pub_time=posting_time,
73 pub_time=posting_time,
74 thread_new=thread,
74 thread_new=thread,
75 poster_ip=ip,
75 poster_ip=ip,
76 poster_user_agent=UNKNOWN_UA, # TODO Get UA at
76 poster_user_agent=UNKNOWN_UA, # TODO Get UA at
77 # last!
77 # last!
78 last_edit_time=posting_time,
78 last_edit_time=posting_time,
79 user=user)
79 user=user)
80
80
81 if image:
81 if image:
82 post_image = PostImage.objects.create(image=image)
82 post_image = PostImage.objects.create(image=image)
83 post.images.add(post_image)
83 post.images.add(post_image)
84 logger.info('Created image #%d for post #%d' % (post_image.id,
84 logger.info('Created image #%d for post #%d' % (post_image.id,
85 post.id))
85 post.id))
86
86
87 thread.replies.add(post)
87 thread.replies.add(post)
88 if tags:
88 if tags:
89 linked_tags = []
89 linked_tags = []
90 for tag in tags:
90 for tag in tags:
91 tag_linked_tags = tag.get_linked_tags()
91 tag_linked_tags = tag.get_linked_tags()
92 if len(tag_linked_tags) > 0:
92 if len(tag_linked_tags) > 0:
93 linked_tags.extend(tag_linked_tags)
93 linked_tags.extend(tag_linked_tags)
94
94
95 tags.extend(linked_tags)
95 tags.extend(linked_tags)
96 map(thread.add_tag, tags)
96 map(thread.add_tag, tags)
97
97
98 if new_thread:
98 if new_thread:
99 Thread.objects.archive_oldest_threads()
99 Thread.objects.process_oldest_threads()
100 self.connect_replies(post)
100 self.connect_replies(post)
101
101
102 logger.info('Created post #%d' % post.id)
102 logger.info('Created post #%d' % post.id)
103
103
104 return post
104 return post
105
105
106 def delete_post(self, post):
106 def delete_post(self, post):
107 """
107 """
108 Deletes post and update or delete its thread
108 Deletes post and update or delete its thread
109 """
109 """
110
110
111 post_id = post.id
111 post_id = post.id
112
112
113 thread = post.get_thread()
113 thread = post.get_thread()
114
114
115 if post.is_opening():
115 if post.is_opening():
116 thread.delete_with_posts()
116 thread.delete_with_posts()
117 else:
117 else:
118 thread.last_edit_time = timezone.now()
118 thread.last_edit_time = timezone.now()
119 thread.save()
119 thread.save()
120
120
121 post.delete()
121 post.delete()
122
122
123 logger.info('Deleted post #%d' % post_id)
123 logger.info('Deleted post #%d' % post_id)
124
124
125 def delete_posts_by_ip(self, ip):
125 def delete_posts_by_ip(self, ip):
126 """
126 """
127 Deletes all posts of the author with same IP
127 Deletes all posts of the author with same IP
128 """
128 """
129
129
130 posts = self.filter(poster_ip=ip)
130 posts = self.filter(poster_ip=ip)
131 map(self.delete_post, posts)
131 map(self.delete_post, posts)
132
132
133 def connect_replies(self, post):
133 def connect_replies(self, post):
134 """
134 """
135 Connects replies to a post to show them as a reflink map
135 Connects replies to a post to show them as a reflink map
136 """
136 """
137
137
138 for reply_number in re.finditer(REGEX_REPLY, post.text.raw):
138 for reply_number in re.finditer(REGEX_REPLY, post.text.raw):
139 post_id = reply_number.group(1)
139 post_id = reply_number.group(1)
140 ref_post = self.filter(id=post_id)
140 ref_post = self.filter(id=post_id)
141 if ref_post.count() > 0:
141 if ref_post.count() > 0:
142 referenced_post = ref_post[0]
142 referenced_post = ref_post[0]
143 referenced_post.referenced_posts.add(post)
143 referenced_post.referenced_posts.add(post)
144 referenced_post.last_edit_time = post.pub_time
144 referenced_post.last_edit_time = post.pub_time
145 referenced_post.build_refmap()
145 referenced_post.build_refmap()
146 referenced_post.save(update_fields=['refmap', 'last_edit_time'])
146 referenced_post.save(update_fields=['refmap', 'last_edit_time'])
147
147
148 referenced_thread = referenced_post.get_thread()
148 referenced_thread = referenced_post.get_thread()
149 referenced_thread.last_edit_time = post.pub_time
149 referenced_thread.last_edit_time = post.pub_time
150 referenced_thread.save(update_fields=['last_edit_time'])
150 referenced_thread.save(update_fields=['last_edit_time'])
151
151
152 def get_posts_per_day(self):
152 def get_posts_per_day(self):
153 """
153 """
154 Gets average count of posts per day for the last 7 days
154 Gets average count of posts per day for the last 7 days
155 """
155 """
156
156
157 today = date.today()
157 today = date.today()
158 ppd = cache.get(CACHE_KEY_PPD + str(today))
158 ppd = cache.get(CACHE_KEY_PPD + str(today))
159 if ppd:
159 if ppd:
160 return ppd
160 return ppd
161
161
162 posts_per_days = []
162 posts_per_days = []
163 for i in POSTS_PER_DAY_RANGE:
163 for i in POSTS_PER_DAY_RANGE:
164 day_end = today - timedelta(i + 1)
164 day_end = today - timedelta(i + 1)
165 day_start = today - timedelta(i + 2)
165 day_start = today - timedelta(i + 2)
166
166
167 day_time_start = timezone.make_aware(datetime.combine(
167 day_time_start = timezone.make_aware(datetime.combine(
168 day_start, dtime()), timezone.get_current_timezone())
168 day_start, dtime()), timezone.get_current_timezone())
169 day_time_end = timezone.make_aware(datetime.combine(
169 day_time_end = timezone.make_aware(datetime.combine(
170 day_end, dtime()), timezone.get_current_timezone())
170 day_end, dtime()), timezone.get_current_timezone())
171
171
172 posts_per_days.append(float(self.filter(
172 posts_per_days.append(float(self.filter(
173 pub_time__lte=day_time_end,
173 pub_time__lte=day_time_end,
174 pub_time__gte=day_time_start).count()))
174 pub_time__gte=day_time_start).count()))
175
175
176 ppd = (sum(posts_per_day for posts_per_day in posts_per_days) /
176 ppd = (sum(posts_per_day for posts_per_day in posts_per_days) /
177 len(posts_per_days))
177 len(posts_per_days))
178 cache.set(CACHE_KEY_PPD + str(today), ppd)
178 cache.set(CACHE_KEY_PPD + str(today), ppd)
179 return ppd
179 return ppd
180
180
181
181
182 class Post(models.Model, Viewable):
182 class Post(models.Model, Viewable):
183 """A post is a message."""
183 """A post is a message."""
184
184
185 objects = PostManager()
185 objects = PostManager()
186
186
187 class Meta:
187 class Meta:
188 app_label = APP_LABEL_BOARDS
188 app_label = APP_LABEL_BOARDS
189 ordering = ('id',)
189 ordering = ('id',)
190
190
191 title = models.CharField(max_length=TITLE_MAX_LENGTH)
191 title = models.CharField(max_length=TITLE_MAX_LENGTH)
192 pub_time = models.DateTimeField()
192 pub_time = models.DateTimeField()
193 text = MarkupField(default_markup_type=DEFAULT_MARKUP_TYPE,
193 text = MarkupField(default_markup_type=DEFAULT_MARKUP_TYPE,
194 escape_html=False)
194 escape_html=False)
195
195
196 images = models.ManyToManyField(PostImage, null=True, blank=True,
196 images = models.ManyToManyField(PostImage, null=True, blank=True,
197 related_name='ip+', db_index=True)
197 related_name='ip+', db_index=True)
198
198
199 poster_ip = models.GenericIPAddressField()
199 poster_ip = models.GenericIPAddressField()
200 poster_user_agent = models.TextField()
200 poster_user_agent = models.TextField()
201
201
202 thread_new = models.ForeignKey('Thread', null=True, default=None,
202 thread_new = models.ForeignKey('Thread', null=True, default=None,
203 db_index=True)
203 db_index=True)
204 last_edit_time = models.DateTimeField()
204 last_edit_time = models.DateTimeField()
205 user = models.ForeignKey('User', null=True, default=None, db_index=True)
205 user = models.ForeignKey('User', null=True, default=None, db_index=True)
206
206
207 referenced_posts = models.ManyToManyField('Post', symmetrical=False,
207 referenced_posts = models.ManyToManyField('Post', symmetrical=False,
208 null=True,
208 null=True,
209 blank=True, related_name='rfp+',
209 blank=True, related_name='rfp+',
210 db_index=True)
210 db_index=True)
211 refmap = models.TextField(null=True, blank=True)
211 refmap = models.TextField(null=True, blank=True)
212
212
213 def __unicode__(self):
213 def __unicode__(self):
214 return '#' + str(self.id) + ' ' + self.title + ' (' + \
214 return '#' + str(self.id) + ' ' + self.title + ' (' + \
215 self.text.raw[:50] + ')'
215 self.text.raw[:50] + ')'
216
216
217 def get_title(self):
217 def get_title(self):
218 """
218 """
219 Gets original post title or part of its text.
219 Gets original post title or part of its text.
220 """
220 """
221
221
222 title = self.title
222 title = self.title
223 if not title:
223 if not title:
224 title = self.text.rendered
224 title = self.text.rendered
225
225
226 return title
226 return title
227
227
228 def build_refmap(self):
228 def build_refmap(self):
229 map_string = ''
229 map_string = ''
230
230
231 first = True
231 first = True
232 for refpost in self.referenced_posts.all():
232 for refpost in self.referenced_posts.all():
233 if not first:
233 if not first:
234 map_string += ', '
234 map_string += ', '
235 map_string += '<a href="%s">&gt;&gt;%s</a>' % (refpost.get_url(), refpost.id)
235 map_string += '<a href="%s">&gt;&gt;%s</a>' % (refpost.get_url(), refpost.id)
236 first = False
236 first = False
237
237
238 self.refmap = map_string
238 self.refmap = map_string
239
239
240 def get_sorted_referenced_posts(self):
240 def get_sorted_referenced_posts(self):
241 return self.refmap
241 return self.refmap
242
242
243 def is_referenced(self):
243 def is_referenced(self):
244 return len(self.refmap) > 0
244 return len(self.refmap) > 0
245
245
246 def is_opening(self):
246 def is_opening(self):
247 """
247 """
248 Checks if this is an opening post or just a reply.
248 Checks if this is an opening post or just a reply.
249 """
249 """
250
250
251 return self.get_thread().get_opening_post_id() == self.id
251 return self.get_thread().get_opening_post_id() == self.id
252
252
253 @transaction.atomic
253 @transaction.atomic
254 def add_tag(self, tag):
254 def add_tag(self, tag):
255 edit_time = timezone.now()
255 edit_time = timezone.now()
256
256
257 thread = self.get_thread()
257 thread = self.get_thread()
258 thread.add_tag(tag)
258 thread.add_tag(tag)
259 self.last_edit_time = edit_time
259 self.last_edit_time = edit_time
260 self.save()
260 self.save()
261
261
262 thread.last_edit_time = edit_time
262 thread.last_edit_time = edit_time
263 thread.save()
263 thread.save()
264
264
265 @transaction.atomic
265 @transaction.atomic
266 def remove_tag(self, tag):
266 def remove_tag(self, tag):
267 edit_time = timezone.now()
267 edit_time = timezone.now()
268
268
269 thread = self.get_thread()
269 thread = self.get_thread()
270 thread.remove_tag(tag)
270 thread.remove_tag(tag)
271 self.last_edit_time = edit_time
271 self.last_edit_time = edit_time
272 self.save()
272 self.save()
273
273
274 thread.last_edit_time = edit_time
274 thread.last_edit_time = edit_time
275 thread.save()
275 thread.save()
276
276
277 def get_url(self, thread=None):
277 def get_url(self, thread=None):
278 """
278 """
279 Gets full url to the post.
279 Gets full url to the post.
280 """
280 """
281
281
282 cache_key = CACHE_KEY_POST_URL + str(self.id)
282 cache_key = CACHE_KEY_POST_URL + str(self.id)
283 link = cache.get(cache_key)
283 link = cache.get(cache_key)
284
284
285 if not link:
285 if not link:
286 if not thread:
286 if not thread:
287 thread = self.get_thread()
287 thread = self.get_thread()
288
288
289 opening_id = thread.get_opening_post_id()
289 opening_id = thread.get_opening_post_id()
290
290
291 if self.id != opening_id:
291 if self.id != opening_id:
292 link = reverse('thread', kwargs={
292 link = reverse('thread', kwargs={
293 'post_id': opening_id}) + '#' + str(self.id)
293 'post_id': opening_id}) + '#' + str(self.id)
294 else:
294 else:
295 link = reverse('thread', kwargs={'post_id': self.id})
295 link = reverse('thread', kwargs={'post_id': self.id})
296
296
297 cache.set(cache_key, link)
297 cache.set(cache_key, link)
298
298
299 return link
299 return link
300
300
301 def get_thread(self):
301 def get_thread(self):
302 """
302 """
303 Gets post's thread.
303 Gets post's thread.
304 """
304 """
305
305
306 return self.thread_new
306 return self.thread_new
307
307
308 def get_referenced_posts(self):
308 def get_referenced_posts(self):
309 return self.referenced_posts.only('id', 'thread_new')
309 return self.referenced_posts.only('id', 'thread_new')
310
310
311 def get_text(self):
311 def get_text(self):
312 return self.text
312 return self.text
313
313
314 def get_view(self, moderator=False, need_open_link=False,
314 def get_view(self, moderator=False, need_open_link=False,
315 truncated=False, *args, **kwargs):
315 truncated=False, *args, **kwargs):
316 if 'is_opening' in kwargs:
316 if 'is_opening' in kwargs:
317 is_opening = kwargs['is_opening']
317 is_opening = kwargs['is_opening']
318 else:
318 else:
319 is_opening = self.is_opening()
319 is_opening = self.is_opening()
320
320
321 if 'thread' in kwargs:
321 if 'thread' in kwargs:
322 thread = kwargs['thread']
322 thread = kwargs['thread']
323 else:
323 else:
324 thread = self.get_thread()
324 thread = self.get_thread()
325
325
326 if 'can_bump' in kwargs:
326 if 'can_bump' in kwargs:
327 can_bump = kwargs['can_bump']
327 can_bump = kwargs['can_bump']
328 else:
328 else:
329 can_bump = thread.can_bump()
329 can_bump = thread.can_bump()
330
330
331 opening_post_id = thread.get_opening_post_id()
331 opening_post_id = thread.get_opening_post_id()
332
332
333 return render_to_string('boards/post.html', {
333 return render_to_string('boards/post.html', {
334 'post': self,
334 'post': self,
335 'moderator': moderator,
335 'moderator': moderator,
336 'is_opening': is_opening,
336 'is_opening': is_opening,
337 'thread': thread,
337 'thread': thread,
338 'bumpable': can_bump,
338 'bumpable': can_bump,
339 'need_open_link': need_open_link,
339 'need_open_link': need_open_link,
340 'truncated': truncated,
340 'truncated': truncated,
341 'opening_post_id': opening_post_id,
341 'opening_post_id': opening_post_id,
342 })
342 })
343
343
344 def get_first_image(self):
344 def get_first_image(self):
345 return self.images.earliest('id')
345 return self.images.earliest('id')
346
346
347 def delete(self, using=None):
347 def delete(self, using=None):
348 """
348 """
349 Delete all post images and the post itself.
349 Delete all post images and the post itself.
350 """
350 """
351
351
352 self.images.all().delete()
352 self.images.all().delete()
353
353
354 super(Post, self).delete(using) No newline at end of file
354 super(Post, self).delete(using)
@@ -1,186 +1,196 b''
1 import logging
1 import logging
2 from django.db.models import Count
2 from django.db.models import Count
3 from django.utils import timezone
3 from django.utils import timezone
4 from django.core.cache import cache
4 from django.core.cache import cache
5 from django.db import models
5 from django.db import models
6 from neboard import settings
6 from boards import settings
7
7
8 __author__ = 'neko259'
8 __author__ = 'neko259'
9
9
10
10
11 logger = logging.getLogger(__name__)
11 logger = logging.getLogger(__name__)
12
12
13
13
14 CACHE_KEY_OPENING_POST = 'opening_post_id'
14 CACHE_KEY_OPENING_POST = 'opening_post_id'
15
15
16
16
17 class ThreadManager(models.Manager):
17 class ThreadManager(models.Manager):
18 def archive_oldest_threads(self):
18 def process_oldest_threads(self):
19 """
19 """
20 Preserves maximum thread count. If there are too many threads,
20 Preserves maximum thread count. If there are too many threads,
21 archive the old ones.
21 archive or delete the old ones.
22 """
22 """
23
23
24 threads = Thread.objects.filter(archived=False).order_by('-bump_time')
24 threads = Thread.objects.filter(archived=False).order_by('-bump_time')
25 thread_count = threads.count()
25 thread_count = threads.count()
26
26
27 if thread_count > settings.MAX_THREAD_COUNT:
27 if thread_count > settings.MAX_THREAD_COUNT:
28 num_threads_to_delete = thread_count - settings.MAX_THREAD_COUNT
28 num_threads_to_delete = thread_count - settings.MAX_THREAD_COUNT
29 old_threads = threads[thread_count - num_threads_to_delete:]
29 old_threads = threads[thread_count - num_threads_to_delete:]
30
30
31 for thread in old_threads:
31 for thread in old_threads:
32 thread.archived = True
32 if settings.ARCHIVE_THREADS:
33 thread.last_edit_time = timezone.now()
33 self._archive_thread(thread)
34 thread.save(update_fields=['archived', 'last_edit_time'])
34 else:
35 self._delete_thread(thread)
36
37 logger.info('Processed %d old threads' % num_threads_to_delete)
35
38
36 logger.info('Archived %d old threads' % num_threads_to_delete)
39 def _archive_thread(self, thread):
40 thread.archived = True
41 thread.last_edit_time = timezone.now()
42 thread.save(update_fields=['archived', 'last_edit_time'])
43
44 def _delete_thread(self, thread):
45 thread.delete_with_posts()
37
46
38
47
39 class Thread(models.Model):
48 class Thread(models.Model):
40 objects = ThreadManager()
49 objects = ThreadManager()
41
50
42 class Meta:
51 class Meta:
43 app_label = 'boards'
52 app_label = 'boards'
44
53
45 tags = models.ManyToManyField('Tag')
54 tags = models.ManyToManyField('Tag')
46 bump_time = models.DateTimeField()
55 bump_time = models.DateTimeField()
47 last_edit_time = models.DateTimeField()
56 last_edit_time = models.DateTimeField()
48 replies = models.ManyToManyField('Post', symmetrical=False, null=True,
57 replies = models.ManyToManyField('Post', symmetrical=False, null=True,
49 blank=True, related_name='tre+')
58 blank=True, related_name='tre+')
50 archived = models.BooleanField(default=False)
59 archived = models.BooleanField(default=False)
51
60
52 def get_tags(self):
61 def get_tags(self):
53 """
62 """
54 Gets a sorted tag list.
63 Gets a sorted tag list.
55 """
64 """
56
65
57 return self.tags.order_by('name')
66 return self.tags.order_by('name')
58
67
59 def bump(self):
68 def bump(self):
60 """
69 """
61 Bumps (moves to up) thread if possible.
70 Bumps (moves to up) thread if possible.
62 """
71 """
63
72
64 if self.can_bump():
73 if self.can_bump():
65 self.bump_time = timezone.now()
74 self.bump_time = timezone.now()
66
75
67 logger.info('Bumped thread %d' % self.id)
76 logger.info('Bumped thread %d' % self.id)
68
77
69 def get_reply_count(self):
78 def get_reply_count(self):
70 return self.replies.count()
79 return self.replies.count()
71
80
72 def get_images_count(self):
81 def get_images_count(self):
73 # TODO Use sum
82 # TODO Use sum
74 total_count = 0
83 total_count = 0
75 for post_with_image in self.replies.annotate(images_count=Count(
84 for post_with_image in self.replies.annotate(images_count=Count(
76 'images')):
85 'images')):
77 total_count += post_with_image.images_count
86 total_count += post_with_image.images_count
78 return total_count
87 return total_count
79
88
80 def can_bump(self):
89 def can_bump(self):
81 """
90 """
82 Checks if the thread can be bumped by replying to it.
91 Checks if the thread can be bumped by replying to it.
83 """
92 """
84
93
85 if self.archived:
94 if self.archived:
86 return False
95 return False
87
96
88 post_count = self.get_reply_count()
97 post_count = self.get_reply_count()
89
98
90 return post_count < settings.MAX_POSTS_PER_THREAD
99 return post_count < settings.MAX_POSTS_PER_THREAD
91
100
101 # TODO Do it in the 'delete' method
92 def delete_with_posts(self):
102 def delete_with_posts(self):
93 """
103 """
94 Completely deletes thread and all its posts
104 Completely deletes thread and all its posts
95 """
105 """
96
106
97 if self.replies.exists():
107 if self.replies.exists():
98 self.replies.all().delete()
108 self.replies.all().delete()
99
109
100 self.delete()
110 self.delete()
101
111
102 def get_last_replies(self):
112 def get_last_replies(self):
103 """
113 """
104 Gets several last replies, not including opening post
114 Gets several last replies, not including opening post
105 """
115 """
106
116
107 if settings.LAST_REPLIES_COUNT > 0:
117 if settings.LAST_REPLIES_COUNT > 0:
108 reply_count = self.get_reply_count()
118 reply_count = self.get_reply_count()
109
119
110 if reply_count > 0:
120 if reply_count > 0:
111 reply_count_to_show = min(settings.LAST_REPLIES_COUNT,
121 reply_count_to_show = min(settings.LAST_REPLIES_COUNT,
112 reply_count - 1)
122 reply_count - 1)
113 replies = self.get_replies()
123 replies = self.get_replies()
114 last_replies = replies[reply_count - reply_count_to_show:]
124 last_replies = replies[reply_count - reply_count_to_show:]
115
125
116 return last_replies
126 return last_replies
117
127
118 def get_skipped_replies_count(self):
128 def get_skipped_replies_count(self):
119 """
129 """
120 Gets number of posts between opening post and last replies.
130 Gets number of posts between opening post and last replies.
121 """
131 """
122 reply_count = self.get_reply_count()
132 reply_count = self.get_reply_count()
123 last_replies_count = min(settings.LAST_REPLIES_COUNT,
133 last_replies_count = min(settings.LAST_REPLIES_COUNT,
124 reply_count - 1)
134 reply_count - 1)
125 return reply_count - last_replies_count - 1
135 return reply_count - last_replies_count - 1
126
136
127 def get_replies(self, view_fields_only=False):
137 def get_replies(self, view_fields_only=False):
128 """
138 """
129 Gets sorted thread posts
139 Gets sorted thread posts
130 """
140 """
131
141
132 query = self.replies.order_by('pub_time').prefetch_related('images')
142 query = self.replies.order_by('pub_time').prefetch_related('images')
133 if view_fields_only:
143 if view_fields_only:
134 query = query.defer('poster_user_agent', 'text_markup_type')
144 query = query.defer('poster_user_agent', 'text_markup_type')
135 return query.all()
145 return query.all()
136
146
137 def get_replies_with_images(self, view_fields_only=False):
147 def get_replies_with_images(self, view_fields_only=False):
138 return self.get_replies(view_fields_only).annotate(images_count=Count(
148 return self.get_replies(view_fields_only).annotate(images_count=Count(
139 'images')).filter(images_count__gt=0)
149 'images')).filter(images_count__gt=0)
140
150
141 def add_tag(self, tag):
151 def add_tag(self, tag):
142 """
152 """
143 Connects thread to a tag and tag to a thread
153 Connects thread to a tag and tag to a thread
144 """
154 """
145
155
146 self.tags.add(tag)
156 self.tags.add(tag)
147 tag.threads.add(self)
157 tag.threads.add(self)
148
158
149 def remove_tag(self, tag):
159 def remove_tag(self, tag):
150 self.tags.remove(tag)
160 self.tags.remove(tag)
151 tag.threads.remove(self)
161 tag.threads.remove(self)
152
162
153 def get_opening_post(self, only_id=False):
163 def get_opening_post(self, only_id=False):
154 """
164 """
155 Gets the first post of the thread
165 Gets the first post of the thread
156 """
166 """
157
167
158 query = self.replies.order_by('pub_time')
168 query = self.replies.order_by('pub_time')
159 if only_id:
169 if only_id:
160 query = query.only('id')
170 query = query.only('id')
161 opening_post = query.first()
171 opening_post = query.first()
162
172
163 return opening_post
173 return opening_post
164
174
165 def get_opening_post_id(self):
175 def get_opening_post_id(self):
166 """
176 """
167 Gets ID of the first thread post.
177 Gets ID of the first thread post.
168 """
178 """
169
179
170 cache_key = CACHE_KEY_OPENING_POST + str(self.id)
180 cache_key = CACHE_KEY_OPENING_POST + str(self.id)
171 opening_post_id = cache.get(cache_key)
181 opening_post_id = cache.get(cache_key)
172 if not opening_post_id:
182 if not opening_post_id:
173 opening_post_id = self.get_opening_post(only_id=True).id
183 opening_post_id = self.get_opening_post(only_id=True).id
174 cache.set(cache_key, opening_post_id)
184 cache.set(cache_key, opening_post_id)
175
185
176 return opening_post_id
186 return opening_post_id
177
187
178 def __unicode__(self):
188 def __unicode__(self):
179 return str(self.id)
189 return str(self.id)
180
190
181 def get_pub_time(self):
191 def get_pub_time(self):
182 """
192 """
183 Gets opening post's pub time because thread does not have its own one.
193 Gets opening post's pub time because thread does not have its own one.
184 """
194 """
185
195
186 return self.get_opening_post().pub_time
196 return self.get_opening_post().pub_time
@@ -1,80 +1,80 b''
1 from django.contrib.syndication.views import Feed
1 from django.contrib.syndication.views import Feed
2 from django.core.urlresolvers import reverse
2 from django.core.urlresolvers import reverse
3 from django.shortcuts import get_object_or_404
3 from django.shortcuts import get_object_or_404
4 from boards.models import Post, Tag, Thread
4 from boards.models import Post, Tag, Thread
5 from neboard import settings
5 from boards import settings
6
6
7 __author__ = 'neko259'
7 __author__ = 'neko259'
8
8
9
9
10 # TODO Make tests for all of these
10 # TODO Make tests for all of these
11 class AllThreadsFeed(Feed):
11 class AllThreadsFeed(Feed):
12
12
13 title = settings.SITE_NAME + ' - All threads'
13 title = settings.SITE_NAME + ' - All threads'
14 link = '/'
14 link = '/'
15 description_template = 'boards/rss/post.html'
15 description_template = 'boards/rss/post.html'
16
16
17 def items(self):
17 def items(self):
18 return Thread.objects.filter(archived=False).order_by('-id')
18 return Thread.objects.filter(archived=False).order_by('-id')
19
19
20 def item_title(self, item):
20 def item_title(self, item):
21 return item.get_opening_post().title
21 return item.get_opening_post().title
22
22
23 def item_link(self, item):
23 def item_link(self, item):
24 return reverse('thread', args={item.get_opening_post_id()})
24 return reverse('thread', args={item.get_opening_post_id()})
25
25
26 def item_pubdate(self, item):
26 def item_pubdate(self, item):
27 return item.get_pub_time()
27 return item.get_pub_time()
28
28
29
29
30 class TagThreadsFeed(Feed):
30 class TagThreadsFeed(Feed):
31
31
32 link = '/'
32 link = '/'
33 description_template = 'boards/rss/post.html'
33 description_template = 'boards/rss/post.html'
34
34
35 def items(self, obj):
35 def items(self, obj):
36 return obj.threads.filter(archived=False).order_by('-id')
36 return obj.threads.filter(archived=False).order_by('-id')
37
37
38 def get_object(self, request, tag_name):
38 def get_object(self, request, tag_name):
39 return get_object_or_404(Tag, name=tag_name)
39 return get_object_or_404(Tag, name=tag_name)
40
40
41 def item_title(self, item):
41 def item_title(self, item):
42 return item.get_opening_post().title
42 return item.get_opening_post().title
43
43
44 def item_link(self, item):
44 def item_link(self, item):
45 return reverse('thread', args={item.get_opening_post_id()})
45 return reverse('thread', args={item.get_opening_post_id()})
46
46
47 def item_pubdate(self, item):
47 def item_pubdate(self, item):
48 return item.get_pub_time()
48 return item.get_pub_time()
49
49
50 def title(self, obj):
50 def title(self, obj):
51 return obj.name
51 return obj.name
52
52
53
53
54 class ThreadPostsFeed(Feed):
54 class ThreadPostsFeed(Feed):
55
55
56 link = '/'
56 link = '/'
57 description_template = 'boards/rss/post.html'
57 description_template = 'boards/rss/post.html'
58
58
59 def items(self, obj):
59 def items(self, obj):
60 return obj.get_thread().get_replies()
60 return obj.get_thread().get_replies()
61
61
62 def get_object(self, request, post_id):
62 def get_object(self, request, post_id):
63 return get_object_or_404(Post, id=post_id)
63 return get_object_or_404(Post, id=post_id)
64
64
65 def item_title(self, item):
65 def item_title(self, item):
66 return item.title
66 return item.title
67
67
68 def item_link(self, item):
68 def item_link(self, item):
69 if not item.is_opening():
69 if not item.is_opening():
70 return reverse('thread', args={
70 return reverse('thread', args={
71 item.get_thread().get_opening_post_id()
71 item.get_thread().get_opening_post_id()
72 }) + "#" + str(item.id)
72 }) + "#" + str(item.id)
73 else:
73 else:
74 return reverse('thread', args={item.id})
74 return reverse('thread', args={item.id})
75
75
76 def item_pubdate(self, item):
76 def item_pubdate(self, item):
77 return item.pub_time
77 return item.pub_time
78
78
79 def title(self, obj):
79 def title(self, obj):
80 return obj.title
80 return obj.title
@@ -1,4 +1,18 b''
1 VERSION = '1.8.1 Kara'
2 SITE_NAME = 'Neboard'
3
1 CACHE_TIMEOUT = 600 # Timeout for caching, if cache is used
4 CACHE_TIMEOUT = 600 # Timeout for caching, if cache is used
2 LOGIN_TIMEOUT = 3600 # Timeout between login tries
5 LOGIN_TIMEOUT = 3600 # Timeout between login tries
3 MAX_TEXT_LENGTH = 30000 # Max post length in characters
6 MAX_TEXT_LENGTH = 30000 # Max post length in characters
4 MAX_IMAGE_SIZE = 8 * 1024 * 1024 # Max image size
7 MAX_IMAGE_SIZE = 8 * 1024 * 1024 # Max image size
8
9 # Thread bumplimit
10 MAX_POSTS_PER_THREAD = 10
11 # Old posts will be archived or deleted if this value is reached
12 MAX_THREAD_COUNT = 5
13 THREADS_PER_PAGE = 3
14 DEFAULT_THEME = 'md'
15 LAST_REPLIES_COUNT = 3
16
17 # Enable archiving threads instead of deletion when the thread limit is reached
18 ARCHIVE_THREADS = True No newline at end of file
@@ -1,259 +1,260 b''
1 # coding=utf-8
1 # coding=utf-8
2 import time
2 import time
3 import logging
3 import logging
4 from django.core.paginator import Paginator
4 from django.core.paginator import Paginator
5
5
6 from django.test import TestCase
6 from django.test import TestCase
7 from django.test.client import Client
7 from django.test.client import Client
8 from django.core.urlresolvers import reverse, NoReverseMatch
8 from django.core.urlresolvers import reverse, NoReverseMatch
9
9
10 from boards.models import Post, Tag, Thread
10 from boards.models import Post, Tag, Thread
11 from boards import urls
11 from boards import urls
12 from neboard import settings
12 from boards import settings
13 import neboard
13
14
14 PAGE_404 = 'boards/404.html'
15 PAGE_404 = 'boards/404.html'
15
16
16 TEST_TEXT = 'test text'
17 TEST_TEXT = 'test text'
17
18
18 NEW_THREAD_PAGE = '/'
19 NEW_THREAD_PAGE = '/'
19 THREAD_PAGE_ONE = '/thread/1/'
20 THREAD_PAGE_ONE = '/thread/1/'
20 THREAD_PAGE = '/thread/'
21 THREAD_PAGE = '/thread/'
21 TAG_PAGE = '/tag/'
22 TAG_PAGE = '/tag/'
22 HTTP_CODE_REDIRECT = 302
23 HTTP_CODE_REDIRECT = 302
23 HTTP_CODE_OK = 200
24 HTTP_CODE_OK = 200
24 HTTP_CODE_NOT_FOUND = 404
25 HTTP_CODE_NOT_FOUND = 404
25
26
26 logger = logging.getLogger(__name__)
27 logger = logging.getLogger(__name__)
27
28
28
29
29 class PostTests(TestCase):
30 class PostTests(TestCase):
30
31
31 def _create_post(self):
32 def _create_post(self):
32 return Post.objects.create_post(title='title',
33 return Post.objects.create_post(title='title',
33 text='text')
34 text='text')
34
35
35 def test_post_add(self):
36 def test_post_add(self):
36 """Test adding post"""
37 """Test adding post"""
37
38
38 post = self._create_post()
39 post = self._create_post()
39
40
40 self.assertIsNotNone(post, 'No post was created')
41 self.assertIsNotNone(post, 'No post was created')
41
42
42 def test_delete_post(self):
43 def test_delete_post(self):
43 """Test post deletion"""
44 """Test post deletion"""
44
45
45 post = self._create_post()
46 post = self._create_post()
46 post_id = post.id
47 post_id = post.id
47
48
48 Post.objects.delete_post(post)
49 Post.objects.delete_post(post)
49
50
50 self.assertFalse(Post.objects.filter(id=post_id).exists())
51 self.assertFalse(Post.objects.filter(id=post_id).exists())
51
52
52 def test_delete_thread(self):
53 def test_delete_thread(self):
53 """Test thread deletion"""
54 """Test thread deletion"""
54
55
55 opening_post = self._create_post()
56 opening_post = self._create_post()
56 thread = opening_post.get_thread()
57 thread = opening_post.get_thread()
57 reply = Post.objects.create_post("", "", thread=thread)
58 reply = Post.objects.create_post("", "", thread=thread)
58
59
59 thread.delete_with_posts()
60 thread.delete_with_posts()
60
61
61 self.assertFalse(Post.objects.filter(id=reply.id).exists())
62 self.assertFalse(Post.objects.filter(id=reply.id).exists())
62
63
63 def test_post_to_thread(self):
64 def test_post_to_thread(self):
64 """Test adding post to a thread"""
65 """Test adding post to a thread"""
65
66
66 op = self._create_post()
67 op = self._create_post()
67 post = Post.objects.create_post("", "", thread=op.get_thread())
68 post = Post.objects.create_post("", "", thread=op.get_thread())
68
69
69 self.assertIsNotNone(post, 'Reply to thread wasn\'t created')
70 self.assertIsNotNone(post, 'Reply to thread wasn\'t created')
70 self.assertEqual(op.get_thread().last_edit_time, post.pub_time,
71 self.assertEqual(op.get_thread().last_edit_time, post.pub_time,
71 'Post\'s create time doesn\'t match thread last edit'
72 'Post\'s create time doesn\'t match thread last edit'
72 ' time')
73 ' time')
73
74
74 def test_delete_posts_by_ip(self):
75 def test_delete_posts_by_ip(self):
75 """Test deleting posts with the given ip"""
76 """Test deleting posts with the given ip"""
76
77
77 post = self._create_post()
78 post = self._create_post()
78 post_id = post.id
79 post_id = post.id
79
80
80 Post.objects.delete_posts_by_ip('0.0.0.0')
81 Post.objects.delete_posts_by_ip('0.0.0.0')
81
82
82 self.assertFalse(Post.objects.filter(id=post_id).exists())
83 self.assertFalse(Post.objects.filter(id=post_id).exists())
83
84
84 def test_get_thread(self):
85 def test_get_thread(self):
85 """Test getting all posts of a thread"""
86 """Test getting all posts of a thread"""
86
87
87 opening_post = self._create_post()
88 opening_post = self._create_post()
88
89
89 for i in range(0, 2):
90 for i in range(0, 2):
90 Post.objects.create_post('title', 'text',
91 Post.objects.create_post('title', 'text',
91 thread=opening_post.get_thread())
92 thread=opening_post.get_thread())
92
93
93 thread = opening_post.get_thread()
94 thread = opening_post.get_thread()
94
95
95 self.assertEqual(3, thread.replies.count())
96 self.assertEqual(3, thread.replies.count())
96
97
97 def test_create_post_with_tag(self):
98 def test_create_post_with_tag(self):
98 """Test adding tag to post"""
99 """Test adding tag to post"""
99
100
100 tag = Tag.objects.create(name='test_tag')
101 tag = Tag.objects.create(name='test_tag')
101 post = Post.objects.create_post(title='title', text='text', tags=[tag])
102 post = Post.objects.create_post(title='title', text='text', tags=[tag])
102
103
103 thread = post.get_thread()
104 thread = post.get_thread()
104 self.assertIsNotNone(post, 'Post not created')
105 self.assertIsNotNone(post, 'Post not created')
105 self.assertTrue(tag in thread.tags.all(), 'Tag not added to thread')
106 self.assertTrue(tag in thread.tags.all(), 'Tag not added to thread')
106 self.assertTrue(thread in tag.threads.all(), 'Thread not added to tag')
107 self.assertTrue(thread in tag.threads.all(), 'Thread not added to tag')
107
108
108 def test_thread_max_count(self):
109 def test_thread_max_count(self):
109 """Test deletion of old posts when the max thread count is reached"""
110 """Test deletion of old posts when the max thread count is reached"""
110
111
111 for i in range(settings.MAX_THREAD_COUNT + 1):
112 for i in range(settings.MAX_THREAD_COUNT + 1):
112 self._create_post()
113 self._create_post()
113
114
114 self.assertEqual(settings.MAX_THREAD_COUNT,
115 self.assertEqual(settings.MAX_THREAD_COUNT,
115 len(Thread.objects.filter(archived=False)))
116 len(Thread.objects.filter(archived=False)))
116
117
117 def test_pages(self):
118 def test_pages(self):
118 """Test that the thread list is properly split into pages"""
119 """Test that the thread list is properly split into pages"""
119
120
120 for i in range(settings.MAX_THREAD_COUNT):
121 for i in range(settings.MAX_THREAD_COUNT):
121 self._create_post()
122 self._create_post()
122
123
123 all_threads = Thread.objects.filter(archived=False)
124 all_threads = Thread.objects.filter(archived=False)
124
125
125 paginator = Paginator(Thread.objects.filter(archived=False),
126 paginator = Paginator(Thread.objects.filter(archived=False),
126 settings.THREADS_PER_PAGE)
127 settings.THREADS_PER_PAGE)
127 posts_in_second_page = paginator.page(2).object_list
128 posts_in_second_page = paginator.page(2).object_list
128 first_post = posts_in_second_page[0]
129 first_post = posts_in_second_page[0]
129
130
130 self.assertEqual(all_threads[settings.THREADS_PER_PAGE].id,
131 self.assertEqual(all_threads[settings.THREADS_PER_PAGE].id,
131 first_post.id)
132 first_post.id)
132
133
133 def test_linked_tag(self):
134 def test_linked_tag(self):
134 """Test adding a linked tag"""
135 """Test adding a linked tag"""
135
136
136 linked_tag = Tag.objects.create(name=u'tag1')
137 linked_tag = Tag.objects.create(name=u'tag1')
137 tag = Tag.objects.create(name=u'tag2', linked=linked_tag)
138 tag = Tag.objects.create(name=u'tag2', linked=linked_tag)
138
139
139 post = Post.objects.create_post("", "", tags=[tag])
140 post = Post.objects.create_post("", "", tags=[tag])
140
141
141 self.assertTrue(linked_tag in post.get_thread().tags.all(),
142 self.assertTrue(linked_tag in post.get_thread().tags.all(),
142 'Linked tag was not added')
143 'Linked tag was not added')
143
144
144
145
145 class PagesTest(TestCase):
146 class PagesTest(TestCase):
146
147
147 def test_404(self):
148 def test_404(self):
148 """Test receiving error 404 when opening a non-existent page"""
149 """Test receiving error 404 when opening a non-existent page"""
149
150
150 tag_name = u'test_tag'
151 tag_name = u'test_tag'
151 tag = Tag.objects.create(name=tag_name)
152 tag = Tag.objects.create(name=tag_name)
152 client = Client()
153 client = Client()
153
154
154 Post.objects.create_post('title', TEST_TEXT, tags=[tag])
155 Post.objects.create_post('title', TEST_TEXT, tags=[tag])
155
156
156 existing_post_id = Post.objects.all()[0].id
157 existing_post_id = Post.objects.all()[0].id
157 response_existing = client.get(THREAD_PAGE + str(existing_post_id) +
158 response_existing = client.get(THREAD_PAGE + str(existing_post_id) +
158 '/')
159 '/')
159 self.assertEqual(HTTP_CODE_OK, response_existing.status_code,
160 self.assertEqual(HTTP_CODE_OK, response_existing.status_code,
160 u'Cannot open existing thread')
161 u'Cannot open existing thread')
161
162
162 response_not_existing = client.get(THREAD_PAGE + str(
163 response_not_existing = client.get(THREAD_PAGE + str(
163 existing_post_id + 1) + '/')
164 existing_post_id + 1) + '/')
164 self.assertEqual(PAGE_404, response_not_existing.templates[0].name,
165 self.assertEqual(PAGE_404, response_not_existing.templates[0].name,
165 u'Not existing thread is opened')
166 u'Not existing thread is opened')
166
167
167 response_existing = client.get(TAG_PAGE + tag_name + '/')
168 response_existing = client.get(TAG_PAGE + tag_name + '/')
168 self.assertEqual(HTTP_CODE_OK,
169 self.assertEqual(HTTP_CODE_OK,
169 response_existing.status_code,
170 response_existing.status_code,
170 u'Cannot open existing tag')
171 u'Cannot open existing tag')
171
172
172 response_not_existing = client.get(TAG_PAGE + u'not_tag' + '/')
173 response_not_existing = client.get(TAG_PAGE + u'not_tag' + '/')
173 self.assertEqual(PAGE_404,
174 self.assertEqual(PAGE_404,
174 response_not_existing.templates[0].name,
175 response_not_existing.templates[0].name,
175 u'Not existing tag is opened')
176 u'Not existing tag is opened')
176
177
177 reply_id = Post.objects.create_post('', TEST_TEXT,
178 reply_id = Post.objects.create_post('', TEST_TEXT,
178 thread=Post.objects.all()[0]
179 thread=Post.objects.all()[0]
179 .get_thread())
180 .get_thread())
180 response_not_existing = client.get(THREAD_PAGE + str(
181 response_not_existing = client.get(THREAD_PAGE + str(
181 reply_id) + '/')
182 reply_id) + '/')
182 self.assertEqual(PAGE_404,
183 self.assertEqual(PAGE_404,
183 response_not_existing.templates[0].name,
184 response_not_existing.templates[0].name,
184 u'Reply is opened as a thread')
185 u'Reply is opened as a thread')
185
186
186
187
187 class FormTest(TestCase):
188 class FormTest(TestCase):
188 def test_post_validation(self):
189 def test_post_validation(self):
189 # Disable captcha for the test
190 # Disable captcha for the test
190 captcha_enabled = settings.ENABLE_CAPTCHA
191 captcha_enabled = neboard.settings.ENABLE_CAPTCHA
191 settings.ENABLE_CAPTCHA = False
192 neboard.settings.ENABLE_CAPTCHA = False
192
193
193 client = Client()
194 client = Client()
194
195
195 valid_tags = u'tag1 tag_2 Ρ‚Π΅Π³_3'
196 valid_tags = u'tag1 tag_2 Ρ‚Π΅Π³_3'
196 invalid_tags = u'$%_356 ---'
197 invalid_tags = u'$%_356 ---'
197
198
198 response = client.post(NEW_THREAD_PAGE, {'title': 'test title',
199 response = client.post(NEW_THREAD_PAGE, {'title': 'test title',
199 'text': TEST_TEXT,
200 'text': TEST_TEXT,
200 'tags': valid_tags})
201 'tags': valid_tags})
201 self.assertEqual(response.status_code, HTTP_CODE_REDIRECT,
202 self.assertEqual(response.status_code, HTTP_CODE_REDIRECT,
202 msg='Posting new message failed: got code ' +
203 msg='Posting new message failed: got code ' +
203 str(response.status_code))
204 str(response.status_code))
204
205
205 self.assertEqual(1, Post.objects.count(),
206 self.assertEqual(1, Post.objects.count(),
206 msg='No posts were created')
207 msg='No posts were created')
207
208
208 client.post(NEW_THREAD_PAGE, {'text': TEST_TEXT,
209 client.post(NEW_THREAD_PAGE, {'text': TEST_TEXT,
209 'tags': invalid_tags})
210 'tags': invalid_tags})
210 self.assertEqual(1, Post.objects.count(), msg='The validation passed '
211 self.assertEqual(1, Post.objects.count(), msg='The validation passed '
211 'where it should fail')
212 'where it should fail')
212
213
213 # Change posting delay so we don't have to wait for 30 seconds or more
214 # Change posting delay so we don't have to wait for 30 seconds or more
214 old_posting_delay = settings.POSTING_DELAY
215 old_posting_delay = neboard.settings.POSTING_DELAY
215 # Wait fot the posting delay or we won't be able to post
216 # Wait fot the posting delay or we won't be able to post
216 settings.POSTING_DELAY = 1
217 settings.POSTING_DELAY = 1
217 time.sleep(settings.POSTING_DELAY + 1)
218 time.sleep(neboard.settings.POSTING_DELAY + 1)
218 response = client.post(THREAD_PAGE_ONE, {'text': TEST_TEXT,
219 response = client.post(THREAD_PAGE_ONE, {'text': TEST_TEXT,
219 'tags': valid_tags})
220 'tags': valid_tags})
220 self.assertEqual(HTTP_CODE_REDIRECT, response.status_code,
221 self.assertEqual(HTTP_CODE_REDIRECT, response.status_code,
221 msg=u'Posting new message failed: got code ' +
222 msg=u'Posting new message failed: got code ' +
222 str(response.status_code))
223 str(response.status_code))
223 # Restore posting delay
224 # Restore posting delay
224 settings.POSTING_DELAY = old_posting_delay
225 settings.POSTING_DELAY = old_posting_delay
225
226
226 self.assertEqual(2, Post.objects.count(),
227 self.assertEqual(2, Post.objects.count(),
227 msg=u'No posts were created')
228 msg=u'No posts were created')
228
229
229 # Restore captcha setting
230 # Restore captcha setting
230 settings.ENABLE_CAPTCHA = captcha_enabled
231 settings.ENABLE_CAPTCHA = captcha_enabled
231
232
232
233
233 class ViewTest(TestCase):
234 class ViewTest(TestCase):
234
235
235 def test_all_views(self):
236 def test_all_views(self):
236 '''
237 '''
237 Try opening all views defined in ulrs.py that don't need additional
238 Try opening all views defined in ulrs.py that don't need additional
238 parameters
239 parameters
239 '''
240 '''
240
241
241 client = Client()
242 client = Client()
242 for url in urls.urlpatterns:
243 for url in urls.urlpatterns:
243 try:
244 try:
244 view_name = url.name
245 view_name = url.name
245 logger.debug('Testing view %s' % view_name)
246 logger.debug('Testing view %s' % view_name)
246
247
247 try:
248 try:
248 response = client.get(reverse(view_name))
249 response = client.get(reverse(view_name))
249
250
250 self.assertEqual(HTTP_CODE_OK, response.status_code,
251 self.assertEqual(HTTP_CODE_OK, response.status_code,
251 '%s view not opened' % view_name)
252 '%s view not opened' % view_name)
252 except NoReverseMatch:
253 except NoReverseMatch:
253 # This view just needs additional arguments
254 # This view just needs additional arguments
254 pass
255 pass
255 except Exception, e:
256 except Exception, e:
256 self.fail('Got exception %s at %s view' % (e, view_name))
257 self.fail('Got exception %s at %s view' % (e, view_name))
257 except AttributeError:
258 except AttributeError:
258 # This is normal, some views do not have names
259 # This is normal, some views do not have names
259 pass
260 pass
@@ -1,129 +1,129 b''
1 """
1 """
2 This module contains helper functions and helper classes.
2 This module contains helper functions and helper classes.
3 """
3 """
4 import hashlib
4 import hashlib
5 import time
6
5 from django.utils import timezone
7 from django.utils import timezone
8 import boards
6
9
7 from neboard import settings
10 from neboard import settings
8 from boards.models import Post, User
11 from boards.models import User
9 from boards.models.post import SETTING_MODERATE
10 from boards.models.user import RANK_USER
12 from boards.models.user import RANK_USER
11 import time
12 import neboard
13
13
14
14
15 KEY_CAPTCHA_FAILS = 'key_captcha_fails'
15 KEY_CAPTCHA_FAILS = 'key_captcha_fails'
16 KEY_CAPTCHA_DELAY_TIME = 'key_captcha_delay_time'
16 KEY_CAPTCHA_DELAY_TIME = 'key_captcha_delay_time'
17 KEY_CAPTCHA_LAST_ACTIVITY = 'key_captcha_last_activity'
17 KEY_CAPTCHA_LAST_ACTIVITY = 'key_captcha_last_activity'
18
18
19
19
20 def need_include_captcha(request):
20 def need_include_captcha(request):
21 """
21 """
22 Check if request is made by a user.
22 Check if request is made by a user.
23 It contains rules which check for bots.
23 It contains rules which check for bots.
24 """
24 """
25
25
26 if not settings.ENABLE_CAPTCHA:
26 if not settings.ENABLE_CAPTCHA:
27 return False
27 return False
28
28
29 enable_captcha = False
29 enable_captcha = False
30
30
31 #newcomer
31 #newcomer
32 if KEY_CAPTCHA_LAST_ACTIVITY not in request.session:
32 if KEY_CAPTCHA_LAST_ACTIVITY not in request.session:
33 return settings.ENABLE_CAPTCHA
33 return settings.ENABLE_CAPTCHA
34
34
35 last_activity = request.session[KEY_CAPTCHA_LAST_ACTIVITY]
35 last_activity = request.session[KEY_CAPTCHA_LAST_ACTIVITY]
36 current_delay = int(time.time()) - last_activity
36 current_delay = int(time.time()) - last_activity
37
37
38 delay_time = (request.session[KEY_CAPTCHA_DELAY_TIME]
38 delay_time = (request.session[KEY_CAPTCHA_DELAY_TIME]
39 if KEY_CAPTCHA_DELAY_TIME in request.session
39 if KEY_CAPTCHA_DELAY_TIME in request.session
40 else settings.CAPTCHA_DEFAULT_SAFE_TIME)
40 else settings.CAPTCHA_DEFAULT_SAFE_TIME)
41
41
42 if current_delay < delay_time:
42 if current_delay < delay_time:
43 enable_captcha = True
43 enable_captcha = True
44
44
45 print 'ENABLING' + str(enable_captcha)
45 print 'ENABLING' + str(enable_captcha)
46
46
47 return enable_captcha
47 return enable_captcha
48
48
49
49
50 def update_captcha_access(request, passed):
50 def update_captcha_access(request, passed):
51 """
51 """
52 Update captcha fields.
52 Update captcha fields.
53 It will reduce delay time if user passed captcha verification and
53 It will reduce delay time if user passed captcha verification and
54 it will increase it otherwise.
54 it will increase it otherwise.
55 """
55 """
56 session = request.session
56 session = request.session
57
57
58 delay_time = (request.session[KEY_CAPTCHA_DELAY_TIME]
58 delay_time = (request.session[KEY_CAPTCHA_DELAY_TIME]
59 if KEY_CAPTCHA_DELAY_TIME in request.session
59 if KEY_CAPTCHA_DELAY_TIME in request.session
60 else settings.CAPTCHA_DEFAULT_SAFE_TIME)
60 else settings.CAPTCHA_DEFAULT_SAFE_TIME)
61
61
62 print "DELAY TIME = " + str(delay_time)
62 print "DELAY TIME = " + str(delay_time)
63
63
64 if passed:
64 if passed:
65 delay_time -= 2 if delay_time >= 7 else 5
65 delay_time -= 2 if delay_time >= 7 else 5
66 else:
66 else:
67 delay_time += 10
67 delay_time += 10
68
68
69 session[KEY_CAPTCHA_LAST_ACTIVITY] = int(time.time())
69 session[KEY_CAPTCHA_LAST_ACTIVITY] = int(time.time())
70 session[KEY_CAPTCHA_DELAY_TIME] = delay_time
70 session[KEY_CAPTCHA_DELAY_TIME] = delay_time
71
71
72
72
73 def get_client_ip(request):
73 def get_client_ip(request):
74 x_forwarded_for = request.META.get('HTTP_X_FORWARDED_FOR')
74 x_forwarded_for = request.META.get('HTTP_X_FORWARDED_FOR')
75 if x_forwarded_for:
75 if x_forwarded_for:
76 ip = x_forwarded_for.split(',')[-1].strip()
76 ip = x_forwarded_for.split(',')[-1].strip()
77 else:
77 else:
78 ip = request.META.get('REMOTE_ADDR')
78 ip = request.META.get('REMOTE_ADDR')
79 return ip
79 return ip
80
80
81
81
82 def datetime_to_epoch(datetime):
82 def datetime_to_epoch(datetime):
83 return int(time.mktime(timezone.localtime(
83 return int(time.mktime(timezone.localtime(
84 datetime,timezone.get_current_timezone()).timetuple())
84 datetime,timezone.get_current_timezone()).timetuple())
85 * 1000000 + datetime.microsecond)
85 * 1000000 + datetime.microsecond)
86
86
87
87
88 def get_user(request):
88 def get_user(request):
89 """
89 """
90 Get current user from the session. If the user does not exist, create
90 Get current user from the session. If the user does not exist, create
91 a new one.
91 a new one.
92 """
92 """
93
93
94 session = request.session
94 session = request.session
95 if not 'user_id' in session:
95 if not 'user_id' in session:
96 request.session.save()
96 request.session.save()
97
97
98 md5 = hashlib.md5()
98 md5 = hashlib.md5()
99 md5.update(session.session_key)
99 md5.update(session.session_key)
100 new_id = md5.hexdigest()
100 new_id = md5.hexdigest()
101
101
102 while User.objects.filter(user_id=new_id).exists():
102 while User.objects.filter(user_id=new_id).exists():
103 md5.update(str(timezone.now()))
103 md5.update(str(timezone.now()))
104 new_id = md5.hexdigest()
104 new_id = md5.hexdigest()
105
105
106 time_now = timezone.now()
106 time_now = timezone.now()
107 user = User.objects.create(user_id=new_id, rank=RANK_USER,
107 user = User.objects.create(user_id=new_id, rank=RANK_USER,
108 registration_time=time_now)
108 registration_time=time_now)
109
109
110 session['user_id'] = user.id
110 session['user_id'] = user.id
111 else:
111 else:
112 user = User.objects.select_related('fav_tags').get(
112 user = User.objects.select_related('fav_tags').get(
113 id=session['user_id'])
113 id=session['user_id'])
114
114
115 return user
115 return user
116
116
117
117
118 def get_theme(request, user=None):
118 def get_theme(request, user=None):
119 """
119 """
120 Get user's CSS theme
120 Get user's CSS theme
121 """
121 """
122
122
123 if not user:
123 if not user:
124 user = get_user(request)
124 user = get_user(request)
125 theme = user.get_setting('theme')
125 theme = user.get_setting('theme')
126 if not theme:
126 if not theme:
127 theme = neboard.settings.DEFAULT_THEME
127 theme = boards.settings.DEFAULT_THEME
128
128
129 return theme No newline at end of file
129 return theme
@@ -1,134 +1,134 b''
1 import string
1 import string
2
2
3 from django.db import transaction
3 from django.db import transaction
4 from django.shortcuts import render, redirect
4 from django.shortcuts import render, redirect
5
5
6 from boards import utils
6 from boards import utils, settings
7 from boards.abstracts.paginator import get_paginator
7 from boards.abstracts.paginator import get_paginator
8 from boards.forms import ThreadForm, PlainErrorList
8 from boards.forms import ThreadForm, PlainErrorList
9 from boards.models import Post, Thread, Ban, Tag
9 from boards.models import Post, Thread, Ban, Tag
10 from boards.views.banned import BannedView
10 from boards.views.banned import BannedView
11 from boards.views.base import BaseBoardView, PARAMETER_FORM
11 from boards.views.base import BaseBoardView, PARAMETER_FORM
12 from boards.views.posting_mixin import PostMixin
12 from boards.views.posting_mixin import PostMixin
13 import neboard
13 import neboard
14
14
15 TAG_DELIMITER = ' '
15 TAG_DELIMITER = ' '
16
16
17 PARAMETER_CURRENT_PAGE = 'current_page'
17 PARAMETER_CURRENT_PAGE = 'current_page'
18 PARAMETER_PAGINATOR = 'paginator'
18 PARAMETER_PAGINATOR = 'paginator'
19 PARAMETER_THREADS = 'threads'
19 PARAMETER_THREADS = 'threads'
20
20
21 TEMPLATE = 'boards/posting_general.html'
21 TEMPLATE = 'boards/posting_general.html'
22 DEFAULT_PAGE = 1
22 DEFAULT_PAGE = 1
23
23
24
24
25 class AllThreadsView(PostMixin, BaseBoardView):
25 class AllThreadsView(PostMixin, BaseBoardView):
26
26
27 user = None
27 user = None
28
28
29 def get(self, request, page=DEFAULT_PAGE, form=None):
29 def get(self, request, page=DEFAULT_PAGE, form=None):
30 context = self.get_context_data(request=request)
30 context = self.get_context_data(request=request)
31
31
32 self.user = utils.get_user(request)
32 self.user = utils.get_user(request)
33
33
34 if not form:
34 if not form:
35 form = ThreadForm(error_class=PlainErrorList)
35 form = ThreadForm(error_class=PlainErrorList)
36
36
37 paginator = get_paginator(self.get_threads(),
37 paginator = get_paginator(self.get_threads(),
38 neboard.settings.THREADS_PER_PAGE)
38 settings.THREADS_PER_PAGE)
39 paginator.current_page = int(page)
39 paginator.current_page = int(page)
40
40
41 threads = paginator.page(page).object_list
41 threads = paginator.page(page).object_list
42
42
43 context[PARAMETER_THREADS] = threads
43 context[PARAMETER_THREADS] = threads
44 context[PARAMETER_FORM] = form
44 context[PARAMETER_FORM] = form
45
45
46 self._get_page_context(paginator, context, page)
46 self._get_page_context(paginator, context, page)
47
47
48 return render(request, TEMPLATE, context)
48 return render(request, TEMPLATE, context)
49
49
50 def post(self, request, page=DEFAULT_PAGE):
50 def post(self, request, page=DEFAULT_PAGE):
51 form = ThreadForm(request.POST, request.FILES,
51 form = ThreadForm(request.POST, request.FILES,
52 error_class=PlainErrorList)
52 error_class=PlainErrorList)
53 form.session = request.session
53 form.session = request.session
54
54
55 if form.is_valid():
55 if form.is_valid():
56 return self.create_thread(request, form)
56 return self.create_thread(request, form)
57 if form.need_to_ban:
57 if form.need_to_ban:
58 # Ban user because he is suspected to be a bot
58 # Ban user because he is suspected to be a bot
59 self._ban_current_user(request)
59 self._ban_current_user(request)
60
60
61 return self.get(request, page, form)
61 return self.get(request, page, form)
62
62
63 @staticmethod
63 @staticmethod
64 def _get_page_context(paginator, context, page):
64 def _get_page_context(paginator, context, page):
65 """
65 """
66 Get pagination context variables
66 Get pagination context variables
67 """
67 """
68
68
69 context[PARAMETER_PAGINATOR] = paginator
69 context[PARAMETER_PAGINATOR] = paginator
70 context[PARAMETER_CURRENT_PAGE] = paginator.page(int(page))
70 context[PARAMETER_CURRENT_PAGE] = paginator.page(int(page))
71
71
72 @staticmethod
72 @staticmethod
73 def parse_tags_string(tag_strings):
73 def parse_tags_string(tag_strings):
74 """
74 """
75 Parses tag list string and returns tag object list.
75 Parses tag list string and returns tag object list.
76 """
76 """
77
77
78 tags = []
78 tags = []
79
79
80 if tag_strings:
80 if tag_strings:
81 tag_strings = tag_strings.split(TAG_DELIMITER)
81 tag_strings = tag_strings.split(TAG_DELIMITER)
82 for tag_name in tag_strings:
82 for tag_name in tag_strings:
83 tag_name = string.lower(tag_name.strip())
83 tag_name = string.lower(tag_name.strip())
84 if len(tag_name) > 0:
84 if len(tag_name) > 0:
85 tag, created = Tag.objects.get_or_create(name=tag_name)
85 tag, created = Tag.objects.get_or_create(name=tag_name)
86 tags.append(tag)
86 tags.append(tag)
87
87
88 return tags
88 return tags
89
89
90 @transaction.atomic
90 @transaction.atomic
91 def create_thread(self, request, form, html_response=True):
91 def create_thread(self, request, form, html_response=True):
92 """
92 """
93 Creates a new thread with an opening post.
93 Creates a new thread with an opening post.
94 """
94 """
95
95
96 ip = utils.get_client_ip(request)
96 ip = utils.get_client_ip(request)
97 is_banned = Ban.objects.filter(ip=ip).exists()
97 is_banned = Ban.objects.filter(ip=ip).exists()
98
98
99 if is_banned:
99 if is_banned:
100 if html_response:
100 if html_response:
101 return redirect(BannedView().as_view())
101 return redirect(BannedView().as_view())
102 else:
102 else:
103 return
103 return
104
104
105 data = form.cleaned_data
105 data = form.cleaned_data
106
106
107 title = data['title']
107 title = data['title']
108 text = data['text']
108 text = data['text']
109
109
110 text = self._remove_invalid_links(text)
110 text = self._remove_invalid_links(text)
111
111
112 if 'image' in data.keys():
112 if 'image' in data.keys():
113 image = data['image']
113 image = data['image']
114 else:
114 else:
115 image = None
115 image = None
116
116
117 tag_strings = data['tags']
117 tag_strings = data['tags']
118
118
119 tags = self.parse_tags_string(tag_strings)
119 tags = self.parse_tags_string(tag_strings)
120
120
121 post = Post.objects.create_post(title=title, text=text, ip=ip,
121 post = Post.objects.create_post(title=title, text=text, ip=ip,
122 image=image, tags=tags,
122 image=image, tags=tags,
123 user=utils.get_user(request))
123 user=utils.get_user(request))
124
124
125 if html_response:
125 if html_response:
126 return redirect(post.get_url())
126 return redirect(post.get_url())
127
127
128 def get_threads(self):
128 def get_threads(self):
129 """
129 """
130 Gets list of threads that will be shown on a page.
130 Gets list of threads that will be shown on a page.
131 """
131 """
132
132
133 return Thread.objects.all().order_by('-bump_time')\
133 return Thread.objects.all().order_by('-bump_time')\
134 .exclude(tags__in=self.user.hidden_tags.all())
134 .exclude(tags__in=self.user.hidden_tags.all())
@@ -1,133 +1,132 b''
1 from django.core.urlresolvers import reverse
1 from django.core.urlresolvers import reverse
2 from django.db import transaction
2 from django.db import transaction
3 from django.http import Http404
3 from django.http import Http404
4 from django.shortcuts import get_object_or_404, render, redirect
4 from django.shortcuts import get_object_or_404, render, redirect
5 from django.views.generic.edit import FormMixin
5 from django.views.generic.edit import FormMixin
6
6
7 from boards import utils
7 from boards import utils, settings
8 from boards.forms import PostForm, PlainErrorList
8 from boards.forms import PostForm, PlainErrorList
9 from boards.models import Post, Ban
9 from boards.models import Post, Ban
10 from boards.views.banned import BannedView
10 from boards.views.banned import BannedView
11 from boards.views.base import BaseBoardView, PARAMETER_FORM
11 from boards.views.base import BaseBoardView, PARAMETER_FORM
12 from boards.views.posting_mixin import PostMixin
12 from boards.views.posting_mixin import PostMixin
13 import neboard
14
13
15
14
16 MODE_GALLERY = 'gallery'
15 MODE_GALLERY = 'gallery'
17 MODE_NORMAL = 'normal'
16 MODE_NORMAL = 'normal'
18
17
19 PARAMETER_MAX_REPLIES = 'max_replies'
18 PARAMETER_MAX_REPLIES = 'max_replies'
20 PARAMETER_THREAD = 'thread'
19 PARAMETER_THREAD = 'thread'
21 PARAMETER_BUMPABLE = 'bumpable'
20 PARAMETER_BUMPABLE = 'bumpable'
22
21
23
22
24 class ThreadView(BaseBoardView, PostMixin, FormMixin):
23 class ThreadView(BaseBoardView, PostMixin, FormMixin):
25
24
26 def get(self, request, post_id, mode=MODE_NORMAL, form=None):
25 def get(self, request, post_id, mode=MODE_NORMAL, form=None):
27 try:
26 try:
28 opening_post = Post.objects.filter(id=post_id).only('thread_new')[0]
27 opening_post = Post.objects.filter(id=post_id).only('thread_new')[0]
29 except IndexError:
28 except IndexError:
30 raise Http404
29 raise Http404
31
30
32 # If this is not OP, don't show it as it is
31 # If this is not OP, don't show it as it is
33 if not opening_post or not opening_post.is_opening():
32 if not opening_post or not opening_post.is_opening():
34 raise Http404
33 raise Http404
35
34
36 if not form:
35 if not form:
37 form = PostForm(error_class=PlainErrorList)
36 form = PostForm(error_class=PlainErrorList)
38
37
39 thread_to_show = opening_post.get_thread()
38 thread_to_show = opening_post.get_thread()
40
39
41 context = self.get_context_data(request=request)
40 context = self.get_context_data(request=request)
42
41
43 context[PARAMETER_FORM] = form
42 context[PARAMETER_FORM] = form
44 context["last_update"] = utils.datetime_to_epoch(
43 context["last_update"] = utils.datetime_to_epoch(
45 thread_to_show.last_edit_time)
44 thread_to_show.last_edit_time)
46 context[PARAMETER_THREAD] = thread_to_show
45 context[PARAMETER_THREAD] = thread_to_show
47 context[PARAMETER_MAX_REPLIES] = neboard.settings.MAX_POSTS_PER_THREAD
46 context[PARAMETER_MAX_REPLIES] = settings.MAX_POSTS_PER_THREAD
48
47
49 if MODE_NORMAL == mode:
48 if MODE_NORMAL == mode:
50 context[PARAMETER_BUMPABLE] = thread_to_show.can_bump()
49 context[PARAMETER_BUMPABLE] = thread_to_show.can_bump()
51 if context[PARAMETER_BUMPABLE]:
50 if context[PARAMETER_BUMPABLE]:
52 context['posts_left'] = neboard.settings.MAX_POSTS_PER_THREAD \
51 context['posts_left'] = settings.MAX_POSTS_PER_THREAD \
53 - thread_to_show.get_reply_count()
52 - thread_to_show.get_reply_count()
54 context['bumplimit_progress'] = str(
53 context['bumplimit_progress'] = str(
55 float(context['posts_left']) /
54 float(context['posts_left']) /
56 neboard.settings.MAX_POSTS_PER_THREAD * 100)
55 settings.MAX_POSTS_PER_THREAD * 100)
57
56
58 context['opening_post'] = opening_post
57 context['opening_post'] = opening_post
59
58
60 document = 'boards/thread.html'
59 document = 'boards/thread.html'
61 elif MODE_GALLERY == mode:
60 elif MODE_GALLERY == mode:
62 context['posts'] = thread_to_show.get_replies_with_images(
61 context['posts'] = thread_to_show.get_replies_with_images(
63 view_fields_only=True)
62 view_fields_only=True)
64
63
65 document = 'boards/thread_gallery.html'
64 document = 'boards/thread_gallery.html'
66 else:
65 else:
67 raise Http404
66 raise Http404
68
67
69 return render(request, document, context)
68 return render(request, document, context)
70
69
71 def post(self, request, post_id, mode=MODE_NORMAL):
70 def post(self, request, post_id, mode=MODE_NORMAL):
72 opening_post = get_object_or_404(Post, id=post_id)
71 opening_post = get_object_or_404(Post, id=post_id)
73
72
74 # If this is not OP, don't show it as it is
73 # If this is not OP, don't show it as it is
75 if not opening_post.is_opening():
74 if not opening_post.is_opening():
76 raise Http404
75 raise Http404
77
76
78 if not opening_post.get_thread().archived:
77 if not opening_post.get_thread().archived:
79 form = PostForm(request.POST, request.FILES,
78 form = PostForm(request.POST, request.FILES,
80 error_class=PlainErrorList)
79 error_class=PlainErrorList)
81 form.session = request.session
80 form.session = request.session
82
81
83 if form.is_valid():
82 if form.is_valid():
84 return self.new_post(request, form, opening_post)
83 return self.new_post(request, form, opening_post)
85 if form.need_to_ban:
84 if form.need_to_ban:
86 # Ban user because he is suspected to be a bot
85 # Ban user because he is suspected to be a bot
87 self._ban_current_user(request)
86 self._ban_current_user(request)
88
87
89 return self.get(request, post_id, mode, form)
88 return self.get(request, post_id, mode, form)
90
89
91 @transaction.atomic
90 @transaction.atomic
92 def new_post(self, request, form, opening_post=None, html_response=True):
91 def new_post(self, request, form, opening_post=None, html_response=True):
93 """Add a new post (in thread or as a reply)."""
92 """Add a new post (in thread or as a reply)."""
94
93
95 ip = utils.get_client_ip(request)
94 ip = utils.get_client_ip(request)
96 is_banned = Ban.objects.filter(ip=ip).exists()
95 is_banned = Ban.objects.filter(ip=ip).exists()
97
96
98 if is_banned:
97 if is_banned:
99 if html_response:
98 if html_response:
100 return redirect(BannedView().as_view())
99 return redirect(BannedView().as_view())
101 else:
100 else:
102 return None
101 return None
103
102
104 data = form.cleaned_data
103 data = form.cleaned_data
105
104
106 title = data['title']
105 title = data['title']
107 text = data['text']
106 text = data['text']
108
107
109 text = self._remove_invalid_links(text)
108 text = self._remove_invalid_links(text)
110
109
111 if 'image' in data.keys():
110 if 'image' in data.keys():
112 image = data['image']
111 image = data['image']
113 else:
112 else:
114 image = None
113 image = None
115
114
116 tags = []
115 tags = []
117
116
118 post_thread = opening_post.get_thread()
117 post_thread = opening_post.get_thread()
119
118
120 post = Post.objects.create_post(title=title, text=text, ip=ip,
119 post = Post.objects.create_post(title=title, text=text, ip=ip,
121 thread=post_thread, image=image,
120 thread=post_thread, image=image,
122 tags=tags,
121 tags=tags,
123 user=utils.get_user(request))
122 user=utils.get_user(request))
124
123
125 thread_to_show = (opening_post.id if opening_post else post.id)
124 thread_to_show = (opening_post.id if opening_post else post.id)
126
125
127 if html_response:
126 if html_response:
128 if opening_post:
127 if opening_post:
129 return redirect(reverse(
128 return redirect(reverse(
130 'thread',
129 'thread',
131 kwargs={'post_id': thread_to_show}) + '#' + str(post.id))
130 kwargs={'post_id': thread_to_show}) + '#' + str(post.id))
132 else:
131 else:
133 return post
132 return post
@@ -0,0 +1,1 b''
1 import settings No newline at end of file
@@ -1,276 +1,262 b''
1 # Django settings for neboard project.
1 # Django settings for neboard project.
2 import os
2 import os
3 from boards.mdx_neboard import markdown_extended
3 from boards.mdx_neboard import markdown_extended
4
4
5 DEBUG = True
5 DEBUG = True
6 TEMPLATE_DEBUG = DEBUG
6 TEMPLATE_DEBUG = DEBUG
7
7
8 ADMINS = (
8 ADMINS = (
9 # ('Your Name', 'your_email@example.com'),
9 # ('Your Name', 'your_email@example.com'),
10 ('admin', 'admin@example.com')
10 ('admin', 'admin@example.com')
11 )
11 )
12
12
13 MANAGERS = ADMINS
13 MANAGERS = ADMINS
14
14
15 DATABASES = {
15 DATABASES = {
16 'default': {
16 'default': {
17 'ENGINE': 'django.db.backends.sqlite3', # Add 'postgresql_psycopg2', 'mysql', 'sqlite3' or 'oracle'.
17 'ENGINE': 'django.db.backends.sqlite3', # Add 'postgresql_psycopg2', 'mysql', 'sqlite3' or 'oracle'.
18 'NAME': 'database.db', # Or path to database file if using sqlite3.
18 'NAME': 'database.db', # Or path to database file if using sqlite3.
19 'USER': '', # Not used with sqlite3.
19 'USER': '', # Not used with sqlite3.
20 'PASSWORD': '', # Not used with sqlite3.
20 'PASSWORD': '', # Not used with sqlite3.
21 'HOST': '', # Set to empty string for localhost. Not used with sqlite3.
21 'HOST': '', # Set to empty string for localhost. Not used with sqlite3.
22 'PORT': '', # Set to empty string for default. Not used with sqlite3.
22 'PORT': '', # Set to empty string for default. Not used with sqlite3.
23 'CONN_MAX_AGE': None,
23 'CONN_MAX_AGE': None,
24 }
24 }
25 }
25 }
26
26
27 # Local time zone for this installation. Choices can be found here:
27 # Local time zone for this installation. Choices can be found here:
28 # http://en.wikipedia.org/wiki/List_of_tz_zones_by_name
28 # http://en.wikipedia.org/wiki/List_of_tz_zones_by_name
29 # although not all choices may be available on all operating systems.
29 # although not all choices may be available on all operating systems.
30 # In a Windows environment this must be set to your system time zone.
30 # In a Windows environment this must be set to your system time zone.
31 TIME_ZONE = 'Europe/Kiev'
31 TIME_ZONE = 'Europe/Kiev'
32
32
33 # Language code for this installation. All choices can be found here:
33 # Language code for this installation. All choices can be found here:
34 # http://www.i18nguy.com/unicode/language-identifiers.html
34 # http://www.i18nguy.com/unicode/language-identifiers.html
35 LANGUAGE_CODE = 'en'
35 LANGUAGE_CODE = 'en'
36
36
37 SITE_ID = 1
37 SITE_ID = 1
38
38
39 # If you set this to False, Django will make some optimizations so as not
39 # If you set this to False, Django will make some optimizations so as not
40 # to load the internationalization machinery.
40 # to load the internationalization machinery.
41 USE_I18N = True
41 USE_I18N = True
42
42
43 # If you set this to False, Django will not format dates, numbers and
43 # If you set this to False, Django will not format dates, numbers and
44 # calendars according to the current locale.
44 # calendars according to the current locale.
45 USE_L10N = True
45 USE_L10N = True
46
46
47 # If you set this to False, Django will not use timezone-aware datetimes.
47 # If you set this to False, Django will not use timezone-aware datetimes.
48 USE_TZ = True
48 USE_TZ = True
49
49
50 # Absolute filesystem path to the directory that will hold user-uploaded files.
50 # Absolute filesystem path to the directory that will hold user-uploaded files.
51 # Example: "/home/media/media.lawrence.com/media/"
51 # Example: "/home/media/media.lawrence.com/media/"
52 MEDIA_ROOT = './media/'
52 MEDIA_ROOT = './media/'
53
53
54 # URL that handles the media served from MEDIA_ROOT. Make sure to use a
54 # URL that handles the media served from MEDIA_ROOT. Make sure to use a
55 # trailing slash.
55 # trailing slash.
56 # Examples: "http://media.lawrence.com/media/", "http://example.com/media/"
56 # Examples: "http://media.lawrence.com/media/", "http://example.com/media/"
57 MEDIA_URL = '/media/'
57 MEDIA_URL = '/media/'
58
58
59 # Absolute path to the directory static files should be collected to.
59 # Absolute path to the directory static files should be collected to.
60 # Don't put anything in this directory yourself; store your static files
60 # Don't put anything in this directory yourself; store your static files
61 # in apps' "static/" subdirectories and in STATICFILES_DIRS.
61 # in apps' "static/" subdirectories and in STATICFILES_DIRS.
62 # Example: "/home/media/media.lawrence.com/static/"
62 # Example: "/home/media/media.lawrence.com/static/"
63 STATIC_ROOT = ''
63 STATIC_ROOT = ''
64
64
65 # URL prefix for static files.
65 # URL prefix for static files.
66 # Example: "http://media.lawrence.com/static/"
66 # Example: "http://media.lawrence.com/static/"
67 STATIC_URL = '/static/'
67 STATIC_URL = '/static/'
68
68
69 # Additional locations of static files
69 # Additional locations of static files
70 # It is really a hack, put real paths, not related
70 # It is really a hack, put real paths, not related
71 STATICFILES_DIRS = (
71 STATICFILES_DIRS = (
72 os.path.dirname(__file__) + '/boards/static',
72 os.path.dirname(__file__) + '/boards/static',
73
73
74 # '/d/work/python/django/neboard/neboard/boards/static',
74 # '/d/work/python/django/neboard/neboard/boards/static',
75 # Put strings here, like "/home/html/static" or "C:/www/django/static".
75 # Put strings here, like "/home/html/static" or "C:/www/django/static".
76 # Always use forward slashes, even on Windows.
76 # Always use forward slashes, even on Windows.
77 # Don't forget to use absolute paths, not relative paths.
77 # Don't forget to use absolute paths, not relative paths.
78 )
78 )
79
79
80 # List of finder classes that know how to find static files in
80 # List of finder classes that know how to find static files in
81 # various locations.
81 # various locations.
82 STATICFILES_FINDERS = (
82 STATICFILES_FINDERS = (
83 'django.contrib.staticfiles.finders.FileSystemFinder',
83 'django.contrib.staticfiles.finders.FileSystemFinder',
84 'django.contrib.staticfiles.finders.AppDirectoriesFinder',
84 'django.contrib.staticfiles.finders.AppDirectoriesFinder',
85 )
85 )
86
86
87 if DEBUG:
87 if DEBUG:
88 STATICFILES_STORAGE = \
88 STATICFILES_STORAGE = \
89 'django.contrib.staticfiles.storage.StaticFilesStorage'
89 'django.contrib.staticfiles.storage.StaticFilesStorage'
90 else:
90 else:
91 STATICFILES_STORAGE = \
91 STATICFILES_STORAGE = \
92 'django.contrib.staticfiles.storage.CachedStaticFilesStorage'
92 'django.contrib.staticfiles.storage.CachedStaticFilesStorage'
93
93
94 # Make this unique, and don't share it with anybody.
94 # Make this unique, and don't share it with anybody.
95 SECRET_KEY = '@1rc$o(7=tt#kd+4s$u6wchm**z^)4x90)7f6z(i&amp;55@o11*8o'
95 SECRET_KEY = '@1rc$o(7=tt#kd+4s$u6wchm**z^)4x90)7f6z(i&amp;55@o11*8o'
96
96
97 # List of callables that know how to import templates from various sources.
97 # List of callables that know how to import templates from various sources.
98 TEMPLATE_LOADERS = (
98 TEMPLATE_LOADERS = (
99 'django.template.loaders.filesystem.Loader',
99 'django.template.loaders.filesystem.Loader',
100 'django.template.loaders.app_directories.Loader',
100 'django.template.loaders.app_directories.Loader',
101 )
101 )
102
102
103 TEMPLATE_CONTEXT_PROCESSORS = (
103 TEMPLATE_CONTEXT_PROCESSORS = (
104 'django.core.context_processors.media',
104 'django.core.context_processors.media',
105 'django.core.context_processors.static',
105 'django.core.context_processors.static',
106 'django.core.context_processors.request',
106 'django.core.context_processors.request',
107 'django.contrib.auth.context_processors.auth',
107 'django.contrib.auth.context_processors.auth',
108 'boards.context_processors.user_and_ui_processor',
108 'boards.context_processors.user_and_ui_processor',
109 )
109 )
110
110
111 MIDDLEWARE_CLASSES = (
111 MIDDLEWARE_CLASSES = (
112 'django.contrib.sessions.middleware.SessionMiddleware',
112 'django.contrib.sessions.middleware.SessionMiddleware',
113 'django.middleware.locale.LocaleMiddleware',
113 'django.middleware.locale.LocaleMiddleware',
114 'django.middleware.common.CommonMiddleware',
114 'django.middleware.common.CommonMiddleware',
115 'django.contrib.auth.middleware.AuthenticationMiddleware',
115 'django.contrib.auth.middleware.AuthenticationMiddleware',
116 'django.contrib.messages.middleware.MessageMiddleware',
116 'django.contrib.messages.middleware.MessageMiddleware',
117 'boards.middlewares.BanMiddleware',
117 'boards.middlewares.BanMiddleware',
118 'boards.middlewares.MinifyHTMLMiddleware',
118 'boards.middlewares.MinifyHTMLMiddleware',
119 )
119 )
120
120
121 ROOT_URLCONF = 'neboard.urls'
121 ROOT_URLCONF = 'neboard.urls'
122
122
123 # Python dotted path to the WSGI application used by Django's runserver.
123 # Python dotted path to the WSGI application used by Django's runserver.
124 WSGI_APPLICATION = 'neboard.wsgi.application'
124 WSGI_APPLICATION = 'neboard.wsgi.application'
125
125
126 TEMPLATE_DIRS = (
126 TEMPLATE_DIRS = (
127 # Put strings here, like "/home/html/django_templates" or "C:/www/django/templates".
127 # Put strings here, like "/home/html/django_templates" or "C:/www/django/templates".
128 # Always use forward slashes, even on Windows.
128 # Always use forward slashes, even on Windows.
129 # Don't forget to use absolute paths, not relative paths.
129 # Don't forget to use absolute paths, not relative paths.
130 'templates',
130 'templates',
131 )
131 )
132
132
133 INSTALLED_APPS = (
133 INSTALLED_APPS = (
134 'django.contrib.auth',
134 'django.contrib.auth',
135 'django.contrib.contenttypes',
135 'django.contrib.contenttypes',
136 'django.contrib.sessions',
136 'django.contrib.sessions',
137 # 'django.contrib.sites',
137 # 'django.contrib.sites',
138 'django.contrib.messages',
138 'django.contrib.messages',
139 'django.contrib.staticfiles',
139 'django.contrib.staticfiles',
140 # Uncomment the next line to enable the admin:
140 # Uncomment the next line to enable the admin:
141 'django.contrib.admin',
141 'django.contrib.admin',
142 # Uncomment the next line to enable admin documentation:
142 # Uncomment the next line to enable admin documentation:
143 # 'django.contrib.admindocs',
143 # 'django.contrib.admindocs',
144 'django.contrib.humanize',
144 'django.contrib.humanize',
145 'django_cleanup',
145 'django_cleanup',
146
146
147 # Migrations
147 # Migrations
148 'south',
148 'south',
149 'debug_toolbar',
149 'debug_toolbar',
150
150
151 'captcha',
151 'captcha',
152
152
153 # Search
153 # Search
154 'haystack',
154 'haystack',
155
155
156 'boards',
156 'boards',
157 )
157 )
158
158
159 DEBUG_TOOLBAR_PANELS = (
159 DEBUG_TOOLBAR_PANELS = (
160 'debug_toolbar.panels.version.VersionDebugPanel',
160 'debug_toolbar.panels.version.VersionDebugPanel',
161 'debug_toolbar.panels.timer.TimerDebugPanel',
161 'debug_toolbar.panels.timer.TimerDebugPanel',
162 'debug_toolbar.panels.settings_vars.SettingsVarsDebugPanel',
162 'debug_toolbar.panels.settings_vars.SettingsVarsDebugPanel',
163 'debug_toolbar.panels.headers.HeaderDebugPanel',
163 'debug_toolbar.panels.headers.HeaderDebugPanel',
164 'debug_toolbar.panels.request_vars.RequestVarsDebugPanel',
164 'debug_toolbar.panels.request_vars.RequestVarsDebugPanel',
165 'debug_toolbar.panels.template.TemplateDebugPanel',
165 'debug_toolbar.panels.template.TemplateDebugPanel',
166 'debug_toolbar.panels.sql.SQLDebugPanel',
166 'debug_toolbar.panels.sql.SQLDebugPanel',
167 'debug_toolbar.panels.signals.SignalDebugPanel',
167 'debug_toolbar.panels.signals.SignalDebugPanel',
168 'debug_toolbar.panels.logger.LoggingPanel',
168 'debug_toolbar.panels.logger.LoggingPanel',
169 )
169 )
170
170
171 # TODO: NEED DESIGN FIXES
171 # TODO: NEED DESIGN FIXES
172 CAPTCHA_OUTPUT_FORMAT = (u' %(hidden_field)s '
172 CAPTCHA_OUTPUT_FORMAT = (u' %(hidden_field)s '
173 u'<div class="form-label">%(image)s</div>'
173 u'<div class="form-label">%(image)s</div>'
174 u'<div class="form-text">%(text_field)s</div>')
174 u'<div class="form-text">%(text_field)s</div>')
175
175
176 # A sample logging configuration. The only tangible logging
176 # A sample logging configuration. The only tangible logging
177 # performed by this configuration is to send an email to
177 # performed by this configuration is to send an email to
178 # the site admins on every HTTP 500 error when DEBUG=False.
178 # the site admins on every HTTP 500 error when DEBUG=False.
179 # See http://docs.djangoproject.com/en/dev/topics/logging for
179 # See http://docs.djangoproject.com/en/dev/topics/logging for
180 # more details on how to customize your logging configuration.
180 # more details on how to customize your logging configuration.
181 LOGGING = {
181 LOGGING = {
182 'version': 1,
182 'version': 1,
183 'disable_existing_loggers': False,
183 'disable_existing_loggers': False,
184 'formatters': {
184 'formatters': {
185 'verbose': {
185 'verbose': {
186 'format': '%(levelname)s %(asctime)s %(module)s %(process)d %(thread)d %(message)s'
186 'format': '%(levelname)s %(asctime)s %(module)s %(process)d %(thread)d %(message)s'
187 },
187 },
188 'simple': {
188 'simple': {
189 'format': '%(levelname)s %(asctime)s [%(module)s] %(message)s'
189 'format': '%(levelname)s %(asctime)s [%(module)s] %(message)s'
190 },
190 },
191 },
191 },
192 'filters': {
192 'filters': {
193 'require_debug_false': {
193 'require_debug_false': {
194 '()': 'django.utils.log.RequireDebugFalse'
194 '()': 'django.utils.log.RequireDebugFalse'
195 }
195 }
196 },
196 },
197 'handlers': {
197 'handlers': {
198 'console': {
198 'console': {
199 'level': 'DEBUG',
199 'level': 'DEBUG',
200 'class': 'logging.StreamHandler',
200 'class': 'logging.StreamHandler',
201 'formatter': 'simple'
201 'formatter': 'simple'
202 },
202 },
203 },
203 },
204 'loggers': {
204 'loggers': {
205 'boards': {
205 'boards': {
206 'handlers': ['console'],
206 'handlers': ['console'],
207 'level': 'DEBUG',
207 'level': 'DEBUG',
208 }
208 }
209 },
209 },
210 }
210 }
211
211
212 HAYSTACK_CONNECTIONS = {
212 HAYSTACK_CONNECTIONS = {
213 'default': {
213 'default': {
214 'ENGINE': 'haystack.backends.whoosh_backend.WhooshEngine',
214 'ENGINE': 'haystack.backends.whoosh_backend.WhooshEngine',
215 'PATH': os.path.join(os.path.dirname(__file__), 'whoosh_index'),
215 'PATH': os.path.join(os.path.dirname(__file__), 'whoosh_index'),
216 },
216 },
217 }
217 }
218
218
219 HAYSTACK_SIGNAL_PROCESSOR = 'haystack.signals.RealtimeSignalProcessor'
219 HAYSTACK_SIGNAL_PROCESSOR = 'haystack.signals.RealtimeSignalProcessor'
220
220
221 MARKUP_FIELD_TYPES = (
221 MARKUP_FIELD_TYPES = (
222 ('markdown', markdown_extended),
222 ('markdown', markdown_extended),
223 )
223 )
224 # Custom imageboard settings
225 # TODO These should me moved to
226 MAX_POSTS_PER_THREAD = 10 # Thread bumplimit
227 MAX_THREAD_COUNT = 5 # Old threads will be deleted to preserve this count
228 THREADS_PER_PAGE = 3
229 SITE_NAME = 'Neboard'
230
224
231 THEMES = [
225 THEMES = [
232 ('md', 'Mystic Dark'),
226 ('md', 'Mystic Dark'),
233 ('md_centered', 'Mystic Dark (centered)'),
227 ('md_centered', 'Mystic Dark (centered)'),
234 ('sw', 'Snow White'),
228 ('sw', 'Snow White'),
235 ('pg', 'Photon Gray'),
229 ('pg', 'Photon Gray'),
236 ]
230 ]
237
231
238 DEFAULT_THEME = 'md'
239
240 POPULAR_TAGS = 10
232 POPULAR_TAGS = 10
241 LAST_REPLIES_COUNT = 3
242
233
243 ENABLE_CAPTCHA = False
234 ENABLE_CAPTCHA = False
244 # if user tries to post before CAPTCHA_DEFAULT_SAFE_TIME. Captcha will be shown
235 # if user tries to post before CAPTCHA_DEFAULT_SAFE_TIME. Captcha will be shown
245 CAPTCHA_DEFAULT_SAFE_TIME = 30 # seconds
236 CAPTCHA_DEFAULT_SAFE_TIME = 30 # seconds
246 POSTING_DELAY = 20 # seconds
237 POSTING_DELAY = 20 # seconds
247
238
248 COMPRESS_HTML = True
239 COMPRESS_HTML = True
249
240
250 VERSION = '1.8.0 Kara'
251
252 # Debug mode middlewares
241 # Debug mode middlewares
253 if DEBUG:
242 if DEBUG:
254
255 SITE_NAME += ' DEBUG'
256
257 MIDDLEWARE_CLASSES += (
243 MIDDLEWARE_CLASSES += (
258 'boards.profiler.ProfilerMiddleware',
244 'boards.profiler.ProfilerMiddleware',
259 'debug_toolbar.middleware.DebugToolbarMiddleware',
245 'debug_toolbar.middleware.DebugToolbarMiddleware',
260 )
246 )
261
247
262 def custom_show_toolbar(request):
248 def custom_show_toolbar(request):
263 return DEBUG
249 return DEBUG
264
250
265 DEBUG_TOOLBAR_CONFIG = {
251 DEBUG_TOOLBAR_CONFIG = {
266 'INTERCEPT_REDIRECTS': False,
252 'INTERCEPT_REDIRECTS': False,
267 'SHOW_TOOLBAR_CALLBACK': custom_show_toolbar,
253 'SHOW_TOOLBAR_CALLBACK': custom_show_toolbar,
268 'HIDE_DJANGO_SQL': False,
254 'HIDE_DJANGO_SQL': False,
269 'ENABLE_STACKTRACES': True,
255 'ENABLE_STACKTRACES': True,
270 }
256 }
271
257
272 # FIXME Uncommenting this fails somehow. Need to investigate this
258 # FIXME Uncommenting this fails somehow. Need to investigate this
273 #DEBUG_TOOLBAR_PANELS += (
259 #DEBUG_TOOLBAR_PANELS += (
274 # 'debug_toolbar.panels.profiling.ProfilingDebugPanel',
260 # 'debug_toolbar.panels.profiling.ProfilingDebugPanel',
275 #)
261 #)
276
262
General Comments 0
You need to be logged in to leave comments. Login now