##// END OF EJS Templates
Moving neboard to python3 support (no python2 for now until we figure out how...
neko259 -
r765:bb8477db default
parent child Browse files
Show More
@@ -1,341 +1,298 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
6 from django import forms
5 from django import forms
7 from django.forms.util import ErrorList
6 from django.forms.util import ErrorList
8 from django.utils.translation import ugettext_lazy as _
7 from django.utils.translation import ugettext_lazy as _
9
8
10 from boards.mdx_neboard import formatters
9 from boards.mdx_neboard import formatters
11 from boards.models.post import TITLE_MAX_LENGTH
10 from boards.models.post import TITLE_MAX_LENGTH
12 from boards.models import PostImage
11 from boards.models import PostImage
13 from neboard import settings
12 from neboard import settings
14 from boards import utils
13 from boards import utils
15 import boards.settings as board_settings
14 import boards.settings as board_settings
16
15
17 VETERAN_POSTING_DELAY = 5
16 VETERAN_POSTING_DELAY = 5
18
17
19 ATTRIBUTE_PLACEHOLDER = 'placeholder'
18 ATTRIBUTE_PLACEHOLDER = 'placeholder'
20
19
21 LAST_POST_TIME = 'last_post_time'
20 LAST_POST_TIME = 'last_post_time'
22 LAST_LOGIN_TIME = 'last_login_time'
21 LAST_LOGIN_TIME = 'last_login_time'
23 TEXT_PLACEHOLDER = _('''Type message here. Use formatting panel for more advanced usage.''')
22 TEXT_PLACEHOLDER = _('''Type message here. Use formatting panel for more advanced usage.''')
24 TAGS_PLACEHOLDER = _('tag1 several_words_tag')
23 TAGS_PLACEHOLDER = _('tag1 several_words_tag')
25
24
26 ERROR_IMAGE_DUPLICATE = _('Such image was already posted')
25 ERROR_IMAGE_DUPLICATE = _('Such image was already posted')
27
26
28 LABEL_TITLE = _('Title')
27 LABEL_TITLE = _('Title')
29 LABEL_TEXT = _('Text')
28 LABEL_TEXT = _('Text')
30 LABEL_TAG = _('Tag')
29 LABEL_TAG = _('Tag')
31 LABEL_SEARCH = _('Search')
30 LABEL_SEARCH = _('Search')
32
31
33 TAG_MAX_LENGTH = 20
32 TAG_MAX_LENGTH = 20
34
33
35 REGEX_TAG = ur'^[\w\d]+$'
34 REGEX_TAG = r'^[\w\d]+$'
36
35
37
36
38 class FormatPanel(forms.Textarea):
37 class FormatPanel(forms.Textarea):
39 def render(self, name, value, attrs=None):
38 def render(self, name, value, attrs=None):
40 output = '<div id="mark-panel">'
39 output = '<div id="mark-panel">'
41 for formatter in formatters:
40 for formatter in formatters:
42 output += u'<span class="mark_btn"' + \
41 output += u'<span class="mark_btn"' + \
43 u' onClick="addMarkToMsg(\'' + formatter.format_left + \
42 u' onClick="addMarkToMsg(\'' + formatter.format_left + \
44 '\', \'' + formatter.format_right + '\')">' + \
43 '\', \'' + formatter.format_right + '\')">' + \
45 formatter.preview_left + formatter.name + \
44 formatter.preview_left + formatter.name + \
46 formatter.preview_right + u'</span>'
45 formatter.preview_right + u'</span>'
47
46
48 output += '</div>'
47 output += '</div>'
49 output += super(FormatPanel, self).render(name, value, attrs=None)
48 output += super(FormatPanel, self).render(name, value, attrs=None)
50
49
51 return output
50 return output
52
51
53
52
54 class PlainErrorList(ErrorList):
53 class PlainErrorList(ErrorList):
55 def __unicode__(self):
54 def __unicode__(self):
56 return self.as_text()
55 return self.as_text()
57
56
58 def as_text(self):
57 def as_text(self):
59 return ''.join([u'(!) %s ' % e for e in self])
58 return ''.join([u'(!) %s ' % e for e in self])
60
59
61
60
62 class NeboardForm(forms.Form):
61 class NeboardForm(forms.Form):
63
62
64 def as_div(self):
63 def as_div(self):
65 """
64 """
66 Returns this form rendered as HTML <as_div>s.
65 Returns this form rendered as HTML <as_div>s.
67 """
66 """
68
67
69 return self._html_output(
68 return self._html_output(
70 # TODO Do not show hidden rows in the list here
69 # TODO Do not show hidden rows in the list here
71 normal_row='<div class="form-row"><div class="form-label">'
70 normal_row='<div class="form-row"><div class="form-label">'
72 '%(label)s'
71 '%(label)s'
73 '</div></div>'
72 '</div></div>'
74 '<div class="form-row"><div class="form-input">'
73 '<div class="form-row"><div class="form-input">'
75 '%(field)s'
74 '%(field)s'
76 '</div></div>'
75 '</div></div>'
77 '<div class="form-row">'
76 '<div class="form-row">'
78 '%(help_text)s'
77 '%(help_text)s'
79 '</div>',
78 '</div>',
80 error_row='<div class="form-row">'
79 error_row='<div class="form-row">'
81 '<div class="form-label"></div>'
80 '<div class="form-label"></div>'
82 '<div class="form-errors">%s</div>'
81 '<div class="form-errors">%s</div>'
83 '</div>',
82 '</div>',
84 row_ender='</div>',
83 row_ender='</div>',
85 help_text_html='%s',
84 help_text_html='%s',
86 errors_on_separate_row=True)
85 errors_on_separate_row=True)
87
86
88 def as_json_errors(self):
87 def as_json_errors(self):
89 errors = []
88 errors = []
90
89
91 for name, field in self.fields.items():
90 for name, field in self.fields.items():
92 if self[name].errors:
91 if self[name].errors:
93 errors.append({
92 errors.append({
94 'field': name,
93 'field': name,
95 'errors': self[name].errors.as_text(),
94 'errors': self[name].errors.as_text(),
96 })
95 })
97
96
98 return errors
97 return errors
99
98
100
99
101 class PostForm(NeboardForm):
100 class PostForm(NeboardForm):
102
101
103 title = forms.CharField(max_length=TITLE_MAX_LENGTH, required=False,
102 title = forms.CharField(max_length=TITLE_MAX_LENGTH, required=False,
104 label=LABEL_TITLE)
103 label=LABEL_TITLE)
105 text = forms.CharField(
104 text = forms.CharField(
106 widget=FormatPanel(attrs={ATTRIBUTE_PLACEHOLDER: TEXT_PLACEHOLDER}),
105 widget=FormatPanel(attrs={ATTRIBUTE_PLACEHOLDER: TEXT_PLACEHOLDER}),
107 required=False, label=LABEL_TEXT)
106 required=False, label=LABEL_TEXT)
108 image = forms.ImageField(required=False, label=_('Image'),
107 image = forms.ImageField(required=False, label=_('Image'),
109 widget=forms.ClearableFileInput(
108 widget=forms.ClearableFileInput(
110 attrs={'accept': 'image/*'}))
109 attrs={'accept': 'image/*'}))
111
110
112 # This field is for spam prevention only
111 # This field is for spam prevention only
113 email = forms.CharField(max_length=100, required=False, label=_('e-mail'),
112 email = forms.CharField(max_length=100, required=False, label=_('e-mail'),
114 widget=forms.TextInput(attrs={
113 widget=forms.TextInput(attrs={
115 'class': 'form-email'}))
114 'class': 'form-email'}))
116
115
117 session = None
116 session = None
118 need_to_ban = False
117 need_to_ban = False
119
118
120 def clean_title(self):
119 def clean_title(self):
121 title = self.cleaned_data['title']
120 title = self.cleaned_data['title']
122 if title:
121 if title:
123 if len(title) > TITLE_MAX_LENGTH:
122 if len(title) > TITLE_MAX_LENGTH:
124 raise forms.ValidationError(_('Title must have less than %s '
123 raise forms.ValidationError(_('Title must have less than %s '
125 'characters') %
124 'characters') %
126 str(TITLE_MAX_LENGTH))
125 str(TITLE_MAX_LENGTH))
127 return title
126 return title
128
127
129 def clean_text(self):
128 def clean_text(self):
130 text = self.cleaned_data['text'].strip()
129 text = self.cleaned_data['text'].strip()
131 if text:
130 if text:
132 if len(text) > board_settings.MAX_TEXT_LENGTH:
131 if len(text) > board_settings.MAX_TEXT_LENGTH:
133 raise forms.ValidationError(_('Text must have less than %s '
132 raise forms.ValidationError(_('Text must have less than %s '
134 'characters') %
133 'characters') %
135 str(board_settings
134 str(board_settings
136 .MAX_TEXT_LENGTH))
135 .MAX_TEXT_LENGTH))
137 return text
136 return text
138
137
139 def clean_image(self):
138 def clean_image(self):
140 image = self.cleaned_data['image']
139 image = self.cleaned_data['image']
141 if image:
140 if image:
142 if image.size > board_settings.MAX_IMAGE_SIZE:
141 if image.size > board_settings.MAX_IMAGE_SIZE:
143 raise forms.ValidationError(
142 raise forms.ValidationError(
144 _('Image must be less than %s bytes')
143 _('Image must be less than %s bytes')
145 % str(board_settings.MAX_IMAGE_SIZE))
144 % str(board_settings.MAX_IMAGE_SIZE))
146
145
147 md5 = hashlib.md5()
146 md5 = hashlib.md5()
148 for chunk in image.chunks():
147 for chunk in image.chunks():
149 md5.update(chunk)
148 md5.update(chunk)
150 image_hash = md5.hexdigest()
149 image_hash = md5.hexdigest()
151 if PostImage.objects.filter(hash=image_hash).exists():
150 if PostImage.objects.filter(hash=image_hash).exists():
152 raise forms.ValidationError(ERROR_IMAGE_DUPLICATE)
151 raise forms.ValidationError(ERROR_IMAGE_DUPLICATE)
153
152
154 return image
153 return image
155
154
156 def clean(self):
155 def clean(self):
157 cleaned_data = super(PostForm, self).clean()
156 cleaned_data = super(PostForm, self).clean()
158
157
159 if not self.session:
158 if not self.session:
160 raise forms.ValidationError('Humans have sessions')
159 raise forms.ValidationError('Humans have sessions')
161
160
162 if cleaned_data['email']:
161 if cleaned_data['email']:
163 self.need_to_ban = True
162 self.need_to_ban = True
164 raise forms.ValidationError('A human cannot enter a hidden field')
163 raise forms.ValidationError('A human cannot enter a hidden field')
165
164
166 if not self.errors:
165 if not self.errors:
167 self._clean_text_image()
166 self._clean_text_image()
168
167
169 if not self.errors and self.session:
168 if not self.errors and self.session:
170 self._validate_posting_speed()
169 self._validate_posting_speed()
171
170
172 return cleaned_data
171 return cleaned_data
173
172
174 def _clean_text_image(self):
173 def _clean_text_image(self):
175 text = self.cleaned_data.get('text')
174 text = self.cleaned_data.get('text')
176 image = self.cleaned_data.get('image')
175 image = self.cleaned_data.get('image')
177
176
178 if (not text) and (not image):
177 if (not text) and (not image):
179 error_message = _('Either text or image must be entered.')
178 error_message = _('Either text or image must be entered.')
180 self._errors['text'] = self.error_class([error_message])
179 self._errors['text'] = self.error_class([error_message])
181
180
182 def _validate_posting_speed(self):
181 def _validate_posting_speed(self):
183 can_post = True
182 can_post = True
184
183
185 # TODO Remove this, it's only for test
184 # TODO Remove this, it's only for test
186 if not 'user_id' in self.session:
185 if not 'user_id' in self.session:
187 return
186 return
188
187
189 posting_delay = settings.POSTING_DELAY
188 posting_delay = settings.POSTING_DELAY
190
189
191 if board_settings.LIMIT_POSTING_SPEED and LAST_POST_TIME in \
190 if board_settings.LIMIT_POSTING_SPEED and LAST_POST_TIME in \
192 self.session:
191 self.session:
193 now = time.time()
192 now = time.time()
194 last_post_time = self.session[LAST_POST_TIME]
193 last_post_time = self.session[LAST_POST_TIME]
195
194
196 current_delay = int(now - last_post_time)
195 current_delay = int(now - last_post_time)
197
196
198 if current_delay < posting_delay:
197 if current_delay < posting_delay:
199 error_message = _('Wait %s seconds after last posting') % str(
198 error_message = _('Wait %s seconds after last posting') % str(
200 posting_delay - current_delay)
199 posting_delay - current_delay)
201 self._errors['text'] = self.error_class([error_message])
200 self._errors['text'] = self.error_class([error_message])
202
201
203 can_post = False
202 can_post = False
204
203
205 if can_post:
204 if can_post:
206 self.session[LAST_POST_TIME] = time.time()
205 self.session[LAST_POST_TIME] = time.time()
207
206
208
207
209 class ThreadForm(PostForm):
208 class ThreadForm(PostForm):
210
209
211 regex_tags = re.compile(ur'^[\w\s\d]+$', re.UNICODE)
210 regex_tags = re.compile(r'^[\w\s\d]+$', re.UNICODE)
212
211
213 tags = forms.CharField(
212 tags = forms.CharField(
214 widget=forms.TextInput(attrs={ATTRIBUTE_PLACEHOLDER: TAGS_PLACEHOLDER}),
213 widget=forms.TextInput(attrs={ATTRIBUTE_PLACEHOLDER: TAGS_PLACEHOLDER}),
215 max_length=100, label=_('Tags'), required=True)
214 max_length=100, label=_('Tags'), required=True)
216
215
217 def clean_tags(self):
216 def clean_tags(self):
218 tags = self.cleaned_data['tags'].strip()
217 tags = self.cleaned_data['tags'].strip()
219
218
220 if not tags or not self.regex_tags.match(tags):
219 if not tags or not self.regex_tags.match(tags):
221 raise forms.ValidationError(
220 raise forms.ValidationError(
222 _('Inappropriate characters in tags.'))
221 _('Inappropriate characters in tags.'))
223
222
224 return tags
223 return tags
225
224
226 def clean(self):
225 def clean(self):
227 cleaned_data = super(ThreadForm, self).clean()
226 cleaned_data = super(ThreadForm, self).clean()
228
227
229 return cleaned_data
228 return cleaned_data
230
229
231
230
232 class PostCaptchaForm(PostForm):
233 captcha = CaptchaField()
234
235 def __init__(self, *args, **kwargs):
236 self.request = kwargs['request']
237 del kwargs['request']
238
239 super(PostCaptchaForm, self).__init__(*args, **kwargs)
240
241 def clean(self):
242 cleaned_data = super(PostCaptchaForm, self).clean()
243
244 success = self.is_valid()
245 utils.update_captcha_access(self.request, success)
246
247 if success:
248 return cleaned_data
249 else:
250 raise forms.ValidationError(_("Captcha validation failed"))
251
252
253 class ThreadCaptchaForm(ThreadForm):
254 captcha = CaptchaField()
255
256 def __init__(self, *args, **kwargs):
257 self.request = kwargs['request']
258 del kwargs['request']
259
260 super(ThreadCaptchaForm, self).__init__(*args, **kwargs)
261
262 def clean(self):
263 cleaned_data = super(ThreadCaptchaForm, self).clean()
264
265 success = self.is_valid()
266 utils.update_captcha_access(self.request, success)
267
268 if success:
269 return cleaned_data
270 else:
271 raise forms.ValidationError(_("Captcha validation failed"))
272
273
274 class SettingsForm(NeboardForm):
231 class SettingsForm(NeboardForm):
275
232
276 theme = forms.ChoiceField(choices=settings.THEMES,
233 theme = forms.ChoiceField(choices=settings.THEMES,
277 label=_('Theme'))
234 label=_('Theme'))
278
235
279
236
280 class AddTagForm(NeboardForm):
237 class AddTagForm(NeboardForm):
281
238
282 tag = forms.CharField(max_length=TAG_MAX_LENGTH, label=LABEL_TAG)
239 tag = forms.CharField(max_length=TAG_MAX_LENGTH, label=LABEL_TAG)
283 method = forms.CharField(widget=forms.HiddenInput(), initial='add_tag')
240 method = forms.CharField(widget=forms.HiddenInput(), initial='add_tag')
284
241
285 def clean_tag(self):
242 def clean_tag(self):
286 tag = self.cleaned_data['tag']
243 tag = self.cleaned_data['tag']
287
244
288 regex_tag = re.compile(REGEX_TAG, re.UNICODE)
245 regex_tag = re.compile(REGEX_TAG, re.UNICODE)
289 if not regex_tag.match(tag):
246 if not regex_tag.match(tag):
290 raise forms.ValidationError(_('Inappropriate characters in tags.'))
247 raise forms.ValidationError(_('Inappropriate characters in tags.'))
291
248
292 return tag
249 return tag
293
250
294 def clean(self):
251 def clean(self):
295 cleaned_data = super(AddTagForm, self).clean()
252 cleaned_data = super(AddTagForm, self).clean()
296
253
297 return cleaned_data
254 return cleaned_data
298
255
299
256
300 class SearchForm(NeboardForm):
257 class SearchForm(NeboardForm):
301 query = forms.CharField(max_length=500, label=LABEL_SEARCH, required=False)
258 query = forms.CharField(max_length=500, label=LABEL_SEARCH, required=False)
302
259
303
260
304 class LoginForm(NeboardForm):
261 class LoginForm(NeboardForm):
305
262
306 password = forms.CharField()
263 password = forms.CharField()
307
264
308 session = None
265 session = None
309
266
310 def clean_password(self):
267 def clean_password(self):
311 password = self.cleaned_data['password']
268 password = self.cleaned_data['password']
312 if board_settings.MASTER_PASSWORD != password:
269 if board_settings.MASTER_PASSWORD != password:
313 raise forms.ValidationError(_('Invalid master password'))
270 raise forms.ValidationError(_('Invalid master password'))
314
271
315 return password
272 return password
316
273
317 def _validate_login_speed(self):
274 def _validate_login_speed(self):
318 can_post = True
275 can_post = True
319
276
320 if LAST_LOGIN_TIME in self.session:
277 if LAST_LOGIN_TIME in self.session:
321 now = time.time()
278 now = time.time()
322 last_login_time = self.session[LAST_LOGIN_TIME]
279 last_login_time = self.session[LAST_LOGIN_TIME]
323
280
324 current_delay = int(now - last_login_time)
281 current_delay = int(now - last_login_time)
325
282
326 if current_delay < board_settings.LOGIN_TIMEOUT:
283 if current_delay < board_settings.LOGIN_TIMEOUT:
327 error_message = _('Wait %s minutes after last login') % str(
284 error_message = _('Wait %s minutes after last login') % str(
328 (board_settings.LOGIN_TIMEOUT - current_delay) / 60)
285 (board_settings.LOGIN_TIMEOUT - current_delay) / 60)
329 self._errors['password'] = self.error_class([error_message])
286 self._errors['password'] = self.error_class([error_message])
330
287
331 can_post = False
288 can_post = False
332
289
333 if can_post:
290 if can_post:
334 self.session[LAST_LOGIN_TIME] = time.time()
291 self.session[LAST_LOGIN_TIME] = time.time()
335
292
336 def clean(self):
293 def clean(self):
337 self._validate_login_speed()
294 self._validate_login_speed()
338
295
339 cleaned_data = super(LoginForm, self).clean()
296 cleaned_data = super(LoginForm, self).clean()
340
297
341 return cleaned_data
298 return cleaned_data
@@ -1,98 +1,98 b''
1 # -*- coding: utf-8 -*-
1 # -*- coding: utf-8 -*-
2 from south.utils import datetime_utils as datetime
2 from south.utils import datetime_utils as datetime
3 from south.db import db
3 from south.db import db
4 from south.v2 import DataMigration
4 from south.v2 import DataMigration
5 from django.db import models
5 from django.db import models
6 from boards import views
6 from boards import views
7
7
8
8
9 class Migration(DataMigration):
9 class Migration(DataMigration):
10
10
11 def forwards(self, orm):
11 def forwards(self, orm):
12 for post in orm.Post.objects.filter(thread=None):
12 for post in orm.Post.objects.filter(thread=None):
13 thread = orm.Thread.objects.create(
13 thread = orm.Thread.objects.create(
14 bump_time=post.bump_time,
14 bump_time=post.bump_time,
15 last_edit_time=post.last_edit_time)
15 last_edit_time=post.last_edit_time)
16
16
17 thread.replies.add(post)
17 thread.replies.add(post)
18 post.thread_new = thread
18 post.thread_new = thread
19 post.save()
19 post.save()
20 print str(post.thread_new.id)
20 print(str(post.thread_new.id))
21
21
22 for reply in post.replies.all():
22 for reply in post.replies.all():
23 thread.replies.add(reply)
23 thread.replies.add(reply)
24 reply.thread_new = thread
24 reply.thread_new = thread
25 reply.save()
25 reply.save()
26
26
27 for tag in post.tags.all():
27 for tag in post.tags.all():
28 thread.tags.add(tag)
28 thread.tags.add(tag)
29 tag.threads.add(thread)
29 tag.threads.add(thread)
30
30
31 def backwards(self, orm):
31 def backwards(self, orm):
32 pass
32 pass
33
33
34 models = {
34 models = {
35 'boards.ban': {
35 'boards.ban': {
36 'Meta': {'object_name': 'Ban'},
36 'Meta': {'object_name': 'Ban'},
37 'can_read': ('django.db.models.fields.BooleanField', [], {'default': 'True'}),
37 'can_read': ('django.db.models.fields.BooleanField', [], {'default': 'True'}),
38 u'id': ('django.db.models.fields.AutoField', [], {'primary_key': 'True'}),
38 u'id': ('django.db.models.fields.AutoField', [], {'primary_key': 'True'}),
39 'ip': ('django.db.models.fields.GenericIPAddressField', [], {'max_length': '39'}),
39 'ip': ('django.db.models.fields.GenericIPAddressField', [], {'max_length': '39'}),
40 'reason': ('django.db.models.fields.CharField', [], {'default': "'Auto'", 'max_length': '200'})
40 'reason': ('django.db.models.fields.CharField', [], {'default': "'Auto'", 'max_length': '200'})
41 },
41 },
42 'boards.post': {
42 'boards.post': {
43 'Meta': {'object_name': 'Post'},
43 'Meta': {'object_name': 'Post'},
44 '_text_rendered': ('django.db.models.fields.TextField', [], {}),
44 '_text_rendered': ('django.db.models.fields.TextField', [], {}),
45 'bump_time': ('django.db.models.fields.DateTimeField', [], {}),
45 'bump_time': ('django.db.models.fields.DateTimeField', [], {}),
46 u'id': ('django.db.models.fields.AutoField', [], {'primary_key': 'True'}),
46 u'id': ('django.db.models.fields.AutoField', [], {'primary_key': 'True'}),
47 'image': ('boards.thumbs.ImageWithThumbsField', [], {'max_length': '100', 'blank': 'True'}),
47 'image': ('boards.thumbs.ImageWithThumbsField', [], {'max_length': '100', 'blank': 'True'}),
48 'image_height': ('django.db.models.fields.IntegerField', [], {'default': '0'}),
48 'image_height': ('django.db.models.fields.IntegerField', [], {'default': '0'}),
49 'image_width': ('django.db.models.fields.IntegerField', [], {'default': '0'}),
49 'image_width': ('django.db.models.fields.IntegerField', [], {'default': '0'}),
50 'last_edit_time': ('django.db.models.fields.DateTimeField', [], {}),
50 'last_edit_time': ('django.db.models.fields.DateTimeField', [], {}),
51 'poster_ip': ('django.db.models.fields.GenericIPAddressField', [], {'max_length': '39'}),
51 'poster_ip': ('django.db.models.fields.GenericIPAddressField', [], {'max_length': '39'}),
52 'poster_user_agent': ('django.db.models.fields.TextField', [], {}),
52 'poster_user_agent': ('django.db.models.fields.TextField', [], {}),
53 'pub_time': ('django.db.models.fields.DateTimeField', [], {}),
53 'pub_time': ('django.db.models.fields.DateTimeField', [], {}),
54 'referenced_posts': ('django.db.models.fields.related.ManyToManyField', [], {'blank': 'True', 'related_name': "'rfp+'", 'null': 'True', 'symmetrical': 'False', 'to': "orm['boards.Post']"}),
54 'referenced_posts': ('django.db.models.fields.related.ManyToManyField', [], {'blank': 'True', 'related_name': "'rfp+'", 'null': 'True', 'symmetrical': 'False', 'to': "orm['boards.Post']"}),
55 'replies': ('django.db.models.fields.related.ManyToManyField', [], {'blank': 'True', 'related_name': "'re+'", 'null': 'True', 'symmetrical': 'False', 'to': "orm['boards.Post']"}),
55 'replies': ('django.db.models.fields.related.ManyToManyField', [], {'blank': 'True', 'related_name': "'re+'", 'null': 'True', 'symmetrical': 'False', 'to': "orm['boards.Post']"}),
56 'tags': ('django.db.models.fields.related.ManyToManyField', [], {'to': "orm['boards.Tag']", 'symmetrical': 'False'}),
56 'tags': ('django.db.models.fields.related.ManyToManyField', [], {'to': "orm['boards.Tag']", 'symmetrical': 'False'}),
57 'text': ('markupfield.fields.MarkupField', [], {'rendered_field': 'True'}),
57 'text': ('markupfield.fields.MarkupField', [], {'rendered_field': 'True'}),
58 'text_markup_type': ('django.db.models.fields.CharField', [], {'default': "'markdown'", 'max_length': '30'}),
58 'text_markup_type': ('django.db.models.fields.CharField', [], {'default': "'markdown'", 'max_length': '30'}),
59 'thread': ('django.db.models.fields.related.ForeignKey', [], {'default': 'None', 'to': "orm['boards.Post']", 'null': 'True'}),
59 'thread': ('django.db.models.fields.related.ForeignKey', [], {'default': 'None', 'to': "orm['boards.Post']", 'null': 'True'}),
60 'thread_new': ('django.db.models.fields.related.ForeignKey', [], {'default': 'None', 'to': "orm['boards.Thread']", 'null': 'True'}),
60 'thread_new': ('django.db.models.fields.related.ForeignKey', [], {'default': 'None', 'to': "orm['boards.Thread']", 'null': 'True'}),
61 'title': ('django.db.models.fields.CharField', [], {'max_length': '50'}),
61 'title': ('django.db.models.fields.CharField', [], {'max_length': '50'}),
62 'user': ('django.db.models.fields.related.ForeignKey', [], {'default': 'None', 'to': "orm['boards.User']", 'null': 'True'})
62 'user': ('django.db.models.fields.related.ForeignKey', [], {'default': 'None', 'to': "orm['boards.User']", 'null': 'True'})
63 },
63 },
64 'boards.setting': {
64 'boards.setting': {
65 'Meta': {'object_name': 'Setting'},
65 'Meta': {'object_name': 'Setting'},
66 u'id': ('django.db.models.fields.AutoField', [], {'primary_key': 'True'}),
66 u'id': ('django.db.models.fields.AutoField', [], {'primary_key': 'True'}),
67 'name': ('django.db.models.fields.CharField', [], {'max_length': '50'}),
67 'name': ('django.db.models.fields.CharField', [], {'max_length': '50'}),
68 'user': ('django.db.models.fields.related.ForeignKey', [], {'to': "orm['boards.User']"}),
68 'user': ('django.db.models.fields.related.ForeignKey', [], {'to': "orm['boards.User']"}),
69 'value': ('django.db.models.fields.CharField', [], {'max_length': '50'})
69 'value': ('django.db.models.fields.CharField', [], {'max_length': '50'})
70 },
70 },
71 'boards.tag': {
71 'boards.tag': {
72 'Meta': {'object_name': 'Tag'},
72 'Meta': {'object_name': 'Tag'},
73 u'id': ('django.db.models.fields.AutoField', [], {'primary_key': 'True'}),
73 u'id': ('django.db.models.fields.AutoField', [], {'primary_key': 'True'}),
74 'linked': ('django.db.models.fields.related.ForeignKey', [], {'to': "orm['boards.Tag']", 'null': 'True', 'blank': 'True'}),
74 'linked': ('django.db.models.fields.related.ForeignKey', [], {'to': "orm['boards.Tag']", 'null': 'True', 'blank': 'True'}),
75 'name': ('django.db.models.fields.CharField', [], {'max_length': '100'}),
75 'name': ('django.db.models.fields.CharField', [], {'max_length': '100'}),
76 'threads': ('django.db.models.fields.related.ManyToManyField', [], {'blank': 'True', 'related_name': "'tag+'", 'null': 'True', 'symmetrical': 'False', 'to': "orm['boards.Thread']"})
76 'threads': ('django.db.models.fields.related.ManyToManyField', [], {'blank': 'True', 'related_name': "'tag+'", 'null': 'True', 'symmetrical': 'False', 'to': "orm['boards.Thread']"})
77 },
77 },
78 'boards.thread': {
78 'boards.thread': {
79 'Meta': {'object_name': 'Thread'},
79 'Meta': {'object_name': 'Thread'},
80 'bump_time': ('django.db.models.fields.DateTimeField', [], {}),
80 'bump_time': ('django.db.models.fields.DateTimeField', [], {}),
81 u'id': ('django.db.models.fields.AutoField', [], {'primary_key': 'True'}),
81 u'id': ('django.db.models.fields.AutoField', [], {'primary_key': 'True'}),
82 'last_edit_time': ('django.db.models.fields.DateTimeField', [], {}),
82 'last_edit_time': ('django.db.models.fields.DateTimeField', [], {}),
83 'replies': ('django.db.models.fields.related.ManyToManyField', [], {'blank': 'True', 'related_name': "'tre+'", 'null': 'True', 'symmetrical': 'False', 'to': "orm['boards.Post']"}),
83 'replies': ('django.db.models.fields.related.ManyToManyField', [], {'blank': 'True', 'related_name': "'tre+'", 'null': 'True', 'symmetrical': 'False', 'to': "orm['boards.Post']"}),
84 'tags': ('django.db.models.fields.related.ManyToManyField', [], {'to': "orm['boards.Tag']", 'symmetrical': 'False'})
84 'tags': ('django.db.models.fields.related.ManyToManyField', [], {'to': "orm['boards.Tag']", 'symmetrical': 'False'})
85 },
85 },
86 'boards.user': {
86 'boards.user': {
87 'Meta': {'object_name': 'User'},
87 'Meta': {'object_name': 'User'},
88 'fav_tags': ('django.db.models.fields.related.ManyToManyField', [], {'symmetrical': 'False', 'to': "orm['boards.Tag']", 'null': 'True', 'blank': 'True'}),
88 'fav_tags': ('django.db.models.fields.related.ManyToManyField', [], {'symmetrical': 'False', 'to': "orm['boards.Tag']", 'null': 'True', 'blank': 'True'}),
89 'fav_threads': ('django.db.models.fields.related.ManyToManyField', [], {'blank': 'True', 'related_name': "'+'", 'null': 'True', 'symmetrical': 'False', 'to': "orm['boards.Post']"}),
89 'fav_threads': ('django.db.models.fields.related.ManyToManyField', [], {'blank': 'True', 'related_name': "'+'", 'null': 'True', 'symmetrical': 'False', 'to': "orm['boards.Post']"}),
90 u'id': ('django.db.models.fields.AutoField', [], {'primary_key': 'True'}),
90 u'id': ('django.db.models.fields.AutoField', [], {'primary_key': 'True'}),
91 'rank': ('django.db.models.fields.IntegerField', [], {}),
91 'rank': ('django.db.models.fields.IntegerField', [], {}),
92 'registration_time': ('django.db.models.fields.DateTimeField', [], {}),
92 'registration_time': ('django.db.models.fields.DateTimeField', [], {}),
93 'user_id': ('django.db.models.fields.CharField', [], {'max_length': '50'})
93 'user_id': ('django.db.models.fields.CharField', [], {'max_length': '50'})
94 }
94 }
95 }
95 }
96
96
97 complete_apps = ['boards']
97 complete_apps = ['boards']
98 symmetrical = True
98 symmetrical = True
@@ -1,343 +1,343 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 re
4 import re
5
5
6 from django.core.cache import cache
6 from django.core.cache import cache
7 from django.core.urlresolvers import reverse
7 from django.core.urlresolvers import reverse
8 from django.db import models, transaction
8 from django.db import models, transaction
9 from django.template.loader import render_to_string
9 from django.template.loader import render_to_string
10 from django.utils import timezone
10 from django.utils import timezone
11 from markupfield.fields import MarkupField
11 from markupfield.fields import MarkupField
12
12
13 from boards.models import PostImage
13 from boards.models import PostImage
14 from boards.models.base import Viewable
14 from boards.models.base import Viewable
15 from boards.models.thread import Thread
15 from boards.models.thread import Thread
16
16
17
17
18 APP_LABEL_BOARDS = 'boards'
18 APP_LABEL_BOARDS = 'boards'
19
19
20 CACHE_KEY_PPD = 'ppd'
20 CACHE_KEY_PPD = 'ppd'
21 CACHE_KEY_POST_URL = 'post_url'
21 CACHE_KEY_POST_URL = 'post_url'
22
22
23 POSTS_PER_DAY_RANGE = 7
23 POSTS_PER_DAY_RANGE = 7
24
24
25 BAN_REASON_AUTO = 'Auto'
25 BAN_REASON_AUTO = 'Auto'
26
26
27 IMAGE_THUMB_SIZE = (200, 150)
27 IMAGE_THUMB_SIZE = (200, 150)
28
28
29 TITLE_MAX_LENGTH = 200
29 TITLE_MAX_LENGTH = 200
30
30
31 DEFAULT_MARKUP_TYPE = 'bbcode'
31 DEFAULT_MARKUP_TYPE = 'bbcode'
32
32
33 # TODO This should be removed
33 # TODO This should be removed
34 NO_IP = '0.0.0.0'
34 NO_IP = '0.0.0.0'
35
35
36 # TODO Real user agent should be saved instead of this
36 # TODO Real user agent should be saved instead of this
37 UNKNOWN_UA = ''
37 UNKNOWN_UA = ''
38
38
39 REGEX_REPLY = re.compile(ur'\[post\](\d+)\[/post\]')
39 REGEX_REPLY = re.compile(r'\[post\](\d+)\[/post\]')
40
40
41 logger = logging.getLogger(__name__)
41 logger = logging.getLogger(__name__)
42
42
43
43
44 class PostManager(models.Manager):
44 class PostManager(models.Manager):
45 def create_post(self, title, text, image=None, thread=None, ip=NO_IP,
45 def create_post(self, title, text, image=None, thread=None, ip=NO_IP,
46 tags=None):
46 tags=None):
47 """
47 """
48 Creates new post
48 Creates new post
49 """
49 """
50
50
51 posting_time = timezone.now()
51 posting_time = timezone.now()
52 if not thread:
52 if not thread:
53 thread = Thread.objects.create(bump_time=posting_time,
53 thread = Thread.objects.create(bump_time=posting_time,
54 last_edit_time=posting_time)
54 last_edit_time=posting_time)
55 new_thread = True
55 new_thread = True
56 else:
56 else:
57 thread.bump()
57 thread.bump()
58 thread.last_edit_time = posting_time
58 thread.last_edit_time = posting_time
59 thread.save()
59 thread.save()
60 new_thread = False
60 new_thread = False
61
61
62 post = self.create(title=title,
62 post = self.create(title=title,
63 text=text,
63 text=text,
64 pub_time=posting_time,
64 pub_time=posting_time,
65 thread_new=thread,
65 thread_new=thread,
66 poster_ip=ip,
66 poster_ip=ip,
67 poster_user_agent=UNKNOWN_UA, # TODO Get UA at
67 poster_user_agent=UNKNOWN_UA, # TODO Get UA at
68 # last!
68 # last!
69 last_edit_time=posting_time)
69 last_edit_time=posting_time)
70
70
71 if image:
71 if image:
72 post_image = PostImage.objects.create(image=image)
72 post_image = PostImage.objects.create(image=image)
73 post.images.add(post_image)
73 post.images.add(post_image)
74 logger.info('Created image #%d for post #%d' % (post_image.id,
74 logger.info('Created image #%d for post #%d' % (post_image.id,
75 post.id))
75 post.id))
76
76
77 thread.replies.add(post)
77 thread.replies.add(post)
78 if tags:
78 if tags:
79 map(thread.add_tag, tags)
79 map(thread.add_tag, tags)
80
80
81 if new_thread:
81 if new_thread:
82 Thread.objects.process_oldest_threads()
82 Thread.objects.process_oldest_threads()
83 self.connect_replies(post)
83 self.connect_replies(post)
84
84
85 logger.info('Created post #%d with title %s' % (post.id,
85 logger.info('Created post #%d with title %s' % (post.id,
86 post.get_title()))
86 post.get_title()))
87
87
88 return post
88 return post
89
89
90 def delete_post(self, post):
90 def delete_post(self, post):
91 """
91 """
92 Deletes post and update or delete its thread
92 Deletes post and update or delete its thread
93 """
93 """
94
94
95 post_id = post.id
95 post_id = post.id
96
96
97 thread = post.get_thread()
97 thread = post.get_thread()
98
98
99 if post.is_opening():
99 if post.is_opening():
100 thread.delete()
100 thread.delete()
101 else:
101 else:
102 thread.last_edit_time = timezone.now()
102 thread.last_edit_time = timezone.now()
103 thread.save()
103 thread.save()
104
104
105 post.delete()
105 post.delete()
106
106
107 logger.info('Deleted post #%d (%s)' % (post_id, post.get_title()))
107 logger.info('Deleted post #%d (%s)' % (post_id, post.get_title()))
108
108
109 def delete_posts_by_ip(self, ip):
109 def delete_posts_by_ip(self, ip):
110 """
110 """
111 Deletes all posts of the author with same IP
111 Deletes all posts of the author with same IP
112 """
112 """
113
113
114 posts = self.filter(poster_ip=ip)
114 posts = self.filter(poster_ip=ip)
115 map(self.delete_post, posts)
115 map(self.delete_post, posts)
116
116
117 def connect_replies(self, post):
117 def connect_replies(self, post):
118 """
118 """
119 Connects replies to a post to show them as a reflink map
119 Connects replies to a post to show them as a reflink map
120 """
120 """
121
121
122 for reply_number in re.finditer(REGEX_REPLY, post.text.raw):
122 for reply_number in re.finditer(REGEX_REPLY, post.text.raw):
123 post_id = reply_number.group(1)
123 post_id = reply_number.group(1)
124 ref_post = self.filter(id=post_id)
124 ref_post = self.filter(id=post_id)
125 if ref_post.count() > 0:
125 if ref_post.count() > 0:
126 referenced_post = ref_post[0]
126 referenced_post = ref_post[0]
127 referenced_post.referenced_posts.add(post)
127 referenced_post.referenced_posts.add(post)
128 referenced_post.last_edit_time = post.pub_time
128 referenced_post.last_edit_time = post.pub_time
129 referenced_post.build_refmap()
129 referenced_post.build_refmap()
130 referenced_post.save(update_fields=['refmap', 'last_edit_time'])
130 referenced_post.save(update_fields=['refmap', 'last_edit_time'])
131
131
132 referenced_thread = referenced_post.get_thread()
132 referenced_thread = referenced_post.get_thread()
133 referenced_thread.last_edit_time = post.pub_time
133 referenced_thread.last_edit_time = post.pub_time
134 referenced_thread.save(update_fields=['last_edit_time'])
134 referenced_thread.save(update_fields=['last_edit_time'])
135
135
136 def get_posts_per_day(self):
136 def get_posts_per_day(self):
137 """
137 """
138 Gets average count of posts per day for the last 7 days
138 Gets average count of posts per day for the last 7 days
139 """
139 """
140
140
141 day_end = date.today()
141 day_end = date.today()
142 day_start = day_end - timedelta(POSTS_PER_DAY_RANGE)
142 day_start = day_end - timedelta(POSTS_PER_DAY_RANGE)
143
143
144 cache_key = CACHE_KEY_PPD + str(day_end)
144 cache_key = CACHE_KEY_PPD + str(day_end)
145 ppd = cache.get(cache_key)
145 ppd = cache.get(cache_key)
146 if ppd:
146 if ppd:
147 return ppd
147 return ppd
148
148
149 day_time_start = timezone.make_aware(datetime.combine(
149 day_time_start = timezone.make_aware(datetime.combine(
150 day_start, dtime()), timezone.get_current_timezone())
150 day_start, dtime()), timezone.get_current_timezone())
151 day_time_end = timezone.make_aware(datetime.combine(
151 day_time_end = timezone.make_aware(datetime.combine(
152 day_end, dtime()), timezone.get_current_timezone())
152 day_end, dtime()), timezone.get_current_timezone())
153
153
154 posts_per_period = float(self.filter(
154 posts_per_period = float(self.filter(
155 pub_time__lte=day_time_end,
155 pub_time__lte=day_time_end,
156 pub_time__gte=day_time_start).count())
156 pub_time__gte=day_time_start).count())
157
157
158 ppd = posts_per_period / POSTS_PER_DAY_RANGE
158 ppd = posts_per_period / POSTS_PER_DAY_RANGE
159
159
160 cache.set(cache_key, ppd)
160 cache.set(cache_key, ppd)
161 return ppd
161 return ppd
162
162
163
163
164 class Post(models.Model, Viewable):
164 class Post(models.Model, Viewable):
165 """A post is a message."""
165 """A post is a message."""
166
166
167 objects = PostManager()
167 objects = PostManager()
168
168
169 class Meta:
169 class Meta:
170 app_label = APP_LABEL_BOARDS
170 app_label = APP_LABEL_BOARDS
171 ordering = ('id',)
171 ordering = ('id',)
172
172
173 title = models.CharField(max_length=TITLE_MAX_LENGTH)
173 title = models.CharField(max_length=TITLE_MAX_LENGTH)
174 pub_time = models.DateTimeField()
174 pub_time = models.DateTimeField()
175 text = MarkupField(default_markup_type=DEFAULT_MARKUP_TYPE,
175 text = MarkupField(default_markup_type=DEFAULT_MARKUP_TYPE,
176 escape_html=False)
176 escape_html=False)
177
177
178 images = models.ManyToManyField(PostImage, null=True, blank=True,
178 images = models.ManyToManyField(PostImage, null=True, blank=True,
179 related_name='ip+', db_index=True)
179 related_name='ip+', db_index=True)
180
180
181 poster_ip = models.GenericIPAddressField()
181 poster_ip = models.GenericIPAddressField()
182 poster_user_agent = models.TextField()
182 poster_user_agent = models.TextField()
183
183
184 thread_new = models.ForeignKey('Thread', null=True, default=None,
184 thread_new = models.ForeignKey('Thread', null=True, default=None,
185 db_index=True)
185 db_index=True)
186 last_edit_time = models.DateTimeField()
186 last_edit_time = models.DateTimeField()
187
187
188 referenced_posts = models.ManyToManyField('Post', symmetrical=False,
188 referenced_posts = models.ManyToManyField('Post', symmetrical=False,
189 null=True,
189 null=True,
190 blank=True, related_name='rfp+',
190 blank=True, related_name='rfp+',
191 db_index=True)
191 db_index=True)
192 refmap = models.TextField(null=True, blank=True)
192 refmap = models.TextField(null=True, blank=True)
193
193
194 def __unicode__(self):
194 def __unicode__(self):
195 return '#' + str(self.id) + ' ' + self.title + ' (' + \
195 return '#' + str(self.id) + ' ' + self.title + ' (' + \
196 self.text.raw[:50] + ')'
196 self.text.raw[:50] + ')'
197
197
198 def get_title(self):
198 def get_title(self):
199 """
199 """
200 Gets original post title or part of its text.
200 Gets original post title or part of its text.
201 """
201 """
202
202
203 title = self.title
203 title = self.title
204 if not title:
204 if not title:
205 title = self.text.rendered
205 title = self.text.rendered
206
206
207 return title
207 return title
208
208
209 def build_refmap(self):
209 def build_refmap(self):
210 """
210 """
211 Builds a replies map string from replies list. This is a cache to stop
211 Builds a replies map string from replies list. This is a cache to stop
212 the server from recalculating the map on every post show.
212 the server from recalculating the map on every post show.
213 """
213 """
214 map_string = ''
214 map_string = ''
215
215
216 first = True
216 first = True
217 for refpost in self.referenced_posts.all():
217 for refpost in self.referenced_posts.all():
218 if not first:
218 if not first:
219 map_string += ', '
219 map_string += ', '
220 map_string += '<a href="%s">&gt;&gt;%s</a>' % (refpost.get_url(),
220 map_string += '<a href="%s">&gt;&gt;%s</a>' % (refpost.get_url(),
221 refpost.id)
221 refpost.id)
222 first = False
222 first = False
223
223
224 self.refmap = map_string
224 self.refmap = map_string
225
225
226 def get_sorted_referenced_posts(self):
226 def get_sorted_referenced_posts(self):
227 return self.refmap
227 return self.refmap
228
228
229 def is_referenced(self):
229 def is_referenced(self):
230 return len(self.refmap) > 0
230 return len(self.refmap) > 0
231
231
232 def is_opening(self):
232 def is_opening(self):
233 """
233 """
234 Checks if this is an opening post or just a reply.
234 Checks if this is an opening post or just a reply.
235 """
235 """
236
236
237 return self.get_thread().get_opening_post_id() == self.id
237 return self.get_thread().get_opening_post_id() == self.id
238
238
239 @transaction.atomic
239 @transaction.atomic
240 def add_tag(self, tag):
240 def add_tag(self, tag):
241 edit_time = timezone.now()
241 edit_time = timezone.now()
242
242
243 thread = self.get_thread()
243 thread = self.get_thread()
244 thread.add_tag(tag)
244 thread.add_tag(tag)
245 self.last_edit_time = edit_time
245 self.last_edit_time = edit_time
246 self.save(update_fields=['last_edit_time'])
246 self.save(update_fields=['last_edit_time'])
247
247
248 thread.last_edit_time = edit_time
248 thread.last_edit_time = edit_time
249 thread.save(update_fields=['last_edit_time'])
249 thread.save(update_fields=['last_edit_time'])
250
250
251 @transaction.atomic
251 @transaction.atomic
252 def remove_tag(self, tag):
252 def remove_tag(self, tag):
253 edit_time = timezone.now()
253 edit_time = timezone.now()
254
254
255 thread = self.get_thread()
255 thread = self.get_thread()
256 thread.remove_tag(tag)
256 thread.remove_tag(tag)
257 self.last_edit_time = edit_time
257 self.last_edit_time = edit_time
258 self.save(update_fields=['last_edit_time'])
258 self.save(update_fields=['last_edit_time'])
259
259
260 thread.last_edit_time = edit_time
260 thread.last_edit_time = edit_time
261 thread.save(update_fields=['last_edit_time'])
261 thread.save(update_fields=['last_edit_time'])
262
262
263 def get_url(self, thread=None):
263 def get_url(self, thread=None):
264 """
264 """
265 Gets full url to the post.
265 Gets full url to the post.
266 """
266 """
267
267
268 cache_key = CACHE_KEY_POST_URL + str(self.id)
268 cache_key = CACHE_KEY_POST_URL + str(self.id)
269 link = cache.get(cache_key)
269 link = cache.get(cache_key)
270
270
271 if not link:
271 if not link:
272 if not thread:
272 if not thread:
273 thread = self.get_thread()
273 thread = self.get_thread()
274
274
275 opening_id = thread.get_opening_post_id()
275 opening_id = thread.get_opening_post_id()
276
276
277 if self.id != opening_id:
277 if self.id != opening_id:
278 link = reverse('thread', kwargs={
278 link = reverse('thread', kwargs={
279 'post_id': opening_id}) + '#' + str(self.id)
279 'post_id': opening_id}) + '#' + str(self.id)
280 else:
280 else:
281 link = reverse('thread', kwargs={'post_id': self.id})
281 link = reverse('thread', kwargs={'post_id': self.id})
282
282
283 cache.set(cache_key, link)
283 cache.set(cache_key, link)
284
284
285 return link
285 return link
286
286
287 def get_thread(self):
287 def get_thread(self):
288 """
288 """
289 Gets post's thread.
289 Gets post's thread.
290 """
290 """
291
291
292 return self.thread_new
292 return self.thread_new
293
293
294 def get_referenced_posts(self):
294 def get_referenced_posts(self):
295 return self.referenced_posts.only('id', 'thread_new')
295 return self.referenced_posts.only('id', 'thread_new')
296
296
297 def get_text(self):
297 def get_text(self):
298 return self.text
298 return self.text
299
299
300 def get_view(self, moderator=False, need_open_link=False,
300 def get_view(self, moderator=False, need_open_link=False,
301 truncated=False, *args, **kwargs):
301 truncated=False, *args, **kwargs):
302 if 'is_opening' in kwargs:
302 if 'is_opening' in kwargs:
303 is_opening = kwargs['is_opening']
303 is_opening = kwargs['is_opening']
304 else:
304 else:
305 is_opening = self.is_opening()
305 is_opening = self.is_opening()
306
306
307 if 'thread' in kwargs:
307 if 'thread' in kwargs:
308 thread = kwargs['thread']
308 thread = kwargs['thread']
309 else:
309 else:
310 thread = self.get_thread()
310 thread = self.get_thread()
311
311
312 if 'can_bump' in kwargs:
312 if 'can_bump' in kwargs:
313 can_bump = kwargs['can_bump']
313 can_bump = kwargs['can_bump']
314 else:
314 else:
315 can_bump = thread.can_bump()
315 can_bump = thread.can_bump()
316
316
317 if is_opening:
317 if is_opening:
318 opening_post_id = self.id
318 opening_post_id = self.id
319 else:
319 else:
320 opening_post_id = thread.get_opening_post_id()
320 opening_post_id = thread.get_opening_post_id()
321
321
322 return render_to_string('boards/post.html', {
322 return render_to_string('boards/post.html', {
323 'post': self,
323 'post': self,
324 'moderator': moderator,
324 'moderator': moderator,
325 'is_opening': is_opening,
325 'is_opening': is_opening,
326 'thread': thread,
326 'thread': thread,
327 'bumpable': can_bump,
327 'bumpable': can_bump,
328 'need_open_link': need_open_link,
328 'need_open_link': need_open_link,
329 'truncated': truncated,
329 'truncated': truncated,
330 'opening_post_id': opening_post_id,
330 'opening_post_id': opening_post_id,
331 })
331 })
332
332
333 def get_first_image(self):
333 def get_first_image(self):
334 return self.images.earliest('id')
334 return self.images.earliest('id')
335
335
336 def delete(self, using=None):
336 def delete(self, using=None):
337 """
337 """
338 Deletes all post images and the post itself.
338 Deletes all post images and the post itself.
339 """
339 """
340
340
341 self.images.all().delete()
341 self.images.all().delete()
342
342
343 super(Post, self).delete(using)
343 super(Post, self).delete(using)
@@ -1,270 +1,270 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 from boards.abstracts.settingsmanager import get_settings_manager
9 from boards.abstracts.settingsmanager import get_settings_manager
10
10
11 from boards.models import Post, Tag, Thread
11 from boards.models import Post, Tag, Thread
12 from boards import urls
12 from boards import urls
13 from boards import settings
13 from boards import settings
14 import neboard
14 import neboard
15
15
16 TEST_TAG = 'test_tag'
16 TEST_TAG = 'test_tag'
17
17
18 PAGE_404 = 'boards/404.html'
18 PAGE_404 = 'boards/404.html'
19
19
20 TEST_TEXT = 'test text'
20 TEST_TEXT = 'test text'
21
21
22 NEW_THREAD_PAGE = '/'
22 NEW_THREAD_PAGE = '/'
23 THREAD_PAGE_ONE = '/thread/1/'
23 THREAD_PAGE_ONE = '/thread/1/'
24 THREAD_PAGE = '/thread/'
24 THREAD_PAGE = '/thread/'
25 TAG_PAGE = '/tag/'
25 TAG_PAGE = '/tag/'
26 HTTP_CODE_REDIRECT = 302
26 HTTP_CODE_REDIRECT = 302
27 HTTP_CODE_OK = 200
27 HTTP_CODE_OK = 200
28 HTTP_CODE_NOT_FOUND = 404
28 HTTP_CODE_NOT_FOUND = 404
29
29
30 logger = logging.getLogger(__name__)
30 logger = logging.getLogger(__name__)
31
31
32
32
33 class PostTests(TestCase):
33 class PostTests(TestCase):
34
34
35 def _create_post(self):
35 def _create_post(self):
36 tag = Tag.objects.create(name=TEST_TAG)
36 tag = Tag.objects.create(name=TEST_TAG)
37 return Post.objects.create_post(title='title', text='text',
37 return Post.objects.create_post(title='title', text='text',
38 tags=[tag])
38 tags=[tag])
39
39
40 def test_post_add(self):
40 def test_post_add(self):
41 """Test adding post"""
41 """Test adding post"""
42
42
43 post = self._create_post()
43 post = self._create_post()
44
44
45 self.assertIsNotNone(post, 'No post was created.')
45 self.assertIsNotNone(post, 'No post was created.')
46 self.assertEqual(TEST_TAG, post.get_thread().tags.all()[0].name,
46 self.assertEqual(TEST_TAG, post.get_thread().tags.all()[0].name,
47 'No tags were added to the post.')
47 'No tags were added to the post.')
48
48
49 def test_delete_post(self):
49 def test_delete_post(self):
50 """Test post deletion"""
50 """Test post deletion"""
51
51
52 post = self._create_post()
52 post = self._create_post()
53 post_id = post.id
53 post_id = post.id
54
54
55 Post.objects.delete_post(post)
55 Post.objects.delete_post(post)
56
56
57 self.assertFalse(Post.objects.filter(id=post_id).exists())
57 self.assertFalse(Post.objects.filter(id=post_id).exists())
58
58
59 def test_delete_thread(self):
59 def test_delete_thread(self):
60 """Test thread deletion"""
60 """Test thread deletion"""
61
61
62 opening_post = self._create_post()
62 opening_post = self._create_post()
63 thread = opening_post.get_thread()
63 thread = opening_post.get_thread()
64 reply = Post.objects.create_post("", "", thread=thread)
64 reply = Post.objects.create_post("", "", thread=thread)
65
65
66 thread.delete()
66 thread.delete()
67
67
68 self.assertFalse(Post.objects.filter(id=reply.id).exists())
68 self.assertFalse(Post.objects.filter(id=reply.id).exists())
69
69
70 def test_post_to_thread(self):
70 def test_post_to_thread(self):
71 """Test adding post to a thread"""
71 """Test adding post to a thread"""
72
72
73 op = self._create_post()
73 op = self._create_post()
74 post = Post.objects.create_post("", "", thread=op.get_thread())
74 post = Post.objects.create_post("", "", thread=op.get_thread())
75
75
76 self.assertIsNotNone(post, 'Reply to thread wasn\'t created')
76 self.assertIsNotNone(post, 'Reply to thread wasn\'t created')
77 self.assertEqual(op.get_thread().last_edit_time, post.pub_time,
77 self.assertEqual(op.get_thread().last_edit_time, post.pub_time,
78 'Post\'s create time doesn\'t match thread last edit'
78 'Post\'s create time doesn\'t match thread last edit'
79 ' time')
79 ' time')
80
80
81 def test_delete_posts_by_ip(self):
81 def test_delete_posts_by_ip(self):
82 """Test deleting posts with the given ip"""
82 """Test deleting posts with the given ip"""
83
83
84 post = self._create_post()
84 post = self._create_post()
85 post_id = post.id
85 post_id = post.id
86
86
87 Post.objects.delete_posts_by_ip('0.0.0.0')
87 Post.objects.delete_posts_by_ip('0.0.0.0')
88
88
89 self.assertFalse(Post.objects.filter(id=post_id).exists())
89 self.assertFalse(Post.objects.filter(id=post_id).exists())
90
90
91 def test_get_thread(self):
91 def test_get_thread(self):
92 """Test getting all posts of a thread"""
92 """Test getting all posts of a thread"""
93
93
94 opening_post = self._create_post()
94 opening_post = self._create_post()
95
95
96 for i in range(0, 2):
96 for i in range(0, 2):
97 Post.objects.create_post('title', 'text',
97 Post.objects.create_post('title', 'text',
98 thread=opening_post.get_thread())
98 thread=opening_post.get_thread())
99
99
100 thread = opening_post.get_thread()
100 thread = opening_post.get_thread()
101
101
102 self.assertEqual(3, thread.replies.count())
102 self.assertEqual(3, thread.replies.count())
103
103
104 def test_create_post_with_tag(self):
104 def test_create_post_with_tag(self):
105 """Test adding tag to post"""
105 """Test adding tag to post"""
106
106
107 tag = Tag.objects.create(name='test_tag')
107 tag = Tag.objects.create(name='test_tag')
108 post = Post.objects.create_post(title='title', text='text', tags=[tag])
108 post = Post.objects.create_post(title='title', text='text', tags=[tag])
109
109
110 thread = post.get_thread()
110 thread = post.get_thread()
111 self.assertIsNotNone(post, 'Post not created')
111 self.assertIsNotNone(post, 'Post not created')
112 self.assertTrue(tag in thread.tags.all(), 'Tag not added to thread')
112 self.assertTrue(tag in thread.tags.all(), 'Tag not added to thread')
113 self.assertTrue(thread in tag.threads.all(), 'Thread not added to tag')
113 self.assertTrue(thread in tag.threads.all(), 'Thread not added to tag')
114
114
115 def test_thread_max_count(self):
115 def test_thread_max_count(self):
116 """Test deletion of old posts when the max thread count is reached"""
116 """Test deletion of old posts when the max thread count is reached"""
117
117
118 for i in range(settings.MAX_THREAD_COUNT + 1):
118 for i in range(settings.MAX_THREAD_COUNT + 1):
119 self._create_post()
119 self._create_post()
120
120
121 self.assertEqual(settings.MAX_THREAD_COUNT,
121 self.assertEqual(settings.MAX_THREAD_COUNT,
122 len(Thread.objects.filter(archived=False)))
122 len(Thread.objects.filter(archived=False)))
123
123
124 def test_pages(self):
124 def test_pages(self):
125 """Test that the thread list is properly split into pages"""
125 """Test that the thread list is properly split into pages"""
126
126
127 for i in range(settings.MAX_THREAD_COUNT):
127 for i in range(settings.MAX_THREAD_COUNT):
128 self._create_post()
128 self._create_post()
129
129
130 all_threads = Thread.objects.filter(archived=False)
130 all_threads = Thread.objects.filter(archived=False)
131
131
132 paginator = Paginator(Thread.objects.filter(archived=False),
132 paginator = Paginator(Thread.objects.filter(archived=False),
133 settings.THREADS_PER_PAGE)
133 settings.THREADS_PER_PAGE)
134 posts_in_second_page = paginator.page(2).object_list
134 posts_in_second_page = paginator.page(2).object_list
135 first_post = posts_in_second_page[0]
135 first_post = posts_in_second_page[0]
136
136
137 self.assertEqual(all_threads[settings.THREADS_PER_PAGE].id,
137 self.assertEqual(all_threads[settings.THREADS_PER_PAGE].id,
138 first_post.id)
138 first_post.id)
139
139
140
140
141 class PagesTest(TestCase):
141 class PagesTest(TestCase):
142
142
143 def test_404(self):
143 def test_404(self):
144 """Test receiving error 404 when opening a non-existent page"""
144 """Test receiving error 404 when opening a non-existent page"""
145
145
146 tag_name = u'test_tag'
146 tag_name = u'test_tag'
147 tag = Tag.objects.create(name=tag_name)
147 tag = Tag.objects.create(name=tag_name)
148 client = Client()
148 client = Client()
149
149
150 Post.objects.create_post('title', TEST_TEXT, tags=[tag])
150 Post.objects.create_post('title', TEST_TEXT, tags=[tag])
151
151
152 existing_post_id = Post.objects.all()[0].id
152 existing_post_id = Post.objects.all()[0].id
153 response_existing = client.get(THREAD_PAGE + str(existing_post_id) +
153 response_existing = client.get(THREAD_PAGE + str(existing_post_id) +
154 '/')
154 '/')
155 self.assertEqual(HTTP_CODE_OK, response_existing.status_code,
155 self.assertEqual(HTTP_CODE_OK, response_existing.status_code,
156 u'Cannot open existing thread')
156 u'Cannot open existing thread')
157
157
158 response_not_existing = client.get(THREAD_PAGE + str(
158 response_not_existing = client.get(THREAD_PAGE + str(
159 existing_post_id + 1) + '/')
159 existing_post_id + 1) + '/')
160 self.assertEqual(PAGE_404, response_not_existing.templates[0].name,
160 self.assertEqual(PAGE_404, response_not_existing.templates[0].name,
161 u'Not existing thread is opened')
161 u'Not existing thread is opened')
162
162
163 response_existing = client.get(TAG_PAGE + tag_name + '/')
163 response_existing = client.get(TAG_PAGE + tag_name + '/')
164 self.assertEqual(HTTP_CODE_OK,
164 self.assertEqual(HTTP_CODE_OK,
165 response_existing.status_code,
165 response_existing.status_code,
166 u'Cannot open existing tag')
166 u'Cannot open existing tag')
167
167
168 response_not_existing = client.get(TAG_PAGE + u'not_tag' + '/')
168 response_not_existing = client.get(TAG_PAGE + u'not_tag' + '/')
169 self.assertEqual(PAGE_404,
169 self.assertEqual(PAGE_404,
170 response_not_existing.templates[0].name,
170 response_not_existing.templates[0].name,
171 u'Not existing tag is opened')
171 u'Not existing tag is opened')
172
172
173 reply_id = Post.objects.create_post('', TEST_TEXT,
173 reply_id = Post.objects.create_post('', TEST_TEXT,
174 thread=Post.objects.all()[0]
174 thread=Post.objects.all()[0]
175 .get_thread())
175 .get_thread())
176 response_not_existing = client.get(THREAD_PAGE + str(
176 response_not_existing = client.get(THREAD_PAGE + str(
177 reply_id) + '/')
177 reply_id) + '/')
178 self.assertEqual(PAGE_404,
178 self.assertEqual(PAGE_404,
179 response_not_existing.templates[0].name,
179 response_not_existing.templates[0].name,
180 u'Reply is opened as a thread')
180 u'Reply is opened as a thread')
181
181
182
182
183 class FormTest(TestCase):
183 class FormTest(TestCase):
184 def test_post_validation(self):
184 def test_post_validation(self):
185 # Disable captcha for the test
185 # Disable captcha for the test
186 captcha_enabled = neboard.settings.ENABLE_CAPTCHA
186 captcha_enabled = neboard.settings.ENABLE_CAPTCHA
187 neboard.settings.ENABLE_CAPTCHA = False
187 neboard.settings.ENABLE_CAPTCHA = False
188
188
189 client = Client()
189 client = Client()
190
190
191 valid_tags = u'tag1 tag_2 Ρ‚Π΅Π³_3'
191 valid_tags = u'tag1 tag_2 Ρ‚Π΅Π³_3'
192 invalid_tags = u'$%_356 ---'
192 invalid_tags = u'$%_356 ---'
193
193
194 response = client.post(NEW_THREAD_PAGE, {'title': 'test title',
194 response = client.post(NEW_THREAD_PAGE, {'title': 'test title',
195 'text': TEST_TEXT,
195 'text': TEST_TEXT,
196 'tags': valid_tags})
196 'tags': valid_tags})
197 self.assertEqual(response.status_code, HTTP_CODE_REDIRECT,
197 self.assertEqual(response.status_code, HTTP_CODE_REDIRECT,
198 msg='Posting new message failed: got code ' +
198 msg='Posting new message failed: got code ' +
199 str(response.status_code))
199 str(response.status_code))
200
200
201 self.assertEqual(1, Post.objects.count(),
201 self.assertEqual(1, Post.objects.count(),
202 msg='No posts were created')
202 msg='No posts were created')
203
203
204 client.post(NEW_THREAD_PAGE, {'text': TEST_TEXT,
204 client.post(NEW_THREAD_PAGE, {'text': TEST_TEXT,
205 'tags': invalid_tags})
205 'tags': invalid_tags})
206 self.assertEqual(1, Post.objects.count(), msg='The validation passed '
206 self.assertEqual(1, Post.objects.count(), msg='The validation passed '
207 'where it should fail')
207 'where it should fail')
208
208
209 # Change posting delay so we don't have to wait for 30 seconds or more
209 # Change posting delay so we don't have to wait for 30 seconds or more
210 old_posting_delay = neboard.settings.POSTING_DELAY
210 old_posting_delay = neboard.settings.POSTING_DELAY
211 # Wait fot the posting delay or we won't be able to post
211 # Wait fot the posting delay or we won't be able to post
212 settings.POSTING_DELAY = 1
212 settings.POSTING_DELAY = 1
213 time.sleep(neboard.settings.POSTING_DELAY + 1)
213 time.sleep(neboard.settings.POSTING_DELAY + 1)
214 response = client.post(THREAD_PAGE_ONE, {'text': TEST_TEXT,
214 response = client.post(THREAD_PAGE_ONE, {'text': TEST_TEXT,
215 'tags': valid_tags})
215 'tags': valid_tags})
216 self.assertEqual(HTTP_CODE_REDIRECT, response.status_code,
216 self.assertEqual(HTTP_CODE_REDIRECT, response.status_code,
217 msg=u'Posting new message failed: got code ' +
217 msg=u'Posting new message failed: got code ' +
218 str(response.status_code))
218 str(response.status_code))
219 # Restore posting delay
219 # Restore posting delay
220 settings.POSTING_DELAY = old_posting_delay
220 settings.POSTING_DELAY = old_posting_delay
221
221
222 self.assertEqual(2, Post.objects.count(),
222 self.assertEqual(2, Post.objects.count(),
223 msg=u'No posts were created')
223 msg=u'No posts were created')
224
224
225 # Restore captcha setting
225 # Restore captcha setting
226 settings.ENABLE_CAPTCHA = captcha_enabled
226 settings.ENABLE_CAPTCHA = captcha_enabled
227
227
228
228
229 class ViewTest(TestCase):
229 class ViewTest(TestCase):
230
230
231 def test_all_views(self):
231 def test_all_views(self):
232 """
232 """
233 Try opening all views defined in ulrs.py that don't need additional
233 Try opening all views defined in ulrs.py that don't need additional
234 parameters
234 parameters
235 """
235 """
236
236
237 client = Client()
237 client = Client()
238 for url in urls.urlpatterns:
238 for url in urls.urlpatterns:
239 try:
239 try:
240 view_name = url.name
240 view_name = url.name
241 logger.debug('Testing view %s' % view_name)
241 logger.debug('Testing view %s' % view_name)
242
242
243 try:
243 try:
244 response = client.get(reverse(view_name))
244 response = client.get(reverse(view_name))
245
245
246 self.assertEqual(HTTP_CODE_OK, response.status_code,
246 self.assertEqual(HTTP_CODE_OK, response.status_code,
247 '%s view not opened' % view_name)
247 '%s view not opened' % view_name)
248 except NoReverseMatch:
248 except NoReverseMatch:
249 # This view just needs additional arguments
249 # This view just needs additional arguments
250 pass
250 pass
251 except Exception, e:
251 except Exception as e:
252 self.fail('Got exception %s at %s view' % (e, view_name))
252 self.fail('Got exception %s at %s view' % (e, view_name))
253 except AttributeError:
253 except AttributeError:
254 # This is normal, some views do not have names
254 # This is normal, some views do not have names
255 pass
255 pass
256
256
257
257
258 class AbstractTest(TestCase):
258 class AbstractTest(TestCase):
259 def test_settings_manager(self):
259 def test_settings_manager(self):
260 request = MockRequest()
260 request = MockRequest()
261 settings_manager = get_settings_manager(request)
261 settings_manager = get_settings_manager(request)
262
262
263 settings_manager.set_setting('test_setting', 'test_value')
263 settings_manager.set_setting('test_setting', 'test_value')
264 self.assertEqual('test_value', settings_manager.get_setting(
264 self.assertEqual('test_value', settings_manager.get_setting(
265 'test_setting'), u'Setting update failed.')
265 'test_setting'), u'Setting update failed.')
266
266
267
267
268 class MockRequest:
268 class MockRequest:
269 def __init__(self):
269 def __init__(self):
270 self.session = dict()
270 self.session = dict()
@@ -1,219 +1,219 b''
1 # -*- encoding: utf-8 -*-
1 # -*- encoding: utf-8 -*-
2 """
2 """
3 django-thumbs by Antonio MelΓ©
3 django-thumbs by Antonio MelΓ©
4 http://django.es
4 http://django.es
5 """
5 """
6 from django.core.files.images import ImageFile
6 from django.core.files.images import ImageFile
7 from django.db.models import ImageField
7 from django.db.models import ImageField
8 from django.db.models.fields.files import ImageFieldFile
8 from django.db.models.fields.files import ImageFieldFile
9 from PIL import Image
9 from PIL import Image
10 from django.core.files.base import ContentFile
10 from django.core.files.base import ContentFile
11 import cStringIO
11 import io
12
12
13
13
14 def generate_thumb(img, thumb_size, format):
14 def generate_thumb(img, thumb_size, format):
15 """
15 """
16 Generates a thumbnail image and returns a ContentFile object with the thumbnail
16 Generates a thumbnail image and returns a ContentFile object with the thumbnail
17
17
18 Parameters:
18 Parameters:
19 ===========
19 ===========
20 img File object
20 img File object
21
21
22 thumb_size desired thumbnail size, ie: (200,120)
22 thumb_size desired thumbnail size, ie: (200,120)
23
23
24 format format of the original image ('jpeg','gif','png',...)
24 format format of the original image ('jpeg','gif','png',...)
25 (this format will be used for the generated thumbnail, too)
25 (this format will be used for the generated thumbnail, too)
26 """
26 """
27
27
28 img.seek(0) # see http://code.djangoproject.com/ticket/8222 for details
28 img.seek(0) # see http://code.djangoproject.com/ticket/8222 for details
29 image = Image.open(img)
29 image = Image.open(img)
30
30
31 # get size
31 # get size
32 thumb_w, thumb_h = thumb_size
32 thumb_w, thumb_h = thumb_size
33 # If you want to generate a square thumbnail
33 # If you want to generate a square thumbnail
34 if thumb_w == thumb_h:
34 if thumb_w == thumb_h:
35 # quad
35 # quad
36 xsize, ysize = image.size
36 xsize, ysize = image.size
37 # get minimum size
37 # get minimum size
38 minsize = min(xsize, ysize)
38 minsize = min(xsize, ysize)
39 # largest square possible in the image
39 # largest square possible in the image
40 xnewsize = (xsize - minsize) / 2
40 xnewsize = (xsize - minsize) / 2
41 ynewsize = (ysize - minsize) / 2
41 ynewsize = (ysize - minsize) / 2
42 # crop it
42 # crop it
43 image2 = image.crop(
43 image2 = image.crop(
44 (xnewsize, ynewsize, xsize - xnewsize, ysize - ynewsize))
44 (xnewsize, ynewsize, xsize - xnewsize, ysize - ynewsize))
45 # load is necessary after crop
45 # load is necessary after crop
46 image2.load()
46 image2.load()
47 # thumbnail of the cropped image (with ANTIALIAS to make it look better)
47 # thumbnail of the cropped image (with ANTIALIAS to make it look better)
48 image2.thumbnail(thumb_size, Image.ANTIALIAS)
48 image2.thumbnail(thumb_size, Image.ANTIALIAS)
49 else:
49 else:
50 # not quad
50 # not quad
51 image2 = image
51 image2 = image
52 image2.thumbnail(thumb_size, Image.ANTIALIAS)
52 image2.thumbnail(thumb_size, Image.ANTIALIAS)
53
53
54 io = cStringIO.StringIO()
54 string_io = io.StringIO()
55 # PNG and GIF are the same, JPG is JPEG
55 # PNG and GIF are the same, JPG is JPEG
56 if format.upper() == 'JPG':
56 if format.upper() == 'JPG':
57 format = 'JPEG'
57 format = 'JPEG'
58
58
59 image2.save(io, format)
59 image2.save(string_io, format)
60 return ContentFile(io.getvalue())
60 return ContentFile(string_io.getvalue())
61
61
62
62
63 class ImageWithThumbsFieldFile(ImageFieldFile):
63 class ImageWithThumbsFieldFile(ImageFieldFile):
64 """
64 """
65 See ImageWithThumbsField for usage example
65 See ImageWithThumbsField for usage example
66 """
66 """
67
67
68 def __init__(self, *args, **kwargs):
68 def __init__(self, *args, **kwargs):
69 super(ImageWithThumbsFieldFile, self).__init__(*args, **kwargs)
69 super(ImageWithThumbsFieldFile, self).__init__(*args, **kwargs)
70 self.sizes = self.field.sizes
70 self.sizes = self.field.sizes
71
71
72 if self.sizes:
72 if self.sizes:
73 def get_size(self, size):
73 def get_size(self, size):
74 if not self:
74 if not self:
75 return ''
75 return ''
76 else:
76 else:
77 split = self.url.rsplit('.', 1)
77 split = self.url.rsplit('.', 1)
78 thumb_url = '%s.%sx%s.%s' % (split[0], w, h, split[1])
78 thumb_url = '%s.%sx%s.%s' % (split[0], w, h, split[1])
79 return thumb_url
79 return thumb_url
80
80
81 for size in self.sizes:
81 for size in self.sizes:
82 (w, h) = size
82 (w, h) = size
83 setattr(self, 'url_%sx%s' % (w, h), get_size(self, size))
83 setattr(self, 'url_%sx%s' % (w, h), get_size(self, size))
84
84
85 def save(self, name, content, save=True):
85 def save(self, name, content, save=True):
86 super(ImageWithThumbsFieldFile, self).save(name, content, save)
86 super(ImageWithThumbsFieldFile, self).save(name, content, save)
87
87
88 if self.sizes:
88 if self.sizes:
89 for size in self.sizes:
89 for size in self.sizes:
90 (w, h) = size
90 (w, h) = size
91 split = self.name.rsplit('.', 1)
91 split = self.name.rsplit('.', 1)
92 thumb_name = '%s.%sx%s.%s' % (split[0], w, h, split[1])
92 thumb_name = '%s.%sx%s.%s' % (split[0], w, h, split[1])
93
93
94 # you can use another thumbnailing function if you like
94 # you can use another thumbnailing function if you like
95 thumb_content = generate_thumb(content, size, split[1])
95 thumb_content = generate_thumb(content, size, split[1])
96
96
97 thumb_name_ = self.storage.save(thumb_name, thumb_content)
97 thumb_name_ = self.storage.save(thumb_name, thumb_content)
98
98
99 if not thumb_name == thumb_name_:
99 if not thumb_name == thumb_name_:
100 raise ValueError(
100 raise ValueError(
101 'There is already a file named %s' % thumb_name)
101 'There is already a file named %s' % thumb_name)
102
102
103 def delete(self, save=True):
103 def delete(self, save=True):
104 name = self.name
104 name = self.name
105 super(ImageWithThumbsFieldFile, self).delete(save)
105 super(ImageWithThumbsFieldFile, self).delete(save)
106 if self.sizes:
106 if self.sizes:
107 for size in self.sizes:
107 for size in self.sizes:
108 (w, h) = size
108 (w, h) = size
109 split = name.rsplit('.', 1)
109 split = name.rsplit('.', 1)
110 thumb_name = '%s.%sx%s.%s' % (split[0], w, h, split[1])
110 thumb_name = '%s.%sx%s.%s' % (split[0], w, h, split[1])
111 try:
111 try:
112 self.storage.delete(thumb_name)
112 self.storage.delete(thumb_name)
113 except:
113 except:
114 pass
114 pass
115
115
116
116
117 class ImageWithThumbsField(ImageField):
117 class ImageWithThumbsField(ImageField):
118 attr_class = ImageWithThumbsFieldFile
118 attr_class = ImageWithThumbsFieldFile
119 """
119 """
120 Usage example:
120 Usage example:
121 ==============
121 ==============
122 photo = ImageWithThumbsField(upload_to='images', sizes=((125,125),(300,200),)
122 photo = ImageWithThumbsField(upload_to='images', sizes=((125,125),(300,200),)
123
123
124 To retrieve image URL, exactly the same way as with ImageField:
124 To retrieve image URL, exactly the same way as with ImageField:
125 my_object.photo.url
125 my_object.photo.url
126 To retrieve thumbnails URL's just add the size to it:
126 To retrieve thumbnails URL's just add the size to it:
127 my_object.photo.url_125x125
127 my_object.photo.url_125x125
128 my_object.photo.url_300x200
128 my_object.photo.url_300x200
129
129
130 Note: The 'sizes' attribute is not required. If you don't provide it,
130 Note: The 'sizes' attribute is not required. If you don't provide it,
131 ImageWithThumbsField will act as a normal ImageField
131 ImageWithThumbsField will act as a normal ImageField
132
132
133 How it works:
133 How it works:
134 =============
134 =============
135 For each size in the 'sizes' atribute of the field it generates a
135 For each size in the 'sizes' atribute of the field it generates a
136 thumbnail with that size and stores it following this format:
136 thumbnail with that size and stores it following this format:
137
137
138 available_filename.[width]x[height].extension
138 available_filename.[width]x[height].extension
139
139
140 Where 'available_filename' is the available filename returned by the storage
140 Where 'available_filename' is the available filename returned by the storage
141 backend for saving the original file.
141 backend for saving the original file.
142
142
143 Following the usage example above: For storing a file called "photo.jpg" it saves:
143 Following the usage example above: For storing a file called "photo.jpg" it saves:
144 photo.jpg (original file)
144 photo.jpg (original file)
145 photo.125x125.jpg (first thumbnail)
145 photo.125x125.jpg (first thumbnail)
146 photo.300x200.jpg (second thumbnail)
146 photo.300x200.jpg (second thumbnail)
147
147
148 With the default storage backend if photo.jpg already exists it will use these filenames:
148 With the default storage backend if photo.jpg already exists it will use these filenames:
149 photo_.jpg
149 photo_.jpg
150 photo_.125x125.jpg
150 photo_.125x125.jpg
151 photo_.300x200.jpg
151 photo_.300x200.jpg
152
152
153 Note: django-thumbs assumes that if filename "any_filename.jpg" is available
153 Note: django-thumbs assumes that if filename "any_filename.jpg" is available
154 filenames with this format "any_filename.[widht]x[height].jpg" will be available, too.
154 filenames with this format "any_filename.[widht]x[height].jpg" will be available, too.
155
155
156 To do:
156 To do:
157 ======
157 ======
158 Add method to regenerate thubmnails
158 Add method to regenerate thubmnails
159
159
160
160
161 """
161 """
162
162
163 preview_width_field = None
163 preview_width_field = None
164 preview_height_field = None
164 preview_height_field = None
165
165
166 def __init__(self, verbose_name=None, name=None, width_field=None,
166 def __init__(self, verbose_name=None, name=None, width_field=None,
167 height_field=None, sizes=None,
167 height_field=None, sizes=None,
168 preview_width_field=None, preview_height_field=None,
168 preview_width_field=None, preview_height_field=None,
169 **kwargs):
169 **kwargs):
170 self.verbose_name = verbose_name
170 self.verbose_name = verbose_name
171 self.name = name
171 self.name = name
172 self.width_field = width_field
172 self.width_field = width_field
173 self.height_field = height_field
173 self.height_field = height_field
174 self.sizes = sizes
174 self.sizes = sizes
175 super(ImageField, self).__init__(**kwargs)
175 super(ImageField, self).__init__(**kwargs)
176
176
177 if sizes is not None and len(sizes) == 1:
177 if sizes is not None and len(sizes) == 1:
178 self.preview_width_field = preview_width_field
178 self.preview_width_field = preview_width_field
179 self.preview_height_field = preview_height_field
179 self.preview_height_field = preview_height_field
180
180
181 def update_dimension_fields(self, instance, force=False, *args, **kwargs):
181 def update_dimension_fields(self, instance, force=False, *args, **kwargs):
182 """
182 """
183 Update original image dimension fields and thumb dimension fields
183 Update original image dimension fields and thumb dimension fields
184 (only if 1 thumb size is defined)
184 (only if 1 thumb size is defined)
185 """
185 """
186
186
187 super(ImageWithThumbsField, self).update_dimension_fields(instance,
187 super(ImageWithThumbsField, self).update_dimension_fields(instance,
188 force, *args,
188 force, *args,
189 **kwargs)
189 **kwargs)
190 thumb_width_field = self.preview_width_field
190 thumb_width_field = self.preview_width_field
191 thumb_height_field = self.preview_height_field
191 thumb_height_field = self.preview_height_field
192
192
193 if thumb_width_field is None or thumb_height_field is None \
193 if thumb_width_field is None or thumb_height_field is None \
194 or len(self.sizes) != 1:
194 or len(self.sizes) != 1:
195 return
195 return
196
196
197 original_width = getattr(instance, self.width_field)
197 original_width = getattr(instance, self.width_field)
198 original_height = getattr(instance, self.height_field)
198 original_height = getattr(instance, self.height_field)
199
199
200 if original_width > 0 and original_height > 0:
200 if original_width > 0 and original_height > 0:
201 thumb_width, thumb_height = self.sizes[0]
201 thumb_width, thumb_height = self.sizes[0]
202
202
203 w_scale = float(thumb_width) / original_width
203 w_scale = float(thumb_width) / original_width
204 h_scale = float(thumb_height) / original_height
204 h_scale = float(thumb_height) / original_height
205 scale_ratio = min(w_scale, h_scale)
205 scale_ratio = min(w_scale, h_scale)
206
206
207 if scale_ratio >= 1:
207 if scale_ratio >= 1:
208 thumb_width_ratio = original_width
208 thumb_width_ratio = original_width
209 thumb_height_ratio = original_height
209 thumb_height_ratio = original_height
210 else:
210 else:
211 thumb_width_ratio = int(original_width * scale_ratio)
211 thumb_width_ratio = int(original_width * scale_ratio)
212 thumb_height_ratio = int(original_height * scale_ratio)
212 thumb_height_ratio = int(original_height * scale_ratio)
213
213
214 setattr(instance, thumb_width_field, thumb_width_ratio)
214 setattr(instance, thumb_width_field, thumb_width_ratio)
215 setattr(instance, thumb_height_field, thumb_height_ratio)
215 setattr(instance, thumb_height_field, thumb_height_ratio)
216
216
217
217
218 from south.modelsinspector import add_introspection_rules
218 from south.modelsinspector import add_introspection_rules
219 add_introspection_rules([], ["^boards\.thumbs\.ImageWithThumbsField"])
219 add_introspection_rules([], ["^boards\.thumbs\.ImageWithThumbsField"])
@@ -1,84 +1,83 b''
1 from django.conf.urls import patterns, url, include
1 from django.conf.urls import patterns, url, include
2 from boards import views
2 from boards import views
3 from boards.rss import AllThreadsFeed, TagThreadsFeed, ThreadPostsFeed
3 from boards.rss import AllThreadsFeed, TagThreadsFeed, ThreadPostsFeed
4 from boards.views import api, tag_threads, all_threads, \
4 from boards.views import api, tag_threads, all_threads, \
5 login, settings, all_tags, logout
5 login, settings, all_tags, logout
6 from boards.views.authors import AuthorsView
6 from boards.views.authors import AuthorsView
7 from boards.views.delete_post import DeletePostView
7 from boards.views.delete_post import DeletePostView
8 from boards.views.ban import BanUserView
8 from boards.views.ban import BanUserView
9 from boards.views.search import BoardSearchView
9 from boards.views.search import BoardSearchView
10 from boards.views.static import StaticPageView
10 from boards.views.static import StaticPageView
11 from boards.views.post_admin import PostAdminView
11 from boards.views.post_admin import PostAdminView
12
12
13 js_info_dict = {
13 js_info_dict = {
14 'packages': ('boards',),
14 'packages': ('boards',),
15 }
15 }
16
16
17 urlpatterns = patterns('',
17 urlpatterns = patterns('',
18
18
19 # /boards/
19 # /boards/
20 url(r'^$', all_threads.AllThreadsView.as_view(), name='index'),
20 url(r'^$', all_threads.AllThreadsView.as_view(), name='index'),
21 # /boards/page/
21 # /boards/page/
22 url(r'^page/(?P<page>\w+)/$', all_threads.AllThreadsView.as_view(),
22 url(r'^page/(?P<page>\w+)/$', all_threads.AllThreadsView.as_view(),
23 name='index'),
23 name='index'),
24
24
25 # login page
25 # login page
26 url(r'^login/$', login.LoginView.as_view(), name='login'),
26 url(r'^login/$', login.LoginView.as_view(), name='login'),
27 url(r'^logout/$', logout.LogoutView.as_view(), name='logout'),
27 url(r'^logout/$', logout.LogoutView.as_view(), name='logout'),
28
28
29 # /boards/tag/tag_name/
29 # /boards/tag/tag_name/
30 url(r'^tag/(?P<tag_name>\w+)/$', tag_threads.TagView.as_view(),
30 url(r'^tag/(?P<tag_name>\w+)/$', tag_threads.TagView.as_view(),
31 name='tag'),
31 name='tag'),
32 # /boards/tag/tag_id/page/
32 # /boards/tag/tag_id/page/
33 url(r'^tag/(?P<tag_name>\w+)/page/(?P<page>\w+)/$',
33 url(r'^tag/(?P<tag_name>\w+)/page/(?P<page>\w+)/$',
34 tag_threads.TagView.as_view(), name='tag'),
34 tag_threads.TagView.as_view(), name='tag'),
35
35
36 # /boards/thread/
36 # /boards/thread/
37 url(r'^thread/(?P<post_id>\w+)/$', views.thread.ThreadView.as_view(),
37 url(r'^thread/(?P<post_id>\w+)/$', views.thread.ThreadView.as_view(),
38 name='thread'),
38 name='thread'),
39 url(r'^thread/(?P<post_id>\w+)/mode/(?P<mode>\w+)/$', views.thread.ThreadView
39 url(r'^thread/(?P<post_id>\w+)/mode/(?P<mode>\w+)/$', views.thread.ThreadView
40 .as_view(), name='thread_mode'),
40 .as_view(), name='thread_mode'),
41
41
42 # /boards/post_admin/
42 # /boards/post_admin/
43 url(r'^post_admin/(?P<post_id>\w+)/$', PostAdminView.as_view(),
43 url(r'^post_admin/(?P<post_id>\w+)/$', PostAdminView.as_view(),
44 name='post_admin'),
44 name='post_admin'),
45
45
46 url(r'^settings/$', settings.SettingsView.as_view(), name='settings'),
46 url(r'^settings/$', settings.SettingsView.as_view(), name='settings'),
47 url(r'^tags/$', all_tags.AllTagsView.as_view(), name='tags'),
47 url(r'^tags/$', all_tags.AllTagsView.as_view(), name='tags'),
48 url(r'^captcha/', include('captcha.urls')),
49 url(r'^authors/$', AuthorsView.as_view(), name='authors'),
48 url(r'^authors/$', AuthorsView.as_view(), name='authors'),
50 url(r'^delete/(?P<post_id>\w+)/$', DeletePostView.as_view(),
49 url(r'^delete/(?P<post_id>\w+)/$', DeletePostView.as_view(),
51 name='delete'),
50 name='delete'),
52 url(r'^ban/(?P<post_id>\w+)/$', BanUserView.as_view(), name='ban'),
51 url(r'^ban/(?P<post_id>\w+)/$', BanUserView.as_view(), name='ban'),
53
52
54 url(r'^banned/$', views.banned.BannedView.as_view(), name='banned'),
53 url(r'^banned/$', views.banned.BannedView.as_view(), name='banned'),
55 url(r'^staticpage/(?P<name>\w+)/$', StaticPageView.as_view(),
54 url(r'^staticpage/(?P<name>\w+)/$', StaticPageView.as_view(),
56 name='staticpage'),
55 name='staticpage'),
57
56
58 # RSS feeds
57 # RSS feeds
59 url(r'^rss/$', AllThreadsFeed()),
58 url(r'^rss/$', AllThreadsFeed()),
60 url(r'^page/(?P<page>\w+)/rss/$', AllThreadsFeed()),
59 url(r'^page/(?P<page>\w+)/rss/$', AllThreadsFeed()),
61 url(r'^tag/(?P<tag_name>\w+)/rss/$', TagThreadsFeed()),
60 url(r'^tag/(?P<tag_name>\w+)/rss/$', TagThreadsFeed()),
62 url(r'^tag/(?P<tag_name>\w+)/page/(?P<page>\w+)/rss/$', TagThreadsFeed()),
61 url(r'^tag/(?P<tag_name>\w+)/page/(?P<page>\w+)/rss/$', TagThreadsFeed()),
63 url(r'^thread/(?P<post_id>\w+)/rss/$', ThreadPostsFeed()),
62 url(r'^thread/(?P<post_id>\w+)/rss/$', ThreadPostsFeed()),
64
63
65 # i18n
64 # i18n
66 url(r'^jsi18n/$', 'boards.views.cached_js_catalog', js_info_dict,
65 url(r'^jsi18n/$', 'boards.views.cached_js_catalog', js_info_dict,
67 name='js_info_dict'),
66 name='js_info_dict'),
68
67
69 # API
68 # API
70 url(r'^api/post/(?P<post_id>\w+)/$', api.get_post, name="get_post"),
69 url(r'^api/post/(?P<post_id>\w+)/$', api.get_post, name="get_post"),
71 url(r'^api/diff_thread/(?P<thread_id>\w+)/(?P<last_update_time>\w+)/$',
70 url(r'^api/diff_thread/(?P<thread_id>\w+)/(?P<last_update_time>\w+)/$',
72 api.api_get_threaddiff, name="get_thread_diff"),
71 api.api_get_threaddiff, name="get_thread_diff"),
73 url(r'^api/threads/(?P<count>\w+)/$', api.api_get_threads,
72 url(r'^api/threads/(?P<count>\w+)/$', api.api_get_threads,
74 name='get_threads'),
73 name='get_threads'),
75 url(r'^api/tags/$', api.api_get_tags, name='get_tags'),
74 url(r'^api/tags/$', api.api_get_tags, name='get_tags'),
76 url(r'^api/thread/(?P<opening_post_id>\w+)/$', api.api_get_thread_posts,
75 url(r'^api/thread/(?P<opening_post_id>\w+)/$', api.api_get_thread_posts,
77 name='get_thread'),
76 name='get_thread'),
78 url(r'^api/add_post/(?P<opening_post_id>\w+)/$', api.api_add_post,
77 url(r'^api/add_post/(?P<opening_post_id>\w+)/$', api.api_add_post,
79 name='add_post'),
78 name='add_post'),
80
79
81 # Search
80 # Search
82 url(r'^search/$', BoardSearchView.as_view(), name='search'),
81 url(r'^search/$', BoardSearchView.as_view(), name='search'),
83
82
84 )
83 )
@@ -1,82 +1,78 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
5 import time
6
6
7 from django.utils import timezone
7 from django.utils import timezone
8
8
9 from neboard import settings
9 from neboard import settings
10
10
11
11
12 KEY_CAPTCHA_FAILS = 'key_captcha_fails'
12 KEY_CAPTCHA_FAILS = 'key_captcha_fails'
13 KEY_CAPTCHA_DELAY_TIME = 'key_captcha_delay_time'
13 KEY_CAPTCHA_DELAY_TIME = 'key_captcha_delay_time'
14 KEY_CAPTCHA_LAST_ACTIVITY = 'key_captcha_last_activity'
14 KEY_CAPTCHA_LAST_ACTIVITY = 'key_captcha_last_activity'
15
15
16
16
17 def need_include_captcha(request):
17 def need_include_captcha(request):
18 """
18 """
19 Check if request is made by a user.
19 Check if request is made by a user.
20 It contains rules which check for bots.
20 It contains rules which check for bots.
21 """
21 """
22
22
23 if not settings.ENABLE_CAPTCHA:
23 if not settings.ENABLE_CAPTCHA:
24 return False
24 return False
25
25
26 enable_captcha = False
26 enable_captcha = False
27
27
28 #newcomer
28 #newcomer
29 if KEY_CAPTCHA_LAST_ACTIVITY not in request.session:
29 if KEY_CAPTCHA_LAST_ACTIVITY not in request.session:
30 return settings.ENABLE_CAPTCHA
30 return settings.ENABLE_CAPTCHA
31
31
32 last_activity = request.session[KEY_CAPTCHA_LAST_ACTIVITY]
32 last_activity = request.session[KEY_CAPTCHA_LAST_ACTIVITY]
33 current_delay = int(time.time()) - last_activity
33 current_delay = int(time.time()) - last_activity
34
34
35 delay_time = (request.session[KEY_CAPTCHA_DELAY_TIME]
35 delay_time = (request.session[KEY_CAPTCHA_DELAY_TIME]
36 if KEY_CAPTCHA_DELAY_TIME in request.session
36 if KEY_CAPTCHA_DELAY_TIME in request.session
37 else settings.CAPTCHA_DEFAULT_SAFE_TIME)
37 else settings.CAPTCHA_DEFAULT_SAFE_TIME)
38
38
39 if current_delay < delay_time:
39 if current_delay < delay_time:
40 enable_captcha = True
40 enable_captcha = True
41
41
42 print 'ENABLING' + str(enable_captcha)
43
44 return enable_captcha
42 return enable_captcha
45
43
46
44
47 def update_captcha_access(request, passed):
45 def update_captcha_access(request, passed):
48 """
46 """
49 Update captcha fields.
47 Update captcha fields.
50 It will reduce delay time if user passed captcha verification and
48 It will reduce delay time if user passed captcha verification and
51 it will increase it otherwise.
49 it will increase it otherwise.
52 """
50 """
53 session = request.session
51 session = request.session
54
52
55 delay_time = (request.session[KEY_CAPTCHA_DELAY_TIME]
53 delay_time = (request.session[KEY_CAPTCHA_DELAY_TIME]
56 if KEY_CAPTCHA_DELAY_TIME in request.session
54 if KEY_CAPTCHA_DELAY_TIME in request.session
57 else settings.CAPTCHA_DEFAULT_SAFE_TIME)
55 else settings.CAPTCHA_DEFAULT_SAFE_TIME)
58
56
59 print "DELAY TIME = " + str(delay_time)
60
61 if passed:
57 if passed:
62 delay_time -= 2 if delay_time >= 7 else 5
58 delay_time -= 2 if delay_time >= 7 else 5
63 else:
59 else:
64 delay_time += 10
60 delay_time += 10
65
61
66 session[KEY_CAPTCHA_LAST_ACTIVITY] = int(time.time())
62 session[KEY_CAPTCHA_LAST_ACTIVITY] = int(time.time())
67 session[KEY_CAPTCHA_DELAY_TIME] = delay_time
63 session[KEY_CAPTCHA_DELAY_TIME] = delay_time
68
64
69
65
70 def get_client_ip(request):
66 def get_client_ip(request):
71 x_forwarded_for = request.META.get('HTTP_X_FORWARDED_FOR')
67 x_forwarded_for = request.META.get('HTTP_X_FORWARDED_FOR')
72 if x_forwarded_for:
68 if x_forwarded_for:
73 ip = x_forwarded_for.split(',')[-1].strip()
69 ip = x_forwarded_for.split(',')[-1].strip()
74 else:
70 else:
75 ip = request.META.get('REMOTE_ADDR')
71 ip = request.META.get('REMOTE_ADDR')
76 return ip
72 return ip
77
73
78
74
79 def datetime_to_epoch(datetime):
75 def datetime_to_epoch(datetime):
80 return int(time.mktime(timezone.localtime(
76 return int(time.mktime(timezone.localtime(
81 datetime,timezone.get_current_timezone()).timetuple())
77 datetime,timezone.get_current_timezone()).timetuple())
82 * 1000000 + datetime.microsecond) No newline at end of file
78 * 1000000 + datetime.microsecond)
@@ -1,260 +1,260 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 bbcode_extended
3 from boards.mdx_neboard import bbcode_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 MARKUP_FIELD_TYPES = (
219 MARKUP_FIELD_TYPES = (
220 ('bbcode', bbcode_extended),
220 ('bbcode', bbcode_extended),
221 )
221 )
222
222
223 THEMES = [
223 THEMES = [
224 ('md', 'Mystic Dark'),
224 ('md', 'Mystic Dark'),
225 ('md_centered', 'Mystic Dark (centered)'),
225 ('md_centered', 'Mystic Dark (centered)'),
226 ('sw', 'Snow White'),
226 ('sw', 'Snow White'),
227 ('pg', 'Photon Gray'),
227 ('pg', 'Photon Gray'),
228 ]
228 ]
229
229
230 POPULAR_TAGS = 10
230 POPULAR_TAGS = 10
231
231
232 ENABLE_CAPTCHA = False
232 ENABLE_CAPTCHA = False
233 # if user tries to post before CAPTCHA_DEFAULT_SAFE_TIME. Captcha will be shown
233 # if user tries to post before CAPTCHA_DEFAULT_SAFE_TIME. Captcha will be shown
234 CAPTCHA_DEFAULT_SAFE_TIME = 30 # seconds
234 CAPTCHA_DEFAULT_SAFE_TIME = 30 # seconds
235 POSTING_DELAY = 20 # seconds
235 POSTING_DELAY = 20 # seconds
236
236
237 COMPRESS_HTML = True
237 COMPRESS_HTML = True
238
238
239 # Debug mode middlewares
239 # Debug mode middlewares
240 if DEBUG:
240 if DEBUG:
241 MIDDLEWARE_CLASSES += (
241 MIDDLEWARE_CLASSES += (
242 'boards.profiler.ProfilerMiddleware',
242 #'boards.profiler.ProfilerMiddleware',
243 'debug_toolbar.middleware.DebugToolbarMiddleware',
243 'debug_toolbar.middleware.DebugToolbarMiddleware',
244 )
244 )
245
245
246 def custom_show_toolbar(request):
246 def custom_show_toolbar(request):
247 return DEBUG
247 return DEBUG
248
248
249 DEBUG_TOOLBAR_CONFIG = {
249 DEBUG_TOOLBAR_CONFIG = {
250 'INTERCEPT_REDIRECTS': False,
250 'INTERCEPT_REDIRECTS': False,
251 'SHOW_TOOLBAR_CALLBACK': custom_show_toolbar,
251 'SHOW_TOOLBAR_CALLBACK': custom_show_toolbar,
252 'HIDE_DJANGO_SQL': False,
252 'HIDE_DJANGO_SQL': False,
253 'ENABLE_STACKTRACES': True,
253 'ENABLE_STACKTRACES': True,
254 }
254 }
255
255
256 # FIXME Uncommenting this fails somehow. Need to investigate this
256 # FIXME Uncommenting this fails somehow. Need to investigate this
257 #DEBUG_TOOLBAR_PANELS += (
257 #DEBUG_TOOLBAR_PANELS += (
258 # 'debug_toolbar.panels.profiling.ProfilingDebugPanel',
258 # 'debug_toolbar.panels.profiling.ProfilingDebugPanel',
259 #)
259 #)
260
260
General Comments 0
You need to be logged in to leave comments. Login now