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