##// END OF EJS Templates
Synced with default branch changes
neko259 -
r785:b8db9e2d merge decentral
parent child Browse files
Show More
@@ -1,1 +0,0 b''
1 import settings No newline at end of file
@@ -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 += '<span class="mark_btn"' + \
43 u' onClick="addMarkToMsg(\'' + formatter.format_left + \
42 ' 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 + '</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(['(!) %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 list(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,346 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 if not tags:
52 tags = []
53
51 posting_time = timezone.now()
54 posting_time = timezone.now()
52 if not thread:
55 if not thread:
53 thread = Thread.objects.create(bump_time=posting_time,
56 thread = Thread.objects.create(bump_time=posting_time,
54 last_edit_time=posting_time)
57 last_edit_time=posting_time)
55 new_thread = True
58 new_thread = True
56 else:
59 else:
57 thread.bump()
60 thread.bump()
58 thread.last_edit_time = posting_time
61 thread.last_edit_time = posting_time
59 thread.save()
62 thread.save()
60 new_thread = False
63 new_thread = False
61
64
62 post = self.create(title=title,
65 post = self.create(title=title,
63 text=text,
66 text=text,
64 pub_time=posting_time,
67 pub_time=posting_time,
65 thread_new=thread,
68 thread_new=thread,
66 poster_ip=ip,
69 poster_ip=ip,
67 poster_user_agent=UNKNOWN_UA, # TODO Get UA at
70 poster_user_agent=UNKNOWN_UA, # TODO Get UA at
68 # last!
71 # last!
69 last_edit_time=posting_time)
72 last_edit_time=posting_time)
70
73
71 if image:
74 if image:
72 post_image = PostImage.objects.create(image=image)
75 post_image = PostImage.objects.create(image=image)
73 post.images.add(post_image)
76 post.images.add(post_image)
74 logger.info('Created image #%d for post #%d' % (post_image.id,
77 logger.info('Created image #%d for post #%d' % (post_image.id,
75 post.id))
78 post.id))
76
79
77 thread.replies.add(post)
80 thread.replies.add(post)
78 if tags:
81 list(map(thread.add_tag, tags))
79 map(thread.add_tag, tags)
80
82
81 if new_thread:
83 if new_thread:
82 Thread.objects.process_oldest_threads()
84 Thread.objects.process_oldest_threads()
83 self.connect_replies(post)
85 self.connect_replies(post)
84
86
85 logger.info('Created post #%d with title %s' % (post.id,
87 logger.info('Created post #%d with title %s' % (post.id,
86 post.get_title()))
88 post.get_title()))
87
89
88 return post
90 return post
89
91
90 def delete_post(self, post):
92 def delete_post(self, post):
91 """
93 """
92 Deletes post and update or delete its thread
94 Deletes post and update or delete its thread
93 """
95 """
94
96
95 post_id = post.id
97 post_id = post.id
96
98
97 thread = post.get_thread()
99 thread = post.get_thread()
98
100
99 if post.is_opening():
101 if post.is_opening():
100 thread.delete()
102 thread.delete()
101 else:
103 else:
102 thread.last_edit_time = timezone.now()
104 thread.last_edit_time = timezone.now()
103 thread.save()
105 thread.save()
104
106
105 post.delete()
107 post.delete()
106
108
107 logger.info('Deleted post #%d (%s)' % (post_id, post.get_title()))
109 logger.info('Deleted post #%d (%s)' % (post_id, post.get_title()))
108
110
109 def delete_posts_by_ip(self, ip):
111 def delete_posts_by_ip(self, ip):
110 """
112 """
111 Deletes all posts of the author with same IP
113 Deletes all posts of the author with same IP
112 """
114 """
113
115
114 posts = self.filter(poster_ip=ip)
116 posts = self.filter(poster_ip=ip)
115 map(self.delete_post, posts)
117 for post in posts:
118 self.delete_post(post)
116
119
117 def connect_replies(self, post):
120 def connect_replies(self, post):
118 """
121 """
119 Connects replies to a post to show them as a reflink map
122 Connects replies to a post to show them as a reflink map
120 """
123 """
121
124
122 for reply_number in re.finditer(REGEX_REPLY, post.text.raw):
125 for reply_number in re.finditer(REGEX_REPLY, post.text.raw):
123 post_id = reply_number.group(1)
126 post_id = reply_number.group(1)
124 ref_post = self.filter(id=post_id)
127 ref_post = self.filter(id=post_id)
125 if ref_post.count() > 0:
128 if ref_post.count() > 0:
126 referenced_post = ref_post[0]
129 referenced_post = ref_post[0]
127 referenced_post.referenced_posts.add(post)
130 referenced_post.referenced_posts.add(post)
128 referenced_post.last_edit_time = post.pub_time
131 referenced_post.last_edit_time = post.pub_time
129 referenced_post.build_refmap()
132 referenced_post.build_refmap()
130 referenced_post.save(update_fields=['refmap', 'last_edit_time'])
133 referenced_post.save(update_fields=['refmap', 'last_edit_time'])
131
134
132 referenced_thread = referenced_post.get_thread()
135 referenced_thread = referenced_post.get_thread()
133 referenced_thread.last_edit_time = post.pub_time
136 referenced_thread.last_edit_time = post.pub_time
134 referenced_thread.save(update_fields=['last_edit_time'])
137 referenced_thread.save(update_fields=['last_edit_time'])
135
138
136 def get_posts_per_day(self):
139 def get_posts_per_day(self):
137 """
140 """
138 Gets average count of posts per day for the last 7 days
141 Gets average count of posts per day for the last 7 days
139 """
142 """
140
143
141 day_end = date.today()
144 day_end = date.today()
142 day_start = day_end - timedelta(POSTS_PER_DAY_RANGE)
145 day_start = day_end - timedelta(POSTS_PER_DAY_RANGE)
143
146
144 cache_key = CACHE_KEY_PPD + str(day_end)
147 cache_key = CACHE_KEY_PPD + str(day_end)
145 ppd = cache.get(cache_key)
148 ppd = cache.get(cache_key)
146 if ppd:
149 if ppd:
147 return ppd
150 return ppd
148
151
149 day_time_start = timezone.make_aware(datetime.combine(
152 day_time_start = timezone.make_aware(datetime.combine(
150 day_start, dtime()), timezone.get_current_timezone())
153 day_start, dtime()), timezone.get_current_timezone())
151 day_time_end = timezone.make_aware(datetime.combine(
154 day_time_end = timezone.make_aware(datetime.combine(
152 day_end, dtime()), timezone.get_current_timezone())
155 day_end, dtime()), timezone.get_current_timezone())
153
156
154 posts_per_period = float(self.filter(
157 posts_per_period = float(self.filter(
155 pub_time__lte=day_time_end,
158 pub_time__lte=day_time_end,
156 pub_time__gte=day_time_start).count())
159 pub_time__gte=day_time_start).count())
157
160
158 ppd = posts_per_period / POSTS_PER_DAY_RANGE
161 ppd = posts_per_period / POSTS_PER_DAY_RANGE
159
162
160 cache.set(cache_key, ppd)
163 cache.set(cache_key, ppd)
161 return ppd
164 return ppd
162
165
163
166
164 class Post(models.Model, Viewable):
167 class Post(models.Model, Viewable):
165 """A post is a message."""
168 """A post is a message."""
166
169
167 objects = PostManager()
170 objects = PostManager()
168
171
169 class Meta:
172 class Meta:
170 app_label = APP_LABEL_BOARDS
173 app_label = APP_LABEL_BOARDS
171 ordering = ('id',)
174 ordering = ('id',)
172
175
173 title = models.CharField(max_length=TITLE_MAX_LENGTH)
176 title = models.CharField(max_length=TITLE_MAX_LENGTH)
174 pub_time = models.DateTimeField()
177 pub_time = models.DateTimeField()
175 text = MarkupField(default_markup_type=DEFAULT_MARKUP_TYPE,
178 text = MarkupField(default_markup_type=DEFAULT_MARKUP_TYPE,
176 escape_html=False)
179 escape_html=False)
177
180
178 images = models.ManyToManyField(PostImage, null=True, blank=True,
181 images = models.ManyToManyField(PostImage, null=True, blank=True,
179 related_name='ip+', db_index=True)
182 related_name='ip+', db_index=True)
180
183
181 poster_ip = models.GenericIPAddressField()
184 poster_ip = models.GenericIPAddressField()
182 poster_user_agent = models.TextField()
185 poster_user_agent = models.TextField()
183
186
184 thread_new = models.ForeignKey('Thread', null=True, default=None,
187 thread_new = models.ForeignKey('Thread', null=True, default=None,
185 db_index=True)
188 db_index=True)
186 last_edit_time = models.DateTimeField()
189 last_edit_time = models.DateTimeField()
187
190
188 referenced_posts = models.ManyToManyField('Post', symmetrical=False,
191 referenced_posts = models.ManyToManyField('Post', symmetrical=False,
189 null=True,
192 null=True,
190 blank=True, related_name='rfp+',
193 blank=True, related_name='rfp+',
191 db_index=True)
194 db_index=True)
192 refmap = models.TextField(null=True, blank=True)
195 refmap = models.TextField(null=True, blank=True)
193
196
194 def __unicode__(self):
197 def __unicode__(self):
195 return '#' + str(self.id) + ' ' + self.title + ' (' + \
198 return '#' + str(self.id) + ' ' + self.title + ' (' + \
196 self.text.raw[:50] + ')'
199 self.text.raw[:50] + ')'
197
200
198 def get_title(self):
201 def get_title(self):
199 """
202 """
200 Gets original post title or part of its text.
203 Gets original post title or part of its text.
201 """
204 """
202
205
203 title = self.title
206 title = self.title
204 if not title:
207 if not title:
205 title = self.text.rendered
208 title = self.text.rendered
206
209
207 return title
210 return title
208
211
209 def build_refmap(self):
212 def build_refmap(self):
210 """
213 """
211 Builds a replies map string from replies list. This is a cache to stop
214 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.
215 the server from recalculating the map on every post show.
213 """
216 """
214 map_string = ''
217 map_string = ''
215
218
216 first = True
219 first = True
217 for refpost in self.referenced_posts.all():
220 for refpost in self.referenced_posts.all():
218 if not first:
221 if not first:
219 map_string += ', '
222 map_string += ', '
220 map_string += '<a href="%s">&gt;&gt;%s</a>' % (refpost.get_url(),
223 map_string += '<a href="%s">&gt;&gt;%s</a>' % (refpost.get_url(),
221 refpost.id)
224 refpost.id)
222 first = False
225 first = False
223
226
224 self.refmap = map_string
227 self.refmap = map_string
225
228
226 def get_sorted_referenced_posts(self):
229 def get_sorted_referenced_posts(self):
227 return self.refmap
230 return self.refmap
228
231
229 def is_referenced(self):
232 def is_referenced(self):
230 return len(self.refmap) > 0
233 return len(self.refmap) > 0
231
234
232 def is_opening(self):
235 def is_opening(self):
233 """
236 """
234 Checks if this is an opening post or just a reply.
237 Checks if this is an opening post or just a reply.
235 """
238 """
236
239
237 return self.get_thread().get_opening_post_id() == self.id
240 return self.get_thread().get_opening_post_id() == self.id
238
241
239 @transaction.atomic
242 @transaction.atomic
240 def add_tag(self, tag):
243 def add_tag(self, tag):
241 edit_time = timezone.now()
244 edit_time = timezone.now()
242
245
243 thread = self.get_thread()
246 thread = self.get_thread()
244 thread.add_tag(tag)
247 thread.add_tag(tag)
245 self.last_edit_time = edit_time
248 self.last_edit_time = edit_time
246 self.save(update_fields=['last_edit_time'])
249 self.save(update_fields=['last_edit_time'])
247
250
248 thread.last_edit_time = edit_time
251 thread.last_edit_time = edit_time
249 thread.save(update_fields=['last_edit_time'])
252 thread.save(update_fields=['last_edit_time'])
250
253
251 @transaction.atomic
254 @transaction.atomic
252 def remove_tag(self, tag):
255 def remove_tag(self, tag):
253 edit_time = timezone.now()
256 edit_time = timezone.now()
254
257
255 thread = self.get_thread()
258 thread = self.get_thread()
256 thread.remove_tag(tag)
259 thread.remove_tag(tag)
257 self.last_edit_time = edit_time
260 self.last_edit_time = edit_time
258 self.save(update_fields=['last_edit_time'])
261 self.save(update_fields=['last_edit_time'])
259
262
260 thread.last_edit_time = edit_time
263 thread.last_edit_time = edit_time
261 thread.save(update_fields=['last_edit_time'])
264 thread.save(update_fields=['last_edit_time'])
262
265
263 def get_url(self, thread=None):
266 def get_url(self, thread=None):
264 """
267 """
265 Gets full url to the post.
268 Gets full url to the post.
266 """
269 """
267
270
268 cache_key = CACHE_KEY_POST_URL + str(self.id)
271 cache_key = CACHE_KEY_POST_URL + str(self.id)
269 link = cache.get(cache_key)
272 link = cache.get(cache_key)
270
273
271 if not link:
274 if not link:
272 if not thread:
275 if not thread:
273 thread = self.get_thread()
276 thread = self.get_thread()
274
277
275 opening_id = thread.get_opening_post_id()
278 opening_id = thread.get_opening_post_id()
276
279
277 if self.id != opening_id:
280 if self.id != opening_id:
278 link = reverse('thread', kwargs={
281 link = reverse('thread', kwargs={
279 'post_id': opening_id}) + '#' + str(self.id)
282 'post_id': opening_id}) + '#' + str(self.id)
280 else:
283 else:
281 link = reverse('thread', kwargs={'post_id': self.id})
284 link = reverse('thread', kwargs={'post_id': self.id})
282
285
283 cache.set(cache_key, link)
286 cache.set(cache_key, link)
284
287
285 return link
288 return link
286
289
287 def get_thread(self):
290 def get_thread(self):
288 """
291 """
289 Gets post's thread.
292 Gets post's thread.
290 """
293 """
291
294
292 return self.thread_new
295 return self.thread_new
293
296
294 def get_referenced_posts(self):
297 def get_referenced_posts(self):
295 return self.referenced_posts.only('id', 'thread_new')
298 return self.referenced_posts.only('id', 'thread_new')
296
299
297 def get_text(self):
300 def get_text(self):
298 return self.text
301 return self.text
299
302
300 def get_view(self, moderator=False, need_open_link=False,
303 def get_view(self, moderator=False, need_open_link=False,
301 truncated=False, *args, **kwargs):
304 truncated=False, *args, **kwargs):
302 if 'is_opening' in kwargs:
305 if 'is_opening' in kwargs:
303 is_opening = kwargs['is_opening']
306 is_opening = kwargs['is_opening']
304 else:
307 else:
305 is_opening = self.is_opening()
308 is_opening = self.is_opening()
306
309
307 if 'thread' in kwargs:
310 if 'thread' in kwargs:
308 thread = kwargs['thread']
311 thread = kwargs['thread']
309 else:
312 else:
310 thread = self.get_thread()
313 thread = self.get_thread()
311
314
312 if 'can_bump' in kwargs:
315 if 'can_bump' in kwargs:
313 can_bump = kwargs['can_bump']
316 can_bump = kwargs['can_bump']
314 else:
317 else:
315 can_bump = thread.can_bump()
318 can_bump = thread.can_bump()
316
319
317 if is_opening:
320 if is_opening:
318 opening_post_id = self.id
321 opening_post_id = self.id
319 else:
322 else:
320 opening_post_id = thread.get_opening_post_id()
323 opening_post_id = thread.get_opening_post_id()
321
324
322 return render_to_string('boards/post.html', {
325 return render_to_string('boards/post.html', {
323 'post': self,
326 'post': self,
324 'moderator': moderator,
327 'moderator': moderator,
325 'is_opening': is_opening,
328 'is_opening': is_opening,
326 'thread': thread,
329 'thread': thread,
327 'bumpable': can_bump,
330 'bumpable': can_bump,
328 'need_open_link': need_open_link,
331 'need_open_link': need_open_link,
329 'truncated': truncated,
332 'truncated': truncated,
330 'opening_post_id': opening_post_id,
333 'opening_post_id': opening_post_id,
331 })
334 })
332
335
333 def get_first_image(self):
336 def get_first_image(self):
334 return self.images.earliest('id')
337 return self.images.earliest('id')
335
338
336 def delete(self, using=None):
339 def delete(self, using=None):
337 """
340 """
338 Deletes all post images and the post itself.
341 Deletes all post images and the post itself.
339 """
342 """
340
343
341 self.images.all().delete()
344 self.images.all().delete()
342
345
343 super(Post, self).delete(using)
346 super(Post, self).delete(using)
@@ -1,456 +1,455 b''
1 html {
1 html {
2 background: #555;
2 background: #555;
3 color: #ffffff;
3 color: #ffffff;
4 }
4 }
5
5
6 body {
6 body {
7 margin: 0;
7 margin: 0;
8 }
8 }
9
9
10 #admin_panel {
10 #admin_panel {
11 background: #FF0000;
11 background: #FF0000;
12 color: #00FF00
12 color: #00FF00
13 }
13 }
14
14
15 .input_field_error {
15 .input_field_error {
16 color: #FF0000;
16 color: #FF0000;
17 }
17 }
18
18
19 .title {
19 .title {
20 font-weight: bold;
20 font-weight: bold;
21 color: #ffcc00;
21 color: #ffcc00;
22 }
22 }
23
23
24 .link, a {
24 .link, a {
25 color: #afdcec;
25 color: #afdcec;
26 }
26 }
27
27
28 .block {
28 .block {
29 display: inline-block;
29 display: inline-block;
30 vertical-align: top;
30 vertical-align: top;
31 }
31 }
32
32
33 .tag {
33 .tag {
34 color: #FFD37D;
34 color: #FFD37D;
35 }
35 }
36
36
37 .post_id {
37 .post_id {
38 color: #fff380;
38 color: #fff380;
39 }
39 }
40
40
41 .post, .dead_post, .archive_post, #posts-table {
41 .post, .dead_post, .archive_post, #posts-table {
42 background: #333;
42 background: #333;
43 padding: 10px;
43 padding: 10px;
44 clear: left;
44 clear: left;
45 word-wrap: break-word;
45 word-wrap: break-word;
46 border-top: 1px solid #777;
46 border-top: 1px solid #777;
47 border-bottom: 1px solid #777;
47 border-bottom: 1px solid #777;
48 }
48 }
49
49
50 .post + .post {
50 .post + .post {
51 border-top: none;
51 border-top: none;
52 }
52 }
53
53
54 .dead_post + .dead_post {
54 .dead_post + .dead_post {
55 border-top: none;
55 border-top: none;
56 }
56 }
57
57
58 .archive_post + .archive_post {
58 .archive_post + .archive_post {
59 border-top: none;
59 border-top: none;
60 }
60 }
61
61
62 .metadata {
62 .metadata {
63 padding-top: 5px;
63 padding-top: 5px;
64 margin-top: 10px;
64 margin-top: 10px;
65 border-top: solid 1px #666;
65 border-top: solid 1px #666;
66 color: #ddd;
66 color: #ddd;
67 }
67 }
68
68
69 .navigation_panel, .tag_info {
69 .navigation_panel, .tag_info {
70 background: #444;
70 background: #444;
71 margin-bottom: 5px;
71 margin-bottom: 5px;
72 margin-top: 5px;
72 margin-top: 5px;
73 padding: 10px;
73 padding: 10px;
74 border-bottom: solid 1px #888;
74 border-bottom: solid 1px #888;
75 border-top: solid 1px #888;
75 border-top: solid 1px #888;
76 color: #eee;
76 color: #eee;
77 }
77 }
78
78
79 .navigation_panel .link {
79 .navigation_panel .link {
80 border-right: 1px solid #fff;
80 border-right: 1px solid #fff;
81 font-weight: bold;
81 font-weight: bold;
82 margin-right: 1ex;
82 margin-right: 1ex;
83 padding-right: 1ex;
83 padding-right: 1ex;
84 }
84 }
85 .navigation_panel .link:last-child {
85 .navigation_panel .link:last-child {
86 border-left: 1px solid #fff;
86 border-left: 1px solid #fff;
87 border-right: none;
87 border-right: none;
88 float: right;
88 float: right;
89 margin-left: 1ex;
89 margin-left: 1ex;
90 margin-right: 0;
90 margin-right: 0;
91 padding-left: 1ex;
91 padding-left: 1ex;
92 padding-right: 0;
92 padding-right: 0;
93 }
93 }
94
94
95 .navigation_panel::after, .post::after {
95 .navigation_panel::after, .post::after {
96 clear: both;
96 clear: both;
97 content: ".";
97 content: ".";
98 display: block;
98 display: block;
99 height: 0;
99 height: 0;
100 line-height: 0;
100 line-height: 0;
101 visibility: hidden;
101 visibility: hidden;
102 }
102 }
103
103
104 p {
104 p {
105 margin-top: .5em;
105 margin-top: .5em;
106 margin-bottom: .5em;
106 margin-bottom: .5em;
107 }
107 }
108
108
109 br {
109 br {
110 margin-bottom: .5em;
110 margin-bottom: .5em;
111 }
111 }
112
112
113 .post-form-w {
113 .post-form-w {
114 background: #333344;
114 background: #333344;
115 border-top: solid 1px #888;
115 border-top: solid 1px #888;
116 border-bottom: solid 1px #888;
116 border-bottom: solid 1px #888;
117 color: #fff;
117 color: #fff;
118 padding: 10px;
118 padding: 10px;
119 margin-bottom: 5px;
119 margin-bottom: 5px;
120 margin-top: 5px;
120 margin-top: 5px;
121 }
121 }
122
122
123 .form-row {
123 .form-row {
124 width: 100%;
124 width: 100%;
125 }
125 }
126
126
127 .form-label {
127 .form-label {
128 padding: .25em 1ex .25em 0;
128 padding: .25em 1ex .25em 0;
129 vertical-align: top;
129 vertical-align: top;
130 }
130 }
131
131
132 .form-input {
132 .form-input {
133 padding: .25em 0;
133 padding: .25em 0;
134 }
134 }
135
135
136 .form-errors {
136 .form-errors {
137 font-weight: bolder;
137 font-weight: bolder;
138 vertical-align: middle;
138 vertical-align: middle;
139 }
139 }
140
140
141 .post-form input:not([name="image"]), .post-form textarea {
141 .post-form input:not([name="image"]), .post-form textarea {
142 background: #333;
142 background: #333;
143 color: #fff;
143 color: #fff;
144 border: solid 1px;
144 border: solid 1px;
145 padding: 0;
145 padding: 0;
146 font: medium sans-serif;
146 font: medium sans-serif;
147 width: 100%;
147 width: 100%;
148 }
148 }
149
149
150 .form-submit {
150 .form-submit {
151 display: table;
151 display: table;
152 margin-bottom: 1ex;
152 margin-bottom: 1ex;
153 margin-top: 1ex;
153 margin-top: 1ex;
154 }
154 }
155
155
156 .form-title {
156 .form-title {
157 font-weight: bold;
157 font-weight: bold;
158 font-size: 2ex;
158 font-size: 2ex;
159 margin-bottom: 0.5ex;
159 margin-bottom: 0.5ex;
160 }
160 }
161
161
162 .post-form input[type="submit"], input[type="submit"] {
162 .post-form input[type="submit"], input[type="submit"] {
163 background: #222;
163 background: #222;
164 border: solid 2px #fff;
164 border: solid 2px #fff;
165 color: #fff;
165 color: #fff;
166 padding: 0.5ex;
166 padding: 0.5ex;
167 }
167 }
168
168
169 input[type="submit"]:hover {
169 input[type="submit"]:hover {
170 background: #060;
170 background: #060;
171 }
171 }
172
172
173 blockquote {
173 blockquote {
174 border-left: solid 2px;
174 border-left: solid 2px;
175 padding-left: 5px;
175 padding-left: 5px;
176 color: #B1FB17;
176 color: #B1FB17;
177 margin: 0;
177 margin: 0;
178 }
178 }
179
179
180 .post > .image {
180 .post > .image {
181 float: left;
181 float: left;
182 margin: 0 1ex .5ex 0;
182 margin: 0 1ex .5ex 0;
183 min-width: 1px;
183 min-width: 1px;
184 text-align: center;
184 text-align: center;
185 display: table-row;
185 display: table-row;
186 }
186 }
187
187
188 .post > .metadata {
188 .post > .metadata {
189 clear: left;
189 clear: left;
190 }
190 }
191
191
192 .get {
192 .get {
193 font-weight: bold;
193 font-weight: bold;
194 color: #d55;
194 color: #d55;
195 }
195 }
196
196
197 * {
197 * {
198 text-decoration: none;
198 text-decoration: none;
199 }
199 }
200
200
201 .dead_post {
201 .dead_post {
202 background-color: #442222;
202 background-color: #442222;
203 }
203 }
204
204
205 .archive_post {
205 .archive_post {
206 background-color: #000;
206 background-color: #000;
207 }
207 }
208
208
209 .mark_btn {
209 .mark_btn {
210 border: 1px solid;
210 border: 1px solid;
211 min-width: 2ex;
211 min-width: 2ex;
212 padding: 2px 2ex;
212 padding: 2px 2ex;
213 }
213 }
214
214
215 .mark_btn:hover {
215 .mark_btn:hover {
216 background: #555;
216 background: #555;
217 }
217 }
218
218
219 .quote {
219 .quote {
220 color: #92cf38;
220 color: #92cf38;
221 font-style: italic;
221 font-style: italic;
222 }
222 }
223
223
224 .multiquote {
224 .multiquote {
225 border-left: solid 4px #ccc;
226 padding: 3px;
225 padding: 3px;
227 display: inline-block;
226 display: inline-block;
228 background: #222;
227 background: #222;
229 border-right: solid 1px #ccc;
228 border-style: solid;
230 border-top: solid 1px #ccc;
229 border-width: 1px 1px 1px 4px;
231 border-bottom: solid 1px #ccc;
230 font-size: 0.9em;
232 }
231 }
233
232
234 .spoiler {
233 .spoiler {
235 background: white;
234 background: white;
236 color: white;
235 color: white;
237 }
236 }
238
237
239 .spoiler:hover {
238 .spoiler:hover {
240 color: black;
239 color: black;
241 }
240 }
242
241
243 .comment {
242 .comment {
244 color: #eb2;
243 color: #eb2;
245 }
244 }
246
245
247 a:hover {
246 a:hover {
248 text-decoration: underline;
247 text-decoration: underline;
249 }
248 }
250
249
251 .last-replies {
250 .last-replies {
252 margin-left: 3ex;
251 margin-left: 3ex;
253 margin-right: 3ex;
252 margin-right: 3ex;
254 }
253 }
255
254
256 .thread {
255 .thread {
257 margin-bottom: 3ex;
256 margin-bottom: 3ex;
258 margin-top: 1ex;
257 margin-top: 1ex;
259 }
258 }
260
259
261 .post:target {
260 .post:target {
262 border: solid 2px white;
261 border: solid 2px white;
263 }
262 }
264
263
265 pre{
264 pre{
266 white-space:pre-wrap
265 white-space:pre-wrap
267 }
266 }
268
267
269 li {
268 li {
270 list-style-position: inside;
269 list-style-position: inside;
271 }
270 }
272
271
273 .fancybox-skin {
272 .fancybox-skin {
274 position: relative;
273 position: relative;
275 background-color: #fff;
274 background-color: #fff;
276 color: #ddd;
275 color: #ddd;
277 text-shadow: none;
276 text-shadow: none;
278 }
277 }
279
278
280 .fancybox-image {
279 .fancybox-image {
281 border: 1px solid black;
280 border: 1px solid black;
282 }
281 }
283
282
284 .image-mode-tab {
283 .image-mode-tab {
285 background: #444;
284 background: #444;
286 color: #eee;
285 color: #eee;
287 margin-top: 5px;
286 margin-top: 5px;
288 padding: 5px;
287 padding: 5px;
289 border-top: 1px solid #888;
288 border-top: 1px solid #888;
290 border-bottom: 1px solid #888;
289 border-bottom: 1px solid #888;
291 }
290 }
292
291
293 .image-mode-tab > label {
292 .image-mode-tab > label {
294 margin: 0 1ex;
293 margin: 0 1ex;
295 }
294 }
296
295
297 .image-mode-tab > label > input {
296 .image-mode-tab > label > input {
298 margin-right: .5ex;
297 margin-right: .5ex;
299 }
298 }
300
299
301 #posts-table {
300 #posts-table {
302 margin-top: 5px;
301 margin-top: 5px;
303 margin-bottom: 5px;
302 margin-bottom: 5px;
304 }
303 }
305
304
306 .tag_info > h2 {
305 .tag_info > h2 {
307 margin: 0;
306 margin: 0;
308 }
307 }
309
308
310 .post-info {
309 .post-info {
311 color: #ddd;
310 color: #ddd;
312 margin-bottom: 1ex;
311 margin-bottom: 1ex;
313 }
312 }
314
313
315 .moderator_info {
314 .moderator_info {
316 color: #e99d41;
315 color: #e99d41;
317 float: right;
316 float: right;
318 font-weight: bold;
317 font-weight: bold;
319 }
318 }
320
319
321 .refmap {
320 .refmap {
322 font-size: 0.9em;
321 font-size: 0.9em;
323 color: #ccc;
322 color: #ccc;
324 margin-top: 1em;
323 margin-top: 1em;
325 }
324 }
326
325
327 .fav {
326 .fav {
328 color: yellow;
327 color: yellow;
329 }
328 }
330
329
331 .not_fav {
330 .not_fav {
332 color: #ccc;
331 color: #ccc;
333 }
332 }
334
333
335 .role {
334 .role {
336 text-decoration: underline;
335 text-decoration: underline;
337 }
336 }
338
337
339 .form-email {
338 .form-email {
340 display: none;
339 display: none;
341 }
340 }
342
341
343 .footer {
342 .footer {
344 margin: 5px;
343 margin: 5px;
345 }
344 }
346
345
347 .bar-value {
346 .bar-value {
348 background: rgba(50, 55, 164, 0.45);
347 background: rgba(50, 55, 164, 0.45);
349 font-size: 0.9em;
348 font-size: 0.9em;
350 height: 1.5em;
349 height: 1.5em;
351 }
350 }
352
351
353 .bar-bg {
352 .bar-bg {
354 position: relative;
353 position: relative;
355 border-top: solid 1px #888;
354 border-top: solid 1px #888;
356 border-bottom: solid 1px #888;
355 border-bottom: solid 1px #888;
357 margin-top: 5px;
356 margin-top: 5px;
358 overflow: hidden;
357 overflow: hidden;
359 }
358 }
360
359
361 .bar-text {
360 .bar-text {
362 padding: 2px;
361 padding: 2px;
363 position: absolute;
362 position: absolute;
364 left: 0;
363 left: 0;
365 top: 0;
364 top: 0;
366 }
365 }
367
366
368 .page_link {
367 .page_link {
369 background: #444;
368 background: #444;
370 border-top: solid 1px #888;
369 border-top: solid 1px #888;
371 border-bottom: solid 1px #888;
370 border-bottom: solid 1px #888;
372 padding: 5px;
371 padding: 5px;
373 color: #eee;
372 color: #eee;
374 font-size: 2ex;
373 font-size: 2ex;
375 }
374 }
376
375
377 .skipped_replies {
376 .skipped_replies {
378 margin: 5px;
377 margin: 5px;
379 }
378 }
380
379
381 .current_page {
380 .current_page {
382 border: solid 1px #afdcec;
381 border: solid 1px #afdcec;
383 padding: 2px;
382 padding: 2px;
384 }
383 }
385
384
386 .current_mode {
385 .current_mode {
387 font-weight: bold;
386 font-weight: bold;
388 }
387 }
389
388
390 .gallery_image {
389 .gallery_image {
391 border: solid 1px;
390 border: solid 1px;
392 padding: 0.5ex;
391 padding: 0.5ex;
393 margin: 0.5ex;
392 margin: 0.5ex;
394 text-align: center;
393 text-align: center;
395 }
394 }
396
395
397 code {
396 code {
398 border: dashed 1px #ccc;
397 border: dashed 1px #ccc;
399 background: #111;
398 background: #111;
400 padding: 2px;
399 padding: 2px;
401 font-size: 1.2em;
400 font-size: 1.2em;
402 display: inline-block;
401 display: inline-block;
403 }
402 }
404
403
405 pre {
404 pre {
406 overflow: auto;
405 overflow: auto;
407 }
406 }
408
407
409 .img-full {
408 .img-full {
410 background: #222;
409 background: #222;
411 border: solid 1px white;
410 border: solid 1px white;
412 }
411 }
413
412
414 .tag_item {
413 .tag_item {
415 display: inline-block;
414 display: inline-block;
416 border: 1px dashed #666;
415 border: 1px dashed #666;
417 margin: 0.2ex;
416 margin: 0.2ex;
418 padding: 0.1ex;
417 padding: 0.1ex;
419 }
418 }
420
419
421 #id_models li {
420 #id_models li {
422 list-style: none;
421 list-style: none;
423 }
422 }
424
423
425 #id_q {
424 #id_q {
426 margin-left: 1ex;
425 margin-left: 1ex;
427 }
426 }
428
427
429 ul {
428 ul {
430 padding-left: 0px;
429 padding-left: 0px;
431 }
430 }
432
431
433 .quote-header {
432 .quote-header {
434 border-bottom: 2px solid #ddd;
433 border-bottom: 2px solid #ddd;
435 margin-bottom: 1ex;
434 margin-bottom: 1ex;
436 padding-bottom: .5ex;
435 padding-bottom: .5ex;
437 color: #ddd;
436 color: #ddd;
438 font-size: 1.2em;
437 font-size: 1.2em;
439 }
438 }
440
439
441 /* Reflink preview */
440 /* Reflink preview */
442 .post_preview {
441 .post_preview {
443 border-left: 1px solid #777;
442 border-left: 1px solid #777;
444 border-right: 1px solid #777;
443 border-right: 1px solid #777;
445 }
444 }
446
445
447 /* Code highlighter */
446 /* Code highlighter */
448 .hljs {
447 .hljs {
449 color: #fff;
448 color: #fff;
450 background: #000;
449 background: #000;
451 display: inline-block;
450 display: inline-block;
452 }
451 }
453
452
454 .hljs, .hljs-subst, .hljs-tag .hljs-title, .lisp .hljs-title, .clojure .hljs-built_in, .nginx .hljs-title {
453 .hljs, .hljs-subst, .hljs-tag .hljs-title, .lisp .hljs-title, .clojure .hljs-built_in, .nginx .hljs-title {
455 color: #fff;
454 color: #fff;
456 }
455 }
@@ -1,270 +1,263 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
186 captcha_enabled = neboard.settings.ENABLE_CAPTCHA
187 neboard.settings.ENABLE_CAPTCHA = False
188
189 client = Client()
185 client = Client()
190
186
191 valid_tags = u'tag1 tag_2 тег_3'
187 valid_tags = u'tag1 tag_2 тег_3'
192 invalid_tags = u'$%_356 ---'
188 invalid_tags = u'$%_356 ---'
193
189
194 response = client.post(NEW_THREAD_PAGE, {'title': 'test title',
190 response = client.post(NEW_THREAD_PAGE, {'title': 'test title',
195 'text': TEST_TEXT,
191 'text': TEST_TEXT,
196 'tags': valid_tags})
192 'tags': valid_tags})
197 self.assertEqual(response.status_code, HTTP_CODE_REDIRECT,
193 self.assertEqual(response.status_code, HTTP_CODE_REDIRECT,
198 msg='Posting new message failed: got code ' +
194 msg='Posting new message failed: got code ' +
199 str(response.status_code))
195 str(response.status_code))
200
196
201 self.assertEqual(1, Post.objects.count(),
197 self.assertEqual(1, Post.objects.count(),
202 msg='No posts were created')
198 msg='No posts were created')
203
199
204 client.post(NEW_THREAD_PAGE, {'text': TEST_TEXT,
200 client.post(NEW_THREAD_PAGE, {'text': TEST_TEXT,
205 'tags': invalid_tags})
201 'tags': invalid_tags})
206 self.assertEqual(1, Post.objects.count(), msg='The validation passed '
202 self.assertEqual(1, Post.objects.count(), msg='The validation passed '
207 'where it should fail')
203 'where it should fail')
208
204
209 # Change posting delay so we don't have to wait for 30 seconds or more
205 # Change posting delay so we don't have to wait for 30 seconds or more
210 old_posting_delay = neboard.settings.POSTING_DELAY
206 old_posting_delay = neboard.settings.POSTING_DELAY
211 # Wait fot the posting delay or we won't be able to post
207 # Wait fot the posting delay or we won't be able to post
212 settings.POSTING_DELAY = 1
208 settings.POSTING_DELAY = 1
213 time.sleep(neboard.settings.POSTING_DELAY + 1)
209 time.sleep(neboard.settings.POSTING_DELAY + 1)
214 response = client.post(THREAD_PAGE_ONE, {'text': TEST_TEXT,
210 response = client.post(THREAD_PAGE_ONE, {'text': TEST_TEXT,
215 'tags': valid_tags})
211 'tags': valid_tags})
216 self.assertEqual(HTTP_CODE_REDIRECT, response.status_code,
212 self.assertEqual(HTTP_CODE_REDIRECT, response.status_code,
217 msg=u'Posting new message failed: got code ' +
213 msg=u'Posting new message failed: got code ' +
218 str(response.status_code))
214 str(response.status_code))
219 # Restore posting delay
215 # Restore posting delay
220 settings.POSTING_DELAY = old_posting_delay
216 settings.POSTING_DELAY = old_posting_delay
221
217
222 self.assertEqual(2, Post.objects.count(),
218 self.assertEqual(2, Post.objects.count(),
223 msg=u'No posts were created')
219 msg=u'No posts were created')
224
220
225 # Restore captcha setting
226 settings.ENABLE_CAPTCHA = captcha_enabled
227
228
221
229 class ViewTest(TestCase):
222 class ViewTest(TestCase):
230
223
231 def test_all_views(self):
224 def test_all_views(self):
232 """
225 """
233 Try opening all views defined in ulrs.py that don't need additional
226 Try opening all views defined in ulrs.py that don't need additional
234 parameters
227 parameters
235 """
228 """
236
229
237 client = Client()
230 client = Client()
238 for url in urls.urlpatterns:
231 for url in urls.urlpatterns:
239 try:
232 try:
240 view_name = url.name
233 view_name = url.name
241 logger.debug('Testing view %s' % view_name)
234 logger.debug('Testing view %s' % view_name)
242
235
243 try:
236 try:
244 response = client.get(reverse(view_name))
237 response = client.get(reverse(view_name))
245
238
246 self.assertEqual(HTTP_CODE_OK, response.status_code,
239 self.assertEqual(HTTP_CODE_OK, response.status_code,
247 '%s view not opened' % view_name)
240 '%s view not opened' % view_name)
248 except NoReverseMatch:
241 except NoReverseMatch:
249 # This view just needs additional arguments
242 # This view just needs additional arguments
250 pass
243 pass
251 except Exception, e:
244 except Exception as e:
252 self.fail('Got exception %s at %s view' % (e, view_name))
245 self.fail('Got exception %s at %s view' % (e, view_name))
253 except AttributeError:
246 except AttributeError:
254 # This is normal, some views do not have names
247 # This is normal, some views do not have names
255 pass
248 pass
256
249
257
250
258 class AbstractTest(TestCase):
251 class AbstractTest(TestCase):
259 def test_settings_manager(self):
252 def test_settings_manager(self):
260 request = MockRequest()
253 request = MockRequest()
261 settings_manager = get_settings_manager(request)
254 settings_manager = get_settings_manager(request)
262
255
263 settings_manager.set_setting('test_setting', 'test_value')
256 settings_manager.set_setting('test_setting', 'test_value')
264 self.assertEqual('test_value', settings_manager.get_setting(
257 self.assertEqual('test_value', settings_manager.get_setting(
265 'test_setting'), u'Setting update failed.')
258 'test_setting'), u'Setting update failed.')
266
259
267
260
268 class MockRequest:
261 class MockRequest:
269 def __init__(self):
262 def __init__(self):
270 self.session = dict()
263 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 output = io.BytesIO()
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(output, format)
60 return ContentFile(io.getvalue())
60 return ContentFile(output.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,139 +1,139 b''
1 import string
1 import string
2
2
3 from django.db import transaction
3 from django.db import transaction
4 from django.shortcuts import render, redirect
4 from django.shortcuts import render, redirect
5
5
6 from boards import utils, settings
6 from boards import utils, settings
7 from boards.abstracts.paginator import get_paginator
7 from boards.abstracts.paginator import get_paginator
8 from boards.abstracts.settingsmanager import get_settings_manager
8 from boards.abstracts.settingsmanager import get_settings_manager
9 from boards.forms import ThreadForm, PlainErrorList
9 from boards.forms import ThreadForm, PlainErrorList
10 from boards.models import Post, Thread, Ban, Tag
10 from boards.models import Post, Thread, Ban, Tag
11 from boards.views.banned import BannedView
11 from boards.views.banned import BannedView
12 from boards.views.base import BaseBoardView, CONTEXT_FORM
12 from boards.views.base import BaseBoardView, CONTEXT_FORM
13 from boards.views.posting_mixin import PostMixin
13 from boards.views.posting_mixin import PostMixin
14
14
15 FORM_TAGS = 'tags'
15 FORM_TAGS = 'tags'
16 FORM_TEXT = 'text'
16 FORM_TEXT = 'text'
17 FORM_TITLE = 'title'
17 FORM_TITLE = 'title'
18 FORM_IMAGE = 'image'
18 FORM_IMAGE = 'image'
19
19
20 TAG_DELIMITER = ' '
20 TAG_DELIMITER = ' '
21
21
22 PARAMETER_CURRENT_PAGE = 'current_page'
22 PARAMETER_CURRENT_PAGE = 'current_page'
23 PARAMETER_PAGINATOR = 'paginator'
23 PARAMETER_PAGINATOR = 'paginator'
24 PARAMETER_THREADS = 'threads'
24 PARAMETER_THREADS = 'threads'
25
25
26 TEMPLATE = 'boards/posting_general.html'
26 TEMPLATE = 'boards/posting_general.html'
27 DEFAULT_PAGE = 1
27 DEFAULT_PAGE = 1
28
28
29
29
30 class AllThreadsView(PostMixin, BaseBoardView):
30 class AllThreadsView(PostMixin, BaseBoardView):
31
31
32 def __init__(self):
32 def __init__(self):
33 self.settings_manager = None
33 self.settings_manager = None
34 super(AllThreadsView, self).__init__()
34 super(AllThreadsView, self).__init__()
35
35
36 def get(self, request, page=DEFAULT_PAGE, form=None):
36 def get(self, request, page=DEFAULT_PAGE, form=None):
37 context = self.get_context_data(request=request)
37 context = self.get_context_data(request=request)
38
38
39 if not form:
39 if not form:
40 form = ThreadForm(error_class=PlainErrorList)
40 form = ThreadForm(error_class=PlainErrorList)
41
41
42 self.settings_manager = get_settings_manager(request)
42 self.settings_manager = get_settings_manager(request)
43 paginator = get_paginator(self.get_threads(),
43 paginator = get_paginator(self.get_threads(),
44 settings.THREADS_PER_PAGE)
44 settings.THREADS_PER_PAGE)
45 paginator.current_page = int(page)
45 paginator.current_page = int(page)
46
46
47 threads = paginator.page(page).object_list
47 threads = paginator.page(page).object_list
48
48
49 context[PARAMETER_THREADS] = threads
49 context[PARAMETER_THREADS] = threads
50 context[CONTEXT_FORM] = form
50 context[CONTEXT_FORM] = form
51
51
52 self._get_page_context(paginator, context, page)
52 self._get_page_context(paginator, context, page)
53
53
54 return render(request, TEMPLATE, context)
54 return render(request, TEMPLATE, context)
55
55
56 def post(self, request, page=DEFAULT_PAGE):
56 def post(self, request, page=DEFAULT_PAGE):
57 form = ThreadForm(request.POST, request.FILES,
57 form = ThreadForm(request.POST, request.FILES,
58 error_class=PlainErrorList)
58 error_class=PlainErrorList)
59 form.session = request.session
59 form.session = request.session
60
60
61 if form.is_valid():
61 if form.is_valid():
62 return self.create_thread(request, form)
62 return self.create_thread(request, form)
63 if form.need_to_ban:
63 if form.need_to_ban:
64 # Ban user because he is suspected to be a bot
64 # Ban user because he is suspected to be a bot
65 self._ban_current_user(request)
65 self._ban_current_user(request)
66
66
67 return self.get(request, page, form)
67 return self.get(request, page, form)
68
68
69 @staticmethod
69 @staticmethod
70 def _get_page_context(paginator, context, page):
70 def _get_page_context(paginator, context, page):
71 """
71 """
72 Get pagination context variables
72 Get pagination context variables
73 """
73 """
74
74
75 context[PARAMETER_PAGINATOR] = paginator
75 context[PARAMETER_PAGINATOR] = paginator
76 context[PARAMETER_CURRENT_PAGE] = paginator.page(int(page))
76 context[PARAMETER_CURRENT_PAGE] = paginator.page(int(page))
77
77
78 @staticmethod
78 @staticmethod
79 def parse_tags_string(tag_strings):
79 def parse_tags_string(tag_strings):
80 """
80 """
81 Parses tag list string and returns tag object list.
81 Parses tag list string and returns tag object list.
82 """
82 """
83
83
84 tags = []
84 tags = []
85
85
86 if tag_strings:
86 if tag_strings:
87 tag_strings = tag_strings.split(TAG_DELIMITER)
87 tag_strings = tag_strings.split(TAG_DELIMITER)
88 for tag_name in tag_strings:
88 for tag_name in tag_strings:
89 tag_name = string.lower(tag_name.strip())
89 tag_name = tag_name.strip().lower()
90 if len(tag_name) > 0:
90 if len(tag_name) > 0:
91 tag, created = Tag.objects.get_or_create(name=tag_name)
91 tag, created = Tag.objects.get_or_create(name=tag_name)
92 tags.append(tag)
92 tags.append(tag)
93
93
94 return tags
94 return tags
95
95
96 @transaction.atomic
96 @transaction.atomic
97 def create_thread(self, request, form, html_response=True):
97 def create_thread(self, request, form, html_response=True):
98 """
98 """
99 Creates a new thread with an opening post.
99 Creates a new thread with an opening post.
100 """
100 """
101
101
102 ip = utils.get_client_ip(request)
102 ip = utils.get_client_ip(request)
103 is_banned = Ban.objects.filter(ip=ip).exists()
103 is_banned = Ban.objects.filter(ip=ip).exists()
104
104
105 if is_banned:
105 if is_banned:
106 if html_response:
106 if html_response:
107 return redirect(BannedView().as_view())
107 return redirect(BannedView().as_view())
108 else:
108 else:
109 return
109 return
110
110
111 data = form.cleaned_data
111 data = form.cleaned_data
112
112
113 title = data[FORM_TITLE]
113 title = data[FORM_TITLE]
114 text = data[FORM_TEXT]
114 text = data[FORM_TEXT]
115
115
116 text = self._remove_invalid_links(text)
116 text = self._remove_invalid_links(text)
117
117
118 if FORM_IMAGE in data.keys():
118 if FORM_IMAGE in list(data.keys()):
119 image = data[FORM_IMAGE]
119 image = data[FORM_IMAGE]
120 else:
120 else:
121 image = None
121 image = None
122
122
123 tag_strings = data[FORM_TAGS]
123 tag_strings = data[FORM_TAGS]
124
124
125 tags = self.parse_tags_string(tag_strings)
125 tags = self.parse_tags_string(tag_strings)
126
126
127 post = Post.objects.create_post(title=title, text=text, image=image,
127 post = Post.objects.create_post(title=title, text=text, image=image,
128 ip=ip, tags=tags)
128 ip=ip, tags=tags)
129
129
130 if html_response:
130 if html_response:
131 return redirect(post.get_url())
131 return redirect(post.get_url())
132
132
133 def get_threads(self):
133 def get_threads(self):
134 """
134 """
135 Gets list of threads that will be shown on a page.
135 Gets list of threads that will be shown on a page.
136 """
136 """
137
137
138 return Thread.objects.all().order_by('-bump_time')\
138 return Thread.objects.all().order_by('-bump_time')\
139 .exclude(tags__in=self.settings_manager.get_hidden_tags())
139 .exclude(tags__in=self.settings_manager.get_hidden_tags())
@@ -1,237 +1,245 b''
1 from datetime import datetime
1 from datetime import datetime
2 import json
2 import json
3 import logging
3 import logging
4 from django.db import transaction
4 from django.db import transaction
5 from django.http import HttpResponse
5 from django.http import HttpResponse
6 from django.shortcuts import get_object_or_404, render
6 from django.shortcuts import get_object_or_404, render
7 from django.template import RequestContext
7 from django.template import RequestContext
8 from django.utils import timezone
8 from django.utils import timezone
9 from django.core import serializers
9 from django.core import serializers
10 from django.template.loader import render_to_string
10
11
11 from boards.forms import PostForm, PlainErrorList
12 from boards.forms import PostForm, PlainErrorList
12 from boards.models import Post, Thread, Tag
13 from boards.models import Post, Thread, Tag
13 from boards.utils import datetime_to_epoch
14 from boards.utils import datetime_to_epoch
14 from boards.views.thread import ThreadView
15 from boards.views.thread import ThreadView
15
16
16 __author__ = 'neko259'
17 __author__ = 'neko259'
17
18
18 PARAMETER_TRUNCATED = 'truncated'
19 PARAMETER_TRUNCATED = 'truncated'
19 PARAMETER_TAG = 'tag'
20 PARAMETER_TAG = 'tag'
20 PARAMETER_OFFSET = 'offset'
21 PARAMETER_OFFSET = 'offset'
21 PARAMETER_DIFF_TYPE = 'type'
22 PARAMETER_DIFF_TYPE = 'type'
22
23
23 DIFF_TYPE_HTML = 'html'
24 DIFF_TYPE_HTML = 'html'
24 DIFF_TYPE_JSON = 'json'
25 DIFF_TYPE_JSON = 'json'
25
26
26 STATUS_OK = 'ok'
27 STATUS_OK = 'ok'
27 STATUS_ERROR = 'error'
28 STATUS_ERROR = 'error'
28
29
29 logger = logging.getLogger(__name__)
30 logger = logging.getLogger(__name__)
30
31
31
32
32 @transaction.atomic
33 @transaction.atomic
33 def api_get_threaddiff(request, thread_id, last_update_time):
34 def api_get_threaddiff(request, thread_id, last_update_time):
34 """
35 """
35 Gets posts that were changed or added since time
36 Gets posts that were changed or added since time
36 """
37 """
37
38
38 thread = get_object_or_404(Post, id=thread_id).get_thread()
39 thread = get_object_or_404(Post, id=thread_id).get_thread()
39
40
40 filter_time = datetime.fromtimestamp(float(last_update_time) / 1000000,
41 filter_time = datetime.fromtimestamp(float(last_update_time) / 1000000,
41 timezone.get_current_timezone())
42 timezone.get_current_timezone())
42
43
43 json_data = {
44 json_data = {
44 'added': [],
45 'added': [],
45 'updated': [],
46 'updated': [],
46 'last_update': None,
47 'last_update': None,
47 }
48 }
48 added_posts = Post.objects.filter(thread_new=thread,
49 added_posts = Post.objects.filter(thread_new=thread,
49 pub_time__gt=filter_time) \
50 pub_time__gt=filter_time) \
50 .order_by('pub_time')
51 .order_by('pub_time')
51 updated_posts = Post.objects.filter(thread_new=thread,
52 updated_posts = Post.objects.filter(thread_new=thread,
52 pub_time__lte=filter_time,
53 pub_time__lte=filter_time,
53 last_edit_time__gt=filter_time)
54 last_edit_time__gt=filter_time)
54
55
55 diff_type = DIFF_TYPE_HTML
56 diff_type = DIFF_TYPE_HTML
56 if PARAMETER_DIFF_TYPE in request.GET:
57 if PARAMETER_DIFF_TYPE in request.GET:
57 diff_type = request.GET[PARAMETER_DIFF_TYPE]
58 diff_type = request.GET[PARAMETER_DIFF_TYPE]
58
59
59 for post in added_posts:
60 for post in added_posts:
60 json_data['added'].append(_get_post_data(post.id, diff_type, request))
61 json_data['added'].append(_get_post_data(post.id, diff_type, request))
61 for post in updated_posts:
62 for post in updated_posts:
62 json_data['updated'].append(_get_post_data(post.id, diff_type, request))
63 json_data['updated'].append(_get_post_data(post.id, diff_type, request))
63 json_data['last_update'] = datetime_to_epoch(thread.last_edit_time)
64 json_data['last_update'] = datetime_to_epoch(thread.last_edit_time)
64
65
65 return HttpResponse(content=json.dumps(json_data))
66 return HttpResponse(content=json.dumps(json_data))
66
67
67
68
68 def api_add_post(request, opening_post_id):
69 def api_add_post(request, opening_post_id):
69 """
70 """
70 Adds a post and return the JSON response for it
71 Adds a post and return the JSON response for it
71 """
72 """
72
73
73 opening_post = get_object_or_404(Post, id=opening_post_id)
74 opening_post = get_object_or_404(Post, id=opening_post_id)
74
75
75 logger.info('Adding post via api...')
76 logger.info('Adding post via api...')
76
77
77 status = STATUS_OK
78 status = STATUS_OK
78 errors = []
79 errors = []
79
80
80 if request.method == 'POST':
81 if request.method == 'POST':
81 form = PostForm(request.POST, request.FILES, error_class=PlainErrorList)
82 form = PostForm(request.POST, request.FILES, error_class=PlainErrorList)
82 form.session = request.session
83 form.session = request.session
83
84
84 if form.need_to_ban:
85 if form.need_to_ban:
85 # Ban user because he is suspected to be a bot
86 # Ban user because he is suspected to be a bot
86 # _ban_current_user(request)
87 # _ban_current_user(request)
87 status = STATUS_ERROR
88 status = STATUS_ERROR
88 if form.is_valid():
89 if form.is_valid():
89 post = ThreadView().new_post(request, form, opening_post,
90 post = ThreadView().new_post(request, form, opening_post,
90 html_response=False)
91 html_response=False)
91 if not post:
92 if not post:
92 status = STATUS_ERROR
93 status = STATUS_ERROR
93 else:
94 else:
94 logger.info('Added post #%d via api.' % post.id)
95 logger.info('Added post #%d via api.' % post.id)
95 else:
96 else:
96 status = STATUS_ERROR
97 status = STATUS_ERROR
97 errors = form.as_json_errors()
98 errors = form.as_json_errors()
98
99
99 response = {
100 response = {
100 'status': status,
101 'status': status,
101 'errors': errors,
102 'errors': errors,
102 }
103 }
103
104
104 return HttpResponse(content=json.dumps(response))
105 return HttpResponse(content=json.dumps(response))
105
106
106
107
107 def get_post(request, post_id):
108 def get_post(request, post_id):
108 """
109 """
109 Gets the html of a post. Used for popups. Post can be truncated if used
110 Gets the html of a post. Used for popups. Post can be truncated if used
110 in threads list with 'truncated' get parameter.
111 in threads list with 'truncated' get parameter.
111 """
112 """
112
113
113 logger.info('Getting post #%s' % post_id)
114 logger.info('Getting post #%s' % post_id)
114
115
115 post = get_object_or_404(Post, id=post_id)
116 post = get_object_or_404(Post, id=post_id)
116
117
117 context = RequestContext(request)
118 context = RequestContext(request)
118 context['post'] = post
119 context['post'] = post
119 if PARAMETER_TRUNCATED in request.GET:
120 if PARAMETER_TRUNCATED in request.GET:
120 context[PARAMETER_TRUNCATED] = True
121 context[PARAMETER_TRUNCATED] = True
121
122
122 return render(request, 'boards/api_post.html', context)
123 return render(request, 'boards/api_post.html', context)
123
124
124
125
125 # TODO Test this
126 # TODO Test this
126 def api_get_threads(request, count):
127 def api_get_threads(request, count):
127 """
128 """
128 Gets the JSON thread opening posts list.
129 Gets the JSON thread opening posts list.
129 Parameters that can be used for filtering:
130 Parameters that can be used for filtering:
130 tag, offset (from which thread to get results)
131 tag, offset (from which thread to get results)
131 """
132 """
132
133
133 if PARAMETER_TAG in request.GET:
134 if PARAMETER_TAG in request.GET:
134 tag_name = request.GET[PARAMETER_TAG]
135 tag_name = request.GET[PARAMETER_TAG]
135 if tag_name is not None:
136 if tag_name is not None:
136 tag = get_object_or_404(Tag, name=tag_name)
137 tag = get_object_or_404(Tag, name=tag_name)
137 threads = tag.threads.filter(archived=False)
138 threads = tag.threads.filter(archived=False)
138 else:
139 else:
139 threads = Thread.objects.filter(archived=False)
140 threads = Thread.objects.filter(archived=False)
140
141
141 if PARAMETER_OFFSET in request.GET:
142 if PARAMETER_OFFSET in request.GET:
142 offset = request.GET[PARAMETER_OFFSET]
143 offset = request.GET[PARAMETER_OFFSET]
143 offset = int(offset) if offset is not None else 0
144 offset = int(offset) if offset is not None else 0
144 else:
145 else:
145 offset = 0
146 offset = 0
146
147
147 threads = threads.order_by('-bump_time')
148 threads = threads.order_by('-bump_time')
148 threads = threads[offset:offset + int(count)]
149 threads = threads[offset:offset + int(count)]
149
150
150 opening_posts = []
151 opening_posts = []
151 for thread in threads:
152 for thread in threads:
152 opening_post = thread.get_opening_post()
153 opening_post = thread.get_opening_post()
153
154
154 # TODO Add tags, replies and images count
155 # TODO Add tags, replies and images count
155 opening_posts.append(_get_post_data(opening_post.id,
156 opening_posts.append(_get_post_data(opening_post.id,
156 include_last_update=True))
157 include_last_update=True))
157
158
158 return HttpResponse(content=json.dumps(opening_posts))
159 return HttpResponse(content=json.dumps(opening_posts))
159
160
160
161
161 # TODO Test this
162 # TODO Test this
162 def api_get_tags(request):
163 def api_get_tags(request):
163 """
164 """
164 Gets all tags or user tags.
165 Gets all tags or user tags.
165 """
166 """
166
167
167 # TODO Get favorite tags for the given user ID
168 # TODO Get favorite tags for the given user ID
168
169
169 tags = Tag.objects.get_not_empty_tags()
170 tags = Tag.objects.get_not_empty_tags()
170 tag_names = []
171 tag_names = []
171 for tag in tags:
172 for tag in tags:
172 tag_names.append(tag.name)
173 tag_names.append(tag.name)
173
174
174 return HttpResponse(content=json.dumps(tag_names))
175 return HttpResponse(content=json.dumps(tag_names))
175
176
176
177
177 # TODO The result can be cached by the thread last update time
178 # TODO The result can be cached by the thread last update time
178 # TODO Test this
179 # TODO Test this
179 def api_get_thread_posts(request, opening_post_id):
180 def api_get_thread_posts(request, opening_post_id):
180 """
181 """
181 Gets the JSON array of thread posts
182 Gets the JSON array of thread posts
182 """
183 """
183
184
184 opening_post = get_object_or_404(Post, id=opening_post_id)
185 opening_post = get_object_or_404(Post, id=opening_post_id)
185 thread = opening_post.get_thread()
186 thread = opening_post.get_thread()
186 posts = thread.get_replies()
187 posts = thread.get_replies()
187
188
188 json_data = {
189 json_data = {
189 'posts': [],
190 'posts': [],
190 'last_update': None,
191 'last_update': None,
191 }
192 }
192 json_post_list = []
193 json_post_list = []
193
194
194 for post in posts:
195 for post in posts:
195 json_post_list.append(_get_post_data(post.id))
196 json_post_list.append(_get_post_data(post.id))
196 json_data['last_update'] = datetime_to_epoch(thread.last_edit_time)
197 json_data['last_update'] = datetime_to_epoch(thread.last_edit_time)
197 json_data['posts'] = json_post_list
198 json_data['posts'] = json_post_list
198
199
199 return HttpResponse(content=json.dumps(json_data))
200 return HttpResponse(content=json.dumps(json_data))
200
201
201
202
202 def api_get_post(request, post_id):
203 def api_get_post(request, post_id):
203 """
204 """
204 Gets the JSON of a post. This can be
205 Gets the JSON of a post. This can be
205 used as and API for external clients.
206 used as and API for external clients.
206 """
207 """
207
208
208 post = get_object_or_404(Post, id=post_id)
209 post = get_object_or_404(Post, id=post_id)
209
210
210 json = serializers.serialize("json", [post], fields=(
211 json = serializers.serialize("json", [post], fields=(
211 "pub_time", "_text_rendered", "title", "text", "image",
212 "pub_time", "_text_rendered", "title", "text", "image",
212 "image_width", "image_height", "replies", "tags"
213 "image_width", "image_height", "replies", "tags"
213 ))
214 ))
214
215
215 return HttpResponse(content=json)
216 return HttpResponse(content=json)
216
217
217
218
218 # TODO Add pub time and replies
219 # TODO Add pub time and replies
219 def _get_post_data(post_id, format_type=DIFF_TYPE_JSON, request=None,
220 def _get_post_data(post_id, format_type=DIFF_TYPE_JSON, request=None,
220 include_last_update=False):
221 include_last_update=False):
221 if format_type == DIFF_TYPE_HTML:
222 if format_type == DIFF_TYPE_HTML:
222 return get_post(request, post_id).content.strip()
223 post = get_object_or_404(Post, id=post_id)
224
225 context = RequestContext(request)
226 context['post'] = post
227 if PARAMETER_TRUNCATED in request.GET:
228 context[PARAMETER_TRUNCATED] = True
229
230 return render_to_string('boards/api_post.html', context)
223 elif format_type == DIFF_TYPE_JSON:
231 elif format_type == DIFF_TYPE_JSON:
224 post = get_object_or_404(Post, id=post_id)
232 post = get_object_or_404(Post, id=post_id)
225 post_json = {
233 post_json = {
226 'id': post.id,
234 'id': post.id,
227 'title': post.title,
235 'title': post.title,
228 'text': post.text.rendered,
236 'text': post.text.rendered,
229 }
237 }
230 if post.images.exists():
238 if post.images.exists():
231 post_image = post.get_first_image()
239 post_image = post.get_first_image()
232 post_json['image'] = post_image.image.url
240 post_json['image'] = post_image.image.url
233 post_json['image_preview'] = post_image.image.url_200x150
241 post_json['image_preview'] = post_image.image.url_200x150
234 if include_last_update:
242 if include_last_update:
235 post_json['bump_time'] = datetime_to_epoch(
243 post_json['bump_time'] = datetime_to_epoch(
236 post.thread_new.bump_time)
244 post.thread_new.bump_time)
237 return post_json
245 return post_json
@@ -1,142 +1,142 b''
1 from django.core.urlresolvers import reverse
1 from django.core.urlresolvers import reverse
2 from django.db import transaction
2 from django.db import transaction
3 from django.http import Http404
3 from django.http import Http404
4 from django.shortcuts import get_object_or_404, render, redirect
4 from django.shortcuts import get_object_or_404, render, redirect
5 from django.views.generic.edit import FormMixin
5 from django.views.generic.edit import FormMixin
6
6
7 from boards import utils, settings
7 from boards import utils, settings
8 from boards.forms import PostForm, PlainErrorList
8 from boards.forms import PostForm, PlainErrorList
9 from boards.models import Post, Ban
9 from boards.models import Post, Ban
10 from boards.views.banned import BannedView
10 from boards.views.banned import BannedView
11 from boards.views.base import BaseBoardView, CONTEXT_FORM
11 from boards.views.base import BaseBoardView, CONTEXT_FORM
12 from boards.views.posting_mixin import PostMixin
12 from boards.views.posting_mixin import PostMixin
13
13
14 TEMPLATE_GALLERY = 'boards/thread_gallery.html'
14 TEMPLATE_GALLERY = 'boards/thread_gallery.html'
15 TEMPLATE_NORMAL = 'boards/thread.html'
15 TEMPLATE_NORMAL = 'boards/thread.html'
16
16
17 CONTEXT_POSTS = 'posts'
17 CONTEXT_POSTS = 'posts'
18 CONTEXT_OP = 'opening_post'
18 CONTEXT_OP = 'opening_post'
19 CONTEXT_BUMPLIMIT_PRG = 'bumplimit_progress'
19 CONTEXT_BUMPLIMIT_PRG = 'bumplimit_progress'
20 CONTEXT_POSTS_LEFT = 'posts_left'
20 CONTEXT_POSTS_LEFT = 'posts_left'
21 CONTEXT_LASTUPDATE = "last_update"
21 CONTEXT_LASTUPDATE = "last_update"
22 CONTEXT_MAX_REPLIES = 'max_replies'
22 CONTEXT_MAX_REPLIES = 'max_replies'
23 CONTEXT_THREAD = 'thread'
23 CONTEXT_THREAD = 'thread'
24 CONTEXT_BUMPABLE = 'bumpable'
24 CONTEXT_BUMPABLE = 'bumpable'
25
25
26 FORM_TITLE = 'title'
26 FORM_TITLE = 'title'
27 FORM_TEXT = 'text'
27 FORM_TEXT = 'text'
28 FORM_IMAGE = 'image'
28 FORM_IMAGE = 'image'
29
29
30 MODE_GALLERY = 'gallery'
30 MODE_GALLERY = 'gallery'
31 MODE_NORMAL = 'normal'
31 MODE_NORMAL = 'normal'
32
32
33
33
34 class ThreadView(BaseBoardView, PostMixin, FormMixin):
34 class ThreadView(BaseBoardView, PostMixin, FormMixin):
35
35
36 def get(self, request, post_id, mode=MODE_NORMAL, form=None):
36 def get(self, request, post_id, mode=MODE_NORMAL, form=None):
37 try:
37 try:
38 opening_post = Post.objects.filter(id=post_id).only('thread_new')[0]
38 opening_post = Post.objects.filter(id=post_id).only('thread_new')[0]
39 except IndexError:
39 except IndexError:
40 raise Http404
40 raise Http404
41
41
42 # If this is not OP, don't show it as it is
42 # If this is not OP, don't show it as it is
43 if not opening_post or not opening_post.is_opening():
43 if not opening_post or not opening_post.is_opening():
44 raise Http404
44 raise Http404
45
45
46 if not form:
46 if not form:
47 form = PostForm(error_class=PlainErrorList)
47 form = PostForm(error_class=PlainErrorList)
48
48
49 thread_to_show = opening_post.get_thread()
49 thread_to_show = opening_post.get_thread()
50
50
51 context = self.get_context_data(request=request)
51 context = self.get_context_data(request=request)
52
52
53 context[CONTEXT_FORM] = form
53 context[CONTEXT_FORM] = form
54 context[CONTEXT_LASTUPDATE] = utils.datetime_to_epoch(
54 context[CONTEXT_LASTUPDATE] = utils.datetime_to_epoch(
55 thread_to_show.last_edit_time)
55 thread_to_show.last_edit_time)
56 context[CONTEXT_THREAD] = thread_to_show
56 context[CONTEXT_THREAD] = thread_to_show
57 context[CONTEXT_MAX_REPLIES] = settings.MAX_POSTS_PER_THREAD
57 context[CONTEXT_MAX_REPLIES] = settings.MAX_POSTS_PER_THREAD
58
58
59 if MODE_NORMAL == mode:
59 if MODE_NORMAL == mode:
60 bumpable = thread_to_show.can_bump()
60 bumpable = thread_to_show.can_bump()
61 context[CONTEXT_BUMPABLE] = bumpable
61 context[CONTEXT_BUMPABLE] = bumpable
62 if bumpable:
62 if bumpable:
63 left_posts = settings.MAX_POSTS_PER_THREAD \
63 left_posts = settings.MAX_POSTS_PER_THREAD \
64 - thread_to_show.get_reply_count()
64 - thread_to_show.get_reply_count()
65 context[CONTEXT_POSTS_LEFT] = left_posts
65 context[CONTEXT_POSTS_LEFT] = left_posts
66 context[CONTEXT_BUMPLIMIT_PRG] = str(
66 context[CONTEXT_BUMPLIMIT_PRG] = str(
67 float(left_posts) / settings.MAX_POSTS_PER_THREAD * 100)
67 float(left_posts) / settings.MAX_POSTS_PER_THREAD * 100)
68
68
69 context[CONTEXT_OP] = opening_post
69 context[CONTEXT_OP] = opening_post
70
70
71 document = TEMPLATE_NORMAL
71 document = TEMPLATE_NORMAL
72 elif MODE_GALLERY == mode:
72 elif MODE_GALLERY == mode:
73 context[CONTEXT_POSTS] = thread_to_show.get_replies_with_images(
73 context[CONTEXT_POSTS] = thread_to_show.get_replies_with_images(
74 view_fields_only=True)
74 view_fields_only=True)
75
75
76 document = TEMPLATE_GALLERY
76 document = TEMPLATE_GALLERY
77 else:
77 else:
78 raise Http404
78 raise Http404
79
79
80 return render(request, document, context)
80 return render(request, document, context)
81
81
82 def post(self, request, post_id, mode=MODE_NORMAL):
82 def post(self, request, post_id, mode=MODE_NORMAL):
83 opening_post = get_object_or_404(Post, id=post_id)
83 opening_post = get_object_or_404(Post, id=post_id)
84
84
85 # If this is not OP, don't show it as it is
85 # If this is not OP, don't show it as it is
86 if not opening_post.is_opening():
86 if not opening_post.is_opening():
87 raise Http404
87 raise Http404
88
88
89 if not opening_post.get_thread().archived:
89 if not opening_post.get_thread().archived:
90 form = PostForm(request.POST, request.FILES,
90 form = PostForm(request.POST, request.FILES,
91 error_class=PlainErrorList)
91 error_class=PlainErrorList)
92 form.session = request.session
92 form.session = request.session
93
93
94 if form.is_valid():
94 if form.is_valid():
95 return self.new_post(request, form, opening_post)
95 return self.new_post(request, form, opening_post)
96 if form.need_to_ban:
96 if form.need_to_ban:
97 # Ban user because he is suspected to be a bot
97 # Ban user because he is suspected to be a bot
98 self._ban_current_user(request)
98 self._ban_current_user(request)
99
99
100 return self.get(request, post_id, mode, form)
100 return self.get(request, post_id, mode, form)
101
101
102 @transaction.atomic
102 @transaction.atomic
103 def new_post(self, request, form, opening_post=None, html_response=True):
103 def new_post(self, request, form, opening_post=None, html_response=True):
104 """Add a new post (in thread or as a reply)."""
104 """Add a new post (in thread or as a reply)."""
105
105
106 ip = utils.get_client_ip(request)
106 ip = utils.get_client_ip(request)
107 is_banned = Ban.objects.filter(ip=ip).exists()
107 is_banned = Ban.objects.filter(ip=ip).exists()
108
108
109 if is_banned:
109 if is_banned:
110 if html_response:
110 if html_response:
111 return redirect(BannedView().as_view())
111 return redirect(BannedView().as_view())
112 else:
112 else:
113 return None
113 return None
114
114
115 data = form.cleaned_data
115 data = form.cleaned_data
116
116
117 title = data[FORM_TITLE]
117 title = data[FORM_TITLE]
118 text = data[FORM_TEXT]
118 text = data[FORM_TEXT]
119
119
120 text = self._remove_invalid_links(text)
120 text = self._remove_invalid_links(text)
121
121
122 if FORM_IMAGE in data.keys():
122 if FORM_IMAGE in list(data.keys()):
123 image = data[FORM_IMAGE]
123 image = data[FORM_IMAGE]
124 else:
124 else:
125 image = None
125 image = None
126
126
127 tags = []
127 tags = []
128
128
129 post_thread = opening_post.get_thread()
129 post_thread = opening_post.get_thread()
130
130
131 post = Post.objects.create_post(title=title, text=text, image=image,
131 post = Post.objects.create_post(title=title, text=text, image=image,
132 thread=post_thread, ip=ip, tags=tags)
132 thread=post_thread, ip=ip, tags=tags)
133
133
134 thread_to_show = (opening_post.id if opening_post else post.id)
134 thread_to_show = (opening_post.id if opening_post else post.id)
135
135
136 if html_response:
136 if html_response:
137 if opening_post:
137 if opening_post:
138 return redirect(
138 return redirect(
139 reverse('thread', kwargs={'post_id': thread_to_show})
139 reverse('thread', kwargs={'post_id': thread_to_show})
140 + '#' + str(post.id))
140 + '#' + str(post.id))
141 else:
141 else:
142 return post
142 return post
@@ -1,1 +0,0 b''
1 import settings No newline at end of file
@@ -1,260 +1,250 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',
152
153 # Search
151 # Search
154 'haystack',
152 'haystack',
155
153
156 'boards',
154 'boards',
157 )
155 )
158
156
159 DEBUG_TOOLBAR_PANELS = (
157 DEBUG_TOOLBAR_PANELS = (
160 'debug_toolbar.panels.version.VersionDebugPanel',
158 'debug_toolbar.panels.version.VersionDebugPanel',
161 'debug_toolbar.panels.timer.TimerDebugPanel',
159 'debug_toolbar.panels.timer.TimerDebugPanel',
162 'debug_toolbar.panels.settings_vars.SettingsVarsDebugPanel',
160 'debug_toolbar.panels.settings_vars.SettingsVarsDebugPanel',
163 'debug_toolbar.panels.headers.HeaderDebugPanel',
161 'debug_toolbar.panels.headers.HeaderDebugPanel',
164 'debug_toolbar.panels.request_vars.RequestVarsDebugPanel',
162 'debug_toolbar.panels.request_vars.RequestVarsDebugPanel',
165 'debug_toolbar.panels.template.TemplateDebugPanel',
163 'debug_toolbar.panels.template.TemplateDebugPanel',
166 'debug_toolbar.panels.sql.SQLDebugPanel',
164 'debug_toolbar.panels.sql.SQLDebugPanel',
167 'debug_toolbar.panels.signals.SignalDebugPanel',
165 'debug_toolbar.panels.signals.SignalDebugPanel',
168 'debug_toolbar.panels.logger.LoggingPanel',
166 'debug_toolbar.panels.logger.LoggingPanel',
169 )
167 )
170
168
171 # TODO: NEED DESIGN FIXES
172 CAPTCHA_OUTPUT_FORMAT = (u' %(hidden_field)s '
173 u'<div class="form-label">%(image)s</div>'
174 u'<div class="form-text">%(text_field)s</div>')
175
176 # A sample logging configuration. The only tangible logging
169 # A sample logging configuration. The only tangible logging
177 # performed by this configuration is to send an email to
170 # performed by this configuration is to send an email to
178 # the site admins on every HTTP 500 error when DEBUG=False.
171 # the site admins on every HTTP 500 error when DEBUG=False.
179 # See http://docs.djangoproject.com/en/dev/topics/logging for
172 # See http://docs.djangoproject.com/en/dev/topics/logging for
180 # more details on how to customize your logging configuration.
173 # more details on how to customize your logging configuration.
181 LOGGING = {
174 LOGGING = {
182 'version': 1,
175 'version': 1,
183 'disable_existing_loggers': False,
176 'disable_existing_loggers': False,
184 'formatters': {
177 'formatters': {
185 'verbose': {
178 'verbose': {
186 'format': '%(levelname)s %(asctime)s %(module)s %(process)d %(thread)d %(message)s'
179 'format': '%(levelname)s %(asctime)s %(module)s %(process)d %(thread)d %(message)s'
187 },
180 },
188 'simple': {
181 'simple': {
189 'format': '%(levelname)s %(asctime)s [%(module)s] %(message)s'
182 'format': '%(levelname)s %(asctime)s [%(module)s] %(message)s'
190 },
183 },
191 },
184 },
192 'filters': {
185 'filters': {
193 'require_debug_false': {
186 'require_debug_false': {
194 '()': 'django.utils.log.RequireDebugFalse'
187 '()': 'django.utils.log.RequireDebugFalse'
195 }
188 }
196 },
189 },
197 'handlers': {
190 'handlers': {
198 'console': {
191 'console': {
199 'level': 'DEBUG',
192 'level': 'DEBUG',
200 'class': 'logging.StreamHandler',
193 'class': 'logging.StreamHandler',
201 'formatter': 'simple'
194 'formatter': 'simple'
202 },
195 },
203 },
196 },
204 'loggers': {
197 'loggers': {
205 'boards': {
198 'boards': {
206 'handlers': ['console'],
199 'handlers': ['console'],
207 'level': 'DEBUG',
200 'level': 'DEBUG',
208 }
201 }
209 },
202 },
210 }
203 }
211
204
212 HAYSTACK_CONNECTIONS = {
205 HAYSTACK_CONNECTIONS = {
213 'default': {
206 'default': {
214 'ENGINE': 'haystack.backends.whoosh_backend.WhooshEngine',
207 'ENGINE': 'haystack.backends.whoosh_backend.WhooshEngine',
215 'PATH': os.path.join(os.path.dirname(__file__), 'whoosh_index'),
208 'PATH': os.path.join(os.path.dirname(__file__), 'whoosh_index'),
216 },
209 },
217 }
210 }
218
211
219 MARKUP_FIELD_TYPES = (
212 MARKUP_FIELD_TYPES = (
220 ('bbcode', bbcode_extended),
213 ('bbcode', bbcode_extended),
221 )
214 )
222
215
223 THEMES = [
216 THEMES = [
224 ('md', 'Mystic Dark'),
217 ('md', 'Mystic Dark'),
225 ('md_centered', 'Mystic Dark (centered)'),
218 ('md_centered', 'Mystic Dark (centered)'),
226 ('sw', 'Snow White'),
219 ('sw', 'Snow White'),
227 ('pg', 'Photon Gray'),
220 ('pg', 'Photon Gray'),
228 ]
221 ]
229
222
230 POPULAR_TAGS = 10
223 POPULAR_TAGS = 10
231
224
232 ENABLE_CAPTCHA = False
233 # if user tries to post before CAPTCHA_DEFAULT_SAFE_TIME. Captcha will be shown
234 CAPTCHA_DEFAULT_SAFE_TIME = 30 # seconds
235 POSTING_DELAY = 20 # seconds
225 POSTING_DELAY = 20 # seconds
236
226
237 COMPRESS_HTML = True
227 COMPRESS_HTML = True
238
228
239 # Debug mode middlewares
229 # Debug mode middlewares
240 if DEBUG:
230 if DEBUG:
241 MIDDLEWARE_CLASSES += (
231 MIDDLEWARE_CLASSES += (
242 'boards.profiler.ProfilerMiddleware',
232 #'boards.profiler.ProfilerMiddleware',
243 'debug_toolbar.middleware.DebugToolbarMiddleware',
233 'debug_toolbar.middleware.DebugToolbarMiddleware',
244 )
234 )
245
235
246 def custom_show_toolbar(request):
236 def custom_show_toolbar(request):
247 return DEBUG
237 return DEBUG
248
238
249 DEBUG_TOOLBAR_CONFIG = {
239 DEBUG_TOOLBAR_CONFIG = {
250 'INTERCEPT_REDIRECTS': False,
240 'INTERCEPT_REDIRECTS': False,
251 'SHOW_TOOLBAR_CALLBACK': custom_show_toolbar,
241 'SHOW_TOOLBAR_CALLBACK': custom_show_toolbar,
252 'HIDE_DJANGO_SQL': False,
242 'HIDE_DJANGO_SQL': False,
253 'ENABLE_STACKTRACES': True,
243 'ENABLE_STACKTRACES': True,
254 }
244 }
255
245
256 # FIXME Uncommenting this fails somehow. Need to investigate this
246 # FIXME Uncommenting this fails somehow. Need to investigate this
257 #DEBUG_TOOLBAR_PANELS += (
247 #DEBUG_TOOLBAR_PANELS += (
258 # 'debug_toolbar.panels.profiling.ProfilingDebugPanel',
248 # 'debug_toolbar.panels.profiling.ProfilingDebugPanel',
259 #)
249 #)
260
250
@@ -1,10 +1,7 b''
1 south>=0.8.4
1 south>=0.8.4
2 line_profiler
3 haystack
2 haystack
4 pillow
3 pillow
5 django>=1.6
4 django>=1.6
6 django_cleanup
5 django_cleanup
7 django-markupfield
6 django-markupfield
8 django-simple-captcha
9 line-profiler
10 bbcode
7 bbcode
General Comments 0
You need to be logged in to leave comments. Login now