##// END OF EJS Templates
Added image duplicate check
neko259 -
r527:5cd89122 default
parent child Browse files
Show More
@@ -0,0 +1,86 b''
1 # -*- coding: utf-8 -*-
2 from south.utils import datetime_utils as datetime
3 from south.db import db
4 from south.v2 import SchemaMigration
5 from django.db import models
6
7
8 class Migration(SchemaMigration):
9
10 def forwards(self, orm):
11 # Adding field 'Post.image_hash'
12 db.add_column(u'boards_post', 'image_hash',
13 self.gf('django.db.models.fields.CharField')(default='', max_length=36),
14 keep_default=False)
15
16
17 def backwards(self, orm):
18 # Deleting field 'Post.image_hash'
19 db.delete_column(u'boards_post', 'image_hash')
20
21
22 models = {
23 'boards.ban': {
24 'Meta': {'object_name': 'Ban'},
25 'can_read': ('django.db.models.fields.BooleanField', [], {'default': 'True'}),
26 u'id': ('django.db.models.fields.AutoField', [], {'primary_key': 'True'}),
27 'ip': ('django.db.models.fields.GenericIPAddressField', [], {'max_length': '39'}),
28 'reason': ('django.db.models.fields.CharField', [], {'default': "'Auto'", 'max_length': '200'})
29 },
30 'boards.post': {
31 'Meta': {'object_name': 'Post'},
32 '_text_rendered': ('django.db.models.fields.TextField', [], {}),
33 u'id': ('django.db.models.fields.AutoField', [], {'primary_key': 'True'}),
34 'image': ('boards.thumbs.ImageWithThumbsField', [], {'max_length': '100', 'blank': 'True'}),
35 'image_hash': ('django.db.models.fields.CharField', [], {'max_length': '36'}),
36 'image_height': ('django.db.models.fields.IntegerField', [], {'default': '0'}),
37 'image_pre_height': ('django.db.models.fields.IntegerField', [], {'default': '0'}),
38 'image_pre_width': ('django.db.models.fields.IntegerField', [], {'default': '0'}),
39 'image_width': ('django.db.models.fields.IntegerField', [], {'default': '0'}),
40 'last_edit_time': ('django.db.models.fields.DateTimeField', [], {}),
41 'poster_ip': ('django.db.models.fields.GenericIPAddressField', [], {'max_length': '39'}),
42 'poster_user_agent': ('django.db.models.fields.TextField', [], {}),
43 'pub_time': ('django.db.models.fields.DateTimeField', [], {}),
44 'referenced_posts': ('django.db.models.fields.related.ManyToManyField', [], {'blank': 'True', 'related_name': "'rfp+'", 'null': 'True', 'symmetrical': 'False', 'to': "orm['boards.Post']"}),
45 'text': ('markupfield.fields.MarkupField', [], {'rendered_field': 'True'}),
46 'text_markup_type': ('django.db.models.fields.CharField', [], {'default': "'markdown'", 'max_length': '30'}),
47 'thread': ('django.db.models.fields.related.ForeignKey', [], {'default': 'None', 'to': "orm['boards.Post']", 'null': 'True'}),
48 'thread_new': ('django.db.models.fields.related.ForeignKey', [], {'default': 'None', 'to': "orm['boards.Thread']", 'null': 'True'}),
49 'title': ('django.db.models.fields.CharField', [], {'max_length': '50'}),
50 'user': ('django.db.models.fields.related.ForeignKey', [], {'default': 'None', 'to': "orm['boards.User']", 'null': 'True'})
51 },
52 'boards.setting': {
53 'Meta': {'object_name': 'Setting'},
54 u'id': ('django.db.models.fields.AutoField', [], {'primary_key': 'True'}),
55 'name': ('django.db.models.fields.CharField', [], {'max_length': '50'}),
56 'user': ('django.db.models.fields.related.ForeignKey', [], {'to': "orm['boards.User']"}),
57 'value': ('django.db.models.fields.CharField', [], {'max_length': '50'})
58 },
59 'boards.tag': {
60 'Meta': {'object_name': 'Tag'},
61 u'id': ('django.db.models.fields.AutoField', [], {'primary_key': 'True'}),
62 'linked': ('django.db.models.fields.related.ForeignKey', [], {'to': "orm['boards.Tag']", 'null': 'True', 'blank': 'True'}),
63 'name': ('django.db.models.fields.CharField', [], {'max_length': '100'}),
64 'threads': ('django.db.models.fields.related.ManyToManyField', [], {'blank': 'True', 'related_name': "'tag+'", 'null': 'True', 'symmetrical': 'False', 'to': "orm['boards.Thread']"})
65 },
66 'boards.thread': {
67 'Meta': {'object_name': 'Thread'},
68 'archived': ('django.db.models.fields.BooleanField', [], {'default': 'False'}),
69 'bump_time': ('django.db.models.fields.DateTimeField', [], {}),
70 u'id': ('django.db.models.fields.AutoField', [], {'primary_key': 'True'}),
71 'last_edit_time': ('django.db.models.fields.DateTimeField', [], {}),
72 'replies': ('django.db.models.fields.related.ManyToManyField', [], {'blank': 'True', 'related_name': "'tre+'", 'null': 'True', 'symmetrical': 'False', 'to': "orm['boards.Post']"}),
73 'tags': ('django.db.models.fields.related.ManyToManyField', [], {'to': "orm['boards.Tag']", 'symmetrical': 'False'})
74 },
75 'boards.user': {
76 'Meta': {'object_name': 'User'},
77 'fav_tags': ('django.db.models.fields.related.ManyToManyField', [], {'symmetrical': 'False', 'to': "orm['boards.Tag']", 'null': 'True', 'blank': 'True'}),
78 'fav_threads': ('django.db.models.fields.related.ManyToManyField', [], {'blank': 'True', 'related_name': "'+'", 'null': 'True', 'symmetrical': 'False', 'to': "orm['boards.Post']"}),
79 u'id': ('django.db.models.fields.AutoField', [], {'primary_key': 'True'}),
80 'rank': ('django.db.models.fields.IntegerField', [], {}),
81 'registration_time': ('django.db.models.fields.DateTimeField', [], {}),
82 'user_id': ('django.db.models.fields.CharField', [], {'max_length': '50'})
83 }
84 }
85
86 complete_apps = ['boards'] No newline at end of file
@@ -0,0 +1,91 b''
1 # -*- coding: utf-8 -*-
2 import hashlib
3
4 from south.utils import datetime_utils as datetime
5 from south.db import db
6 from south.v2 import DataMigration
7 from django.db import models
8
9 class Migration(DataMigration):
10
11 def forwards(self, orm):
12 # Note: Don't use "from appname.models import ModelName".
13 # Use orm.ModelName to refer to models in this application,
14 # and orm['appname.ModelName'] for models in other applications.
15 for post in orm.Post.objects.filter(image_width__gt=0):
16 md5 = hashlib.md5()
17 for chunk in post.image.chunks():
18 md5.update(chunk)
19 image_hash = md5.hexdigest()
20 post.image_hash = image_hash
21 post.save()
22
23 def backwards(self, orm):
24 "Write your backwards methods here."
25
26 models = {
27 'boards.ban': {
28 'Meta': {'object_name': 'Ban'},
29 'can_read': ('django.db.models.fields.BooleanField', [], {'default': 'True'}),
30 u'id': ('django.db.models.fields.AutoField', [], {'primary_key': 'True'}),
31 'ip': ('django.db.models.fields.GenericIPAddressField', [], {'max_length': '39'}),
32 'reason': ('django.db.models.fields.CharField', [], {'default': "'Auto'", 'max_length': '200'})
33 },
34 'boards.post': {
35 'Meta': {'object_name': 'Post'},
36 '_text_rendered': ('django.db.models.fields.TextField', [], {}),
37 u'id': ('django.db.models.fields.AutoField', [], {'primary_key': 'True'}),
38 'image': ('boards.thumbs.ImageWithThumbsField', [], {'max_length': '100', 'blank': 'True'}),
39 'image_hash': ('django.db.models.fields.CharField', [], {'max_length': '36'}),
40 'image_height': ('django.db.models.fields.IntegerField', [], {'default': '0'}),
41 'image_pre_height': ('django.db.models.fields.IntegerField', [], {'default': '0'}),
42 'image_pre_width': ('django.db.models.fields.IntegerField', [], {'default': '0'}),
43 'image_width': ('django.db.models.fields.IntegerField', [], {'default': '0'}),
44 'last_edit_time': ('django.db.models.fields.DateTimeField', [], {}),
45 'poster_ip': ('django.db.models.fields.GenericIPAddressField', [], {'max_length': '39'}),
46 'poster_user_agent': ('django.db.models.fields.TextField', [], {}),
47 'pub_time': ('django.db.models.fields.DateTimeField', [], {}),
48 'referenced_posts': ('django.db.models.fields.related.ManyToManyField', [], {'blank': 'True', 'related_name': "'rfp+'", 'null': 'True', 'symmetrical': 'False', 'to': "orm['boards.Post']"}),
49 'text': ('markupfield.fields.MarkupField', [], {'rendered_field': 'True'}),
50 'text_markup_type': ('django.db.models.fields.CharField', [], {'default': "'markdown'", 'max_length': '30'}),
51 'thread': ('django.db.models.fields.related.ForeignKey', [], {'default': 'None', 'to': "orm['boards.Post']", 'null': 'True'}),
52 'thread_new': ('django.db.models.fields.related.ForeignKey', [], {'default': 'None', 'to': "orm['boards.Thread']", 'null': 'True'}),
53 'title': ('django.db.models.fields.CharField', [], {'max_length': '50'}),
54 'user': ('django.db.models.fields.related.ForeignKey', [], {'default': 'None', 'to': "orm['boards.User']", 'null': 'True'})
55 },
56 'boards.setting': {
57 'Meta': {'object_name': 'Setting'},
58 u'id': ('django.db.models.fields.AutoField', [], {'primary_key': 'True'}),
59 'name': ('django.db.models.fields.CharField', [], {'max_length': '50'}),
60 'user': ('django.db.models.fields.related.ForeignKey', [], {'to': "orm['boards.User']"}),
61 'value': ('django.db.models.fields.CharField', [], {'max_length': '50'})
62 },
63 'boards.tag': {
64 'Meta': {'object_name': 'Tag'},
65 u'id': ('django.db.models.fields.AutoField', [], {'primary_key': 'True'}),
66 'linked': ('django.db.models.fields.related.ForeignKey', [], {'to': "orm['boards.Tag']", 'null': 'True', 'blank': 'True'}),
67 'name': ('django.db.models.fields.CharField', [], {'max_length': '100'}),
68 'threads': ('django.db.models.fields.related.ManyToManyField', [], {'blank': 'True', 'related_name': "'tag+'", 'null': 'True', 'symmetrical': 'False', 'to': "orm['boards.Thread']"})
69 },
70 'boards.thread': {
71 'Meta': {'object_name': 'Thread'},
72 'archived': ('django.db.models.fields.BooleanField', [], {'default': 'False'}),
73 'bump_time': ('django.db.models.fields.DateTimeField', [], {}),
74 u'id': ('django.db.models.fields.AutoField', [], {'primary_key': 'True'}),
75 'last_edit_time': ('django.db.models.fields.DateTimeField', [], {}),
76 'replies': ('django.db.models.fields.related.ManyToManyField', [], {'blank': 'True', 'related_name': "'tre+'", 'null': 'True', 'symmetrical': 'False', 'to': "orm['boards.Post']"}),
77 'tags': ('django.db.models.fields.related.ManyToManyField', [], {'to': "orm['boards.Tag']", 'symmetrical': 'False'})
78 },
79 'boards.user': {
80 'Meta': {'object_name': 'User'},
81 'fav_tags': ('django.db.models.fields.related.ManyToManyField', [], {'symmetrical': 'False', 'to': "orm['boards.Tag']", 'null': 'True', 'blank': 'True'}),
82 'fav_threads': ('django.db.models.fields.related.ManyToManyField', [], {'blank': 'True', 'related_name': "'+'", 'null': 'True', 'symmetrical': 'False', 'to': "orm['boards.Post']"}),
83 u'id': ('django.db.models.fields.AutoField', [], {'primary_key': 'True'}),
84 'rank': ('django.db.models.fields.IntegerField', [], {}),
85 'registration_time': ('django.db.models.fields.DateTimeField', [], {}),
86 'user_id': ('django.db.models.fields.CharField', [], {'max_length': '50'})
87 }
88 }
89
90 complete_apps = ['boards']
91 symmetrical = True
@@ -1,285 +1,301 b''
1 import re
1 import re
2 import time
3 import hashlib
4
2 from captcha.fields import CaptchaField
5 from captcha.fields import CaptchaField
3 from django import forms
6 from django import forms
4 from django.forms.util import ErrorList
7 from django.forms.util import ErrorList
5 from django.utils.translation import ugettext_lazy as _
8 from django.utils.translation import ugettext_lazy as _
6 import time
9
7 from boards.mdx_neboard import formatters
10 from boards.mdx_neboard import formatters
8 from boards.models.post import TITLE_MAX_LENGTH
11 from boards.models.post import TITLE_MAX_LENGTH
9 from boards.models import User
12 from boards.models import User, Post
10 from neboard import settings
13 from neboard import settings
11 from boards import utils
14 from boards import utils
12 import boards.settings as board_settings
15 import boards.settings as board_settings
13
16
14 ATTRIBUTE_PLACEHOLDER = 'placeholder'
17 ATTRIBUTE_PLACEHOLDER = 'placeholder'
15
18
16 LAST_POST_TIME = 'last_post_time'
19 LAST_POST_TIME = 'last_post_time'
17 LAST_LOGIN_TIME = 'last_login_time'
20 LAST_LOGIN_TIME = 'last_login_time'
18 TEXT_PLACEHOLDER = _('''Type message here. You can reply to message >>123 like
21 TEXT_PLACEHOLDER = _('''Type message here. You can reply to message >>123 like
19 this. 2 new lines are required to start new paragraph.''')
22 this. 2 new lines are required to start new paragraph.''')
20 TAGS_PLACEHOLDER = _('tag1 several_words_tag')
23 TAGS_PLACEHOLDER = _('tag1 several_words_tag')
21
24
25 ERROR_IMAGE_DUPLICATE = _('Such image was already posted')
26
27 LABEL_TITLE = _('Title')
28 LABEL_TEXT = _('Text')
29
22
30
23 class FormatPanel(forms.Textarea):
31 class FormatPanel(forms.Textarea):
24 def render(self, name, value, attrs=None):
32 def render(self, name, value, attrs=None):
25 output = '<div id="mark-panel">'
33 output = '<div id="mark-panel">'
26 for formatter in formatters:
34 for formatter in formatters:
27 output += u'<span class="mark_btn"' + \
35 output += u'<span class="mark_btn"' + \
28 u' onClick="addMarkToMsg(\'' + formatter.format_left + \
36 u' onClick="addMarkToMsg(\'' + formatter.format_left + \
29 '\', \'' + formatter.format_right + '\')">' + \
37 '\', \'' + formatter.format_right + '\')">' + \
30 formatter.preview_left + formatter.name + \
38 formatter.preview_left + formatter.name + \
31 formatter.preview_right + u'</span>'
39 formatter.preview_right + u'</span>'
32
40
33 output += '</div>'
41 output += '</div>'
34 output += super(FormatPanel, self).render(name, value, attrs=None)
42 output += super(FormatPanel, self).render(name, value, attrs=None)
35
43
36 return output
44 return output
37
45
38
46
39 class PlainErrorList(ErrorList):
47 class PlainErrorList(ErrorList):
40 def __unicode__(self):
48 def __unicode__(self):
41 return self.as_text()
49 return self.as_text()
42
50
43 def as_text(self):
51 def as_text(self):
44 return ''.join([u'(!) %s ' % e for e in self])
52 return ''.join([u'(!) %s ' % e for e in self])
45
53
46
54
47 class NeboardForm(forms.Form):
55 class NeboardForm(forms.Form):
48
56
49 def as_div(self):
57 def as_div(self):
50 """
58 """
51 Returns this form rendered as HTML <as_div>s.
59 Returns this form rendered as HTML <as_div>s.
52 """
60 """
53
61
54 return self._html_output(
62 return self._html_output(
55 # TODO Do not show hidden rows in the list here
63 # TODO Do not show hidden rows in the list here
56 normal_row='<div class="form-row">'
64 normal_row='<div class="form-row">'
57 '<div class="form-label">'
65 '<div class="form-label">'
58 '%(label)s'
66 '%(label)s'
59 '</div>'
67 '</div>'
60 '<div class="form-input">'
68 '<div class="form-input">'
61 '%(field)s'
69 '%(field)s'
62 '</div>'
70 '</div>'
63 '%(help_text)s'
71 '%(help_text)s'
64 '</div>',
72 '</div>',
65 error_row='<div class="form-row">'
73 error_row='<div class="form-row">'
66 '<div class="form-label"></div>'
74 '<div class="form-label"></div>'
67 '<div class="form-errors">%s</div>'
75 '<div class="form-errors">%s</div>'
68 '</div>',
76 '</div>',
69 row_ender='</div>',
77 row_ender='</div>',
70 help_text_html='%s',
78 help_text_html='%s',
71 errors_on_separate_row=True)
79 errors_on_separate_row=True)
72
80
73
81
74 class PostForm(NeboardForm):
82 class PostForm(NeboardForm):
75
83
76 title = forms.CharField(max_length=TITLE_MAX_LENGTH, required=False,
84 title = forms.CharField(max_length=TITLE_MAX_LENGTH, required=False,
77 label=_('Title'))
85 label=LABEL_TITLE)
78 text = forms.CharField(
86 text = forms.CharField(
79 widget=FormatPanel(attrs={ATTRIBUTE_PLACEHOLDER: TEXT_PLACEHOLDER}),
87 widget=FormatPanel(attrs={ATTRIBUTE_PLACEHOLDER: TEXT_PLACEHOLDER}),
80 required=False, label=_('Text'))
88 required=False, label=LABEL_TEXT)
81 image = forms.ImageField(required=False, label=_('Image'))
89 image = forms.ImageField(required=False, label=_('Image'))
82
90
83 # This field is for spam prevention only
91 # This field is for spam prevention only
84 email = forms.CharField(max_length=100, required=False, label=_('e-mail'),
92 email = forms.CharField(max_length=100, required=False, label=_('e-mail'),
85 widget=forms.TextInput(attrs={
93 widget=forms.TextInput(attrs={
86 'class': 'form-email'}))
94 'class': 'form-email'}))
87
95
88 session = None
96 session = None
89 need_to_ban = False
97 need_to_ban = False
90
98
91 def clean_title(self):
99 def clean_title(self):
92 title = self.cleaned_data['title']
100 title = self.cleaned_data['title']
93 if title:
101 if title:
94 if len(title) > TITLE_MAX_LENGTH:
102 if len(title) > TITLE_MAX_LENGTH:
95 raise forms.ValidationError(_('Title must have less than %s '
103 raise forms.ValidationError(_('Title must have less than %s '
96 'characters') %
104 'characters') %
97 str(TITLE_MAX_LENGTH))
105 str(TITLE_MAX_LENGTH))
98 return title
106 return title
99
107
100 def clean_text(self):
108 def clean_text(self):
101 text = self.cleaned_data['text']
109 text = self.cleaned_data['text']
102 if text:
110 if text:
103 if len(text) > board_settings.MAX_TEXT_LENGTH:
111 if len(text) > board_settings.MAX_TEXT_LENGTH:
104 raise forms.ValidationError(_('Text must have less than %s '
112 raise forms.ValidationError(_('Text must have less than %s '
105 'characters') %
113 'characters') %
106 str(board_settings
114 str(board_settings
107 .MAX_TEXT_LENGTH))
115 .MAX_TEXT_LENGTH))
108 return text
116 return text
109
117
110 def clean_image(self):
118 def clean_image(self):
111 image = self.cleaned_data['image']
119 image = self.cleaned_data['image']
112 if image:
120 if image:
113 if image._size > board_settings.MAX_IMAGE_SIZE:
121 if image._size > board_settings.MAX_IMAGE_SIZE:
114 raise forms.ValidationError(
122 raise forms.ValidationError(
115 _('Image must be less than %s bytes')
123 _('Image must be less than %s bytes')
116 % str(board_settings.MAX_IMAGE_SIZE))
124 % str(board_settings.MAX_IMAGE_SIZE))
125
126 md5 = hashlib.md5()
127 for chunk in image.chunks():
128 md5.update(chunk)
129 image_hash = md5.hexdigest()
130 if Post.objects.filter(image_hash=image_hash).exists():
131 raise forms.ValidationError(ERROR_IMAGE_DUPLICATE)
132
117 return image
133 return image
118
134
119 def clean(self):
135 def clean(self):
120 cleaned_data = super(PostForm, self).clean()
136 cleaned_data = super(PostForm, self).clean()
121
137
122 if not self.session:
138 if not self.session:
123 raise forms.ValidationError('Humans have sessions')
139 raise forms.ValidationError('Humans have sessions')
124
140
125 if cleaned_data['email']:
141 if cleaned_data['email']:
126 self.need_to_ban = True
142 self.need_to_ban = True
127 raise forms.ValidationError('A human cannot enter a hidden field')
143 raise forms.ValidationError('A human cannot enter a hidden field')
128
144
129 if not self.errors:
145 if not self.errors:
130 self._clean_text_image()
146 self._clean_text_image()
131
147
132 if not self.errors and self.session:
148 if not self.errors and self.session:
133 self._validate_posting_speed()
149 self._validate_posting_speed()
134
150
135 return cleaned_data
151 return cleaned_data
136
152
137 def _clean_text_image(self):
153 def _clean_text_image(self):
138 text = self.cleaned_data.get('text')
154 text = self.cleaned_data.get('text')
139 image = self.cleaned_data.get('image')
155 image = self.cleaned_data.get('image')
140
156
141 if (not text) and (not image):
157 if (not text) and (not image):
142 error_message = _('Either text or image must be entered.')
158 error_message = _('Either text or image must be entered.')
143 self._errors['text'] = self.error_class([error_message])
159 self._errors['text'] = self.error_class([error_message])
144
160
145 def _validate_posting_speed(self):
161 def _validate_posting_speed(self):
146 can_post = True
162 can_post = True
147
163
148 if LAST_POST_TIME in self.session:
164 if LAST_POST_TIME in self.session:
149 now = time.time()
165 now = time.time()
150 last_post_time = self.session[LAST_POST_TIME]
166 last_post_time = self.session[LAST_POST_TIME]
151
167
152 current_delay = int(now - last_post_time)
168 current_delay = int(now - last_post_time)
153
169
154 if current_delay < settings.POSTING_DELAY:
170 if current_delay < settings.POSTING_DELAY:
155 error_message = _('Wait %s seconds after last posting') % str(
171 error_message = _('Wait %s seconds after last posting') % str(
156 settings.POSTING_DELAY - current_delay)
172 settings.POSTING_DELAY - current_delay)
157 self._errors['text'] = self.error_class([error_message])
173 self._errors['text'] = self.error_class([error_message])
158
174
159 can_post = False
175 can_post = False
160
176
161 if can_post:
177 if can_post:
162 self.session[LAST_POST_TIME] = time.time()
178 self.session[LAST_POST_TIME] = time.time()
163
179
164
180
165 class ThreadForm(PostForm):
181 class ThreadForm(PostForm):
166
182
167 regex_tags = re.compile(ur'^[\w\s\d]+$', re.UNICODE)
183 regex_tags = re.compile(ur'^[\w\s\d]+$', re.UNICODE)
168
184
169 tags = forms.CharField(
185 tags = forms.CharField(
170 widget=forms.TextInput(attrs={ATTRIBUTE_PLACEHOLDER: TAGS_PLACEHOLDER}),
186 widget=forms.TextInput(attrs={ATTRIBUTE_PLACEHOLDER: TAGS_PLACEHOLDER}),
171 max_length=100, label=_('Tags'))
187 max_length=100, label=_('Tags'))
172
188
173 def clean_tags(self):
189 def clean_tags(self):
174 tags = self.cleaned_data['tags']
190 tags = self.cleaned_data['tags']
175
191
176 if tags:
192 if tags:
177 if not self.regex_tags.match(tags):
193 if not self.regex_tags.match(tags):
178 raise forms.ValidationError(
194 raise forms.ValidationError(
179 _('Inappropriate characters in tags.'))
195 _('Inappropriate characters in tags.'))
180
196
181 return tags
197 return tags
182
198
183 def clean(self):
199 def clean(self):
184 cleaned_data = super(ThreadForm, self).clean()
200 cleaned_data = super(ThreadForm, self).clean()
185
201
186 return cleaned_data
202 return cleaned_data
187
203
188
204
189 class PostCaptchaForm(PostForm):
205 class PostCaptchaForm(PostForm):
190 captcha = CaptchaField()
206 captcha = CaptchaField()
191
207
192 def __init__(self, *args, **kwargs):
208 def __init__(self, *args, **kwargs):
193 self.request = kwargs['request']
209 self.request = kwargs['request']
194 del kwargs['request']
210 del kwargs['request']
195
211
196 super(PostCaptchaForm, self).__init__(*args, **kwargs)
212 super(PostCaptchaForm, self).__init__(*args, **kwargs)
197
213
198 def clean(self):
214 def clean(self):
199 cleaned_data = super(PostCaptchaForm, self).clean()
215 cleaned_data = super(PostCaptchaForm, self).clean()
200
216
201 success = self.is_valid()
217 success = self.is_valid()
202 utils.update_captcha_access(self.request, success)
218 utils.update_captcha_access(self.request, success)
203
219
204 if success:
220 if success:
205 return cleaned_data
221 return cleaned_data
206 else:
222 else:
207 raise forms.ValidationError(_("Captcha validation failed"))
223 raise forms.ValidationError(_("Captcha validation failed"))
208
224
209
225
210 class ThreadCaptchaForm(ThreadForm):
226 class ThreadCaptchaForm(ThreadForm):
211 captcha = CaptchaField()
227 captcha = CaptchaField()
212
228
213 def __init__(self, *args, **kwargs):
229 def __init__(self, *args, **kwargs):
214 self.request = kwargs['request']
230 self.request = kwargs['request']
215 del kwargs['request']
231 del kwargs['request']
216
232
217 super(ThreadCaptchaForm, self).__init__(*args, **kwargs)
233 super(ThreadCaptchaForm, self).__init__(*args, **kwargs)
218
234
219 def clean(self):
235 def clean(self):
220 cleaned_data = super(ThreadCaptchaForm, self).clean()
236 cleaned_data = super(ThreadCaptchaForm, self).clean()
221
237
222 success = self.is_valid()
238 success = self.is_valid()
223 utils.update_captcha_access(self.request, success)
239 utils.update_captcha_access(self.request, success)
224
240
225 if success:
241 if success:
226 return cleaned_data
242 return cleaned_data
227 else:
243 else:
228 raise forms.ValidationError(_("Captcha validation failed"))
244 raise forms.ValidationError(_("Captcha validation failed"))
229
245
230
246
231 class SettingsForm(NeboardForm):
247 class SettingsForm(NeboardForm):
232
248
233 theme = forms.ChoiceField(choices=settings.THEMES,
249 theme = forms.ChoiceField(choices=settings.THEMES,
234 label=_('Theme'))
250 label=_('Theme'))
235
251
236
252
237 class ModeratorSettingsForm(SettingsForm):
253 class ModeratorSettingsForm(SettingsForm):
238
254
239 moderate = forms.BooleanField(required=False, label=_('Enable moderation '
255 moderate = forms.BooleanField(required=False, label=_('Enable moderation '
240 'panel'))
256 'panel'))
241
257
242
258
243 class LoginForm(NeboardForm):
259 class LoginForm(NeboardForm):
244
260
245 user_id = forms.CharField()
261 user_id = forms.CharField()
246
262
247 session = None
263 session = None
248
264
249 def clean_user_id(self):
265 def clean_user_id(self):
250 user_id = self.cleaned_data['user_id']
266 user_id = self.cleaned_data['user_id']
251 if user_id:
267 if user_id:
252 users = User.objects.filter(user_id=user_id)
268 users = User.objects.filter(user_id=user_id)
253 if len(users) == 0:
269 if len(users) == 0:
254 raise forms.ValidationError(_('No such user found'))
270 raise forms.ValidationError(_('No such user found'))
255
271
256 return user_id
272 return user_id
257
273
258 def _validate_login_speed(self):
274 def _validate_login_speed(self):
259 can_post = True
275 can_post = True
260
276
261 if LAST_LOGIN_TIME in self.session:
277 if LAST_LOGIN_TIME in self.session:
262 now = time.time()
278 now = time.time()
263 last_login_time = self.session[LAST_LOGIN_TIME]
279 last_login_time = self.session[LAST_LOGIN_TIME]
264
280
265 current_delay = int(now - last_login_time)
281 current_delay = int(now - last_login_time)
266
282
267 if current_delay < board_settings.LOGIN_TIMEOUT:
283 if current_delay < board_settings.LOGIN_TIMEOUT:
268 error_message = _('Wait %s minutes after last login') % str(
284 error_message = _('Wait %s minutes after last login') % str(
269 (board_settings.LOGIN_TIMEOUT - current_delay) / 60)
285 (board_settings.LOGIN_TIMEOUT - current_delay) / 60)
270 self._errors['user_id'] = self.error_class([error_message])
286 self._errors['user_id'] = self.error_class([error_message])
271
287
272 can_post = False
288 can_post = False
273
289
274 if can_post:
290 if can_post:
275 self.session[LAST_LOGIN_TIME] = time.time()
291 self.session[LAST_LOGIN_TIME] = time.time()
276
292
277 def clean(self):
293 def clean(self):
278 if not self.session:
294 if not self.session:
279 raise forms.ValidationError('Humans have sessions')
295 raise forms.ValidationError('Humans have sessions')
280
296
281 self._validate_login_speed()
297 self._validate_login_speed()
282
298
283 cleaned_data = super(LoginForm, self).clean()
299 cleaned_data = super(LoginForm, self).clean()
284
300
285 return cleaned_data
301 return cleaned_data
1 NO CONTENT: modified file, binary diff hidden
NO CONTENT: modified file, binary diff hidden
@@ -1,355 +1,359 b''
1 # SOME DESCRIPTIVE TITLE.
1 # SOME DESCRIPTIVE TITLE.
2 # Copyright (C) YEAR THE PACKAGE'S COPYRIGHT HOLDER
2 # Copyright (C) YEAR THE PACKAGE'S COPYRIGHT HOLDER
3 # This file is distributed under the same license as the PACKAGE package.
3 # This file is distributed under the same license as the PACKAGE package.
4 # FIRST AUTHOR <EMAIL@ADDRESS>, YEAR.
4 # FIRST AUTHOR <EMAIL@ADDRESS>, YEAR.
5 #
5 #
6 msgid ""
6 msgid ""
7 msgstr ""
7 msgstr ""
8 "Project-Id-Version: PACKAGE VERSION\n"
8 "Project-Id-Version: PACKAGE VERSION\n"
9 "Report-Msgid-Bugs-To: \n"
9 "Report-Msgid-Bugs-To: \n"
10 "POT-Creation-Date: 2014-01-13 10:53+0200\n"
10 "POT-Creation-Date: 2014-01-15 10:46+0200\n"
11 "PO-Revision-Date: YEAR-MO-DA HO:MI+ZONE\n"
11 "PO-Revision-Date: YEAR-MO-DA HO:MI+ZONE\n"
12 "Last-Translator: FULL NAME <EMAIL@ADDRESS>\n"
12 "Last-Translator: FULL NAME <EMAIL@ADDRESS>\n"
13 "Language-Team: LANGUAGE <LL@li.org>\n"
13 "Language-Team: LANGUAGE <LL@li.org>\n"
14 "Language: ru\n"
14 "Language: ru\n"
15 "MIME-Version: 1.0\n"
15 "MIME-Version: 1.0\n"
16 "Content-Type: text/plain; charset=UTF-8\n"
16 "Content-Type: text/plain; charset=UTF-8\n"
17 "Content-Transfer-Encoding: 8bit\n"
17 "Content-Transfer-Encoding: 8bit\n"
18 "Plural-Forms: nplurals=3; plural=(n%10==1 && n%100!=11 ? 0 : n%10>=2 && n"
18 "Plural-Forms: nplurals=3; plural=(n%10==1 && n%100!=11 ? 0 : n%10>=2 && n"
19 "%10<=4 && (n%100<10 || n%100>=20) ? 1 : 2);\n"
19 "%10<=4 && (n%100<10 || n%100>=20) ? 1 : 2);\n"
20
20
21 #: authors.py:5
21 #: authors.py:5
22 msgid "author"
22 msgid "author"
23 msgstr "Π°Π²Ρ‚ΠΎΡ€"
23 msgstr "Π°Π²Ρ‚ΠΎΡ€"
24
24
25 #: authors.py:6
25 #: authors.py:6
26 msgid "developer"
26 msgid "developer"
27 msgstr "Ρ€Π°Π·Ρ€Π°Π±ΠΎΡ‚Ρ‡ΠΈΠΊ"
27 msgstr "Ρ€Π°Π·Ρ€Π°Π±ΠΎΡ‚Ρ‡ΠΈΠΊ"
28
28
29 #: authors.py:7
29 #: authors.py:7
30 msgid "javascript developer"
30 msgid "javascript developer"
31 msgstr "Ρ€Π°Π·Ρ€Π°Π±ΠΎΡ‚Ρ‡ΠΈΠΊ javascript"
31 msgstr "Ρ€Π°Π·Ρ€Π°Π±ΠΎΡ‚Ρ‡ΠΈΠΊ javascript"
32
32
33 #: authors.py:8
33 #: authors.py:8
34 msgid "designer"
34 msgid "designer"
35 msgstr "Π΄ΠΈΠ·Π°ΠΉΠ½Π΅Ρ€"
35 msgstr "Π΄ΠΈΠ·Π°ΠΉΠ½Π΅Ρ€"
36
36
37 #: forms.py:18
37 #: forms.py:21
38 msgid ""
38 msgid ""
39 "Type message here. You can reply to message >>123 like\n"
39 "Type message here. You can reply to message >>123 like\n"
40 " this. 2 new lines are required to start new paragraph."
40 " this. 2 new lines are required to start new paragraph."
41 msgstr ""
41 msgstr ""
42 "Π’Π²Π΅Π΄ΠΈΡ‚Π΅ сообщСниС здСсь. Π’Ρ‹ ΠΌΠΎΠΆΠ΅Ρ‚Π΅ ΠΎΡ‚Π²Π΅Ρ‚ΠΈΡ‚ΡŒ Π½Π° сообщСниС >>123 Π²ΠΎΡ‚ Ρ‚Π°ΠΊ. 2 "
42 "Π’Π²Π΅Π΄ΠΈΡ‚Π΅ сообщСниС здСсь. Π’Ρ‹ ΠΌΠΎΠΆΠ΅Ρ‚Π΅ ΠΎΡ‚Π²Π΅Ρ‚ΠΈΡ‚ΡŒ Π½Π° сообщСниС >>123 Π²ΠΎΡ‚ Ρ‚Π°ΠΊ. 2 "
43 "пСрСноса строки ΠΎΠ±ΡΠ·Π°Ρ‚Π΅Π»ΡŒΠ½Ρ‹ для создания Π½ΠΎΠ²ΠΎΠ³ΠΎ Π°Π±Π·Π°Ρ†Π°."
43 "пСрСноса строки ΠΎΠ±ΡΠ·Π°Ρ‚Π΅Π»ΡŒΠ½Ρ‹ для создания Π½ΠΎΠ²ΠΎΠ³ΠΎ Π°Π±Π·Π°Ρ†Π°."
44
44
45 #: forms.py:20
45 #: forms.py:23
46 msgid "tag1 several_words_tag"
46 msgid "tag1 several_words_tag"
47 msgstr "Ρ‚Π΅Π³1 Ρ‚Π΅Π³_ΠΈΠ·_Π½Π΅ΡΠΊΠΎΠ»ΡŒΠΊΠΈΡ…_слов"
47 msgstr "Ρ‚Π΅Π³1 Ρ‚Π΅Π³_ΠΈΠ·_Π½Π΅ΡΠΊΠΎΠ»ΡŒΠΊΠΈΡ…_слов"
48
48
49 #: forms.py:77
49 #: forms.py:25
50 msgid "Such image was already posted"
51 msgstr "Π’Π°ΠΊΠΎΠ΅ ΠΈΠ·ΠΎΠ±Ρ€Π°ΠΆΠ΅Π½ΠΈΠ΅ ΡƒΠΆΠ΅ Π±Ρ‹Π»ΠΎ Π·Π°Π³Ρ€ΡƒΠΆΠ΅Π½ΠΎ"
52
53 #: forms.py:27
50 msgid "Title"
54 msgid "Title"
51 msgstr "Π—Π°Π³ΠΎΠ»ΠΎΠ²ΠΎΠΊ"
55 msgstr "Π—Π°Π³ΠΎΠ»ΠΎΠ²ΠΎΠΊ"
52
56
53 #: forms.py:80
57 #: forms.py:28
54 msgid "Text"
58 msgid "Text"
55 msgstr "ВСкст"
59 msgstr "ВСкст"
56
60
57 #: forms.py:81
61 #: forms.py:89
58 msgid "Image"
62 msgid "Image"
59 msgstr "Π˜Π·ΠΎΠ±Ρ€Π°ΠΆΠ΅Π½ΠΈΠ΅"
63 msgstr "Π˜Π·ΠΎΠ±Ρ€Π°ΠΆΠ΅Π½ΠΈΠ΅"
60
64
61 #: forms.py:84
65 #: forms.py:92
62 msgid "e-mail"
66 msgid "e-mail"
63 msgstr ""
67 msgstr ""
64
68
65 #: forms.py:95
69 #: forms.py:103
66 #, python-format
70 #, python-format
67 msgid "Title must have less than %s characters"
71 msgid "Title must have less than %s characters"
68 msgstr "Π—Π°Π³ΠΎΠ»ΠΎΠ²ΠΎΠΊ Π΄ΠΎΠ»ΠΆΠ΅Π½ ΠΈΠΌΠ΅Ρ‚ΡŒ мСньшС %s символов"
72 msgstr "Π—Π°Π³ΠΎΠ»ΠΎΠ²ΠΎΠΊ Π΄ΠΎΠ»ΠΆΠ΅Π½ ΠΈΠΌΠ΅Ρ‚ΡŒ мСньшС %s символов"
69
73
70 #: forms.py:104
74 #: forms.py:112
71 #, python-format
75 #, python-format
72 msgid "Text must have less than %s characters"
76 msgid "Text must have less than %s characters"
73 msgstr "ВСкст Π΄ΠΎΠ»ΠΆΠ΅Π½ Π±Ρ‹Ρ‚ΡŒ ΠΊΠΎΡ€ΠΎΡ‡Π΅ %s символов"
77 msgstr "ВСкст Π΄ΠΎΠ»ΠΆΠ΅Π½ Π±Ρ‹Ρ‚ΡŒ ΠΊΠΎΡ€ΠΎΡ‡Π΅ %s символов"
74
78
75 #: forms.py:115
79 #: forms.py:123
76 #, python-format
80 #, python-format
77 msgid "Image must be less than %s bytes"
81 msgid "Image must be less than %s bytes"
78 msgstr "Π˜Π·ΠΎΠ±Ρ€Π°ΠΆΠ΅Π½ΠΈΠ΅ Π΄ΠΎΠ»ΠΆΠ½ΠΎ Π±Ρ‹Ρ‚ΡŒ ΠΌΠ΅Π½Π΅Π΅ %s Π±Π°ΠΉΡ‚"
82 msgstr "Π˜Π·ΠΎΠ±Ρ€Π°ΠΆΠ΅Π½ΠΈΠ΅ Π΄ΠΎΠ»ΠΆΠ½ΠΎ Π±Ρ‹Ρ‚ΡŒ ΠΌΠ΅Π½Π΅Π΅ %s Π±Π°ΠΉΡ‚"
79
83
80 #: forms.py:142
84 #: forms.py:158
81 msgid "Either text or image must be entered."
85 msgid "Either text or image must be entered."
82 msgstr "ВСкст ΠΈΠ»ΠΈ ΠΊΠ°Ρ€Ρ‚ΠΈΠ½ΠΊΠ° Π΄ΠΎΠ»ΠΆΠ½Ρ‹ Π±Ρ‹Ρ‚ΡŒ Π²Π²Π΅Π΄Π΅Π½Ρ‹."
86 msgstr "ВСкст ΠΈΠ»ΠΈ ΠΊΠ°Ρ€Ρ‚ΠΈΠ½ΠΊΠ° Π΄ΠΎΠ»ΠΆΠ½Ρ‹ Π±Ρ‹Ρ‚ΡŒ Π²Π²Π΅Π΄Π΅Π½Ρ‹."
83
87
84 #: forms.py:155
88 #: forms.py:171
85 #, python-format
89 #, python-format
86 msgid "Wait %s seconds after last posting"
90 msgid "Wait %s seconds after last posting"
87 msgstr "ΠŸΠΎΠ΄ΠΎΠΆΠ΄ΠΈΡ‚Π΅ %s сСкунд послС послСднСго постинга"
91 msgstr "ΠŸΠΎΠ΄ΠΎΠΆΠ΄ΠΈΡ‚Π΅ %s сСкунд послС послСднСго постинга"
88
92
89 #: forms.py:171 templates/boards/tags.html:6 templates/boards/rss/post.html:10
93 #: forms.py:187 templates/boards/tags.html:6 templates/boards/rss/post.html:10
90 msgid "Tags"
94 msgid "Tags"
91 msgstr "Π’Π΅Π³ΠΈ"
95 msgstr "Π’Π΅Π³ΠΈ"
92
96
93 #: forms.py:179
97 #: forms.py:195
94 msgid "Inappropriate characters in tags."
98 msgid "Inappropriate characters in tags."
95 msgstr "НСдопустимыС символы Π² Ρ‚Π΅Π³Π°Ρ…."
99 msgstr "НСдопустимыС символы Π² Ρ‚Π΅Π³Π°Ρ…."
96
100
97 #: forms.py:207 forms.py:228
101 #: forms.py:223 forms.py:244
98 msgid "Captcha validation failed"
102 msgid "Captcha validation failed"
99 msgstr "ΠŸΡ€ΠΎΠ²Π΅Ρ€ΠΊΠ° ΠΊΠ°ΠΏΡ‡ΠΈ ΠΏΡ€ΠΎΠ²Π°Π»Π΅Π½Π°"
103 msgstr "ΠŸΡ€ΠΎΠ²Π΅Ρ€ΠΊΠ° ΠΊΠ°ΠΏΡ‡ΠΈ ΠΏΡ€ΠΎΠ²Π°Π»Π΅Π½Π°"
100
104
101 #: forms.py:234
105 #: forms.py:250
102 msgid "Theme"
106 msgid "Theme"
103 msgstr "Π’Π΅ΠΌΠ°"
107 msgstr "Π’Π΅ΠΌΠ°"
104
108
105 #: forms.py:239
109 #: forms.py:255
106 msgid "Enable moderation panel"
110 msgid "Enable moderation panel"
107 msgstr "Π’ΠΊΠ»ΡŽΡ‡ΠΈΡ‚ΡŒ панСль ΠΌΠΎΠ΄Π΅Ρ€Π°Ρ†ΠΈΠΈ"
111 msgstr "Π’ΠΊΠ»ΡŽΡ‡ΠΈΡ‚ΡŒ панСль ΠΌΠΎΠ΄Π΅Ρ€Π°Ρ†ΠΈΠΈ"
108
112
109 #: forms.py:254
113 #: forms.py:270
110 msgid "No such user found"
114 msgid "No such user found"
111 msgstr "Π”Π°Π½Π½Ρ‹ΠΉ ΠΏΠΎΠ»ΡŒΠ·ΠΎΠ²Π°Ρ‚Π΅Π»ΡŒ Π½Π΅ Π½Π°ΠΉΠ΄Π΅Π½"
115 msgstr "Π”Π°Π½Π½Ρ‹ΠΉ ΠΏΠΎΠ»ΡŒΠ·ΠΎΠ²Π°Ρ‚Π΅Π»ΡŒ Π½Π΅ Π½Π°ΠΉΠ΄Π΅Π½"
112
116
113 #: forms.py:268
117 #: forms.py:284
114 #, python-format
118 #, python-format
115 msgid "Wait %s minutes after last login"
119 msgid "Wait %s minutes after last login"
116 msgstr "ΠŸΠΎΠ΄ΠΎΠΆΠ΄ΠΈΡ‚Π΅ %s ΠΌΠΈΠ½ΡƒΡ‚ послС послСднСго Π²Ρ…ΠΎΠ΄Π°"
120 msgstr "ΠŸΠΎΠ΄ΠΎΠΆΠ΄ΠΈΡ‚Π΅ %s ΠΌΠΈΠ½ΡƒΡ‚ послС послСднСго Π²Ρ…ΠΎΠ΄Π°"
117
121
118 #: templates/boards/404.html:6
122 #: templates/boards/404.html:6
119 msgid "Not found"
123 msgid "Not found"
120 msgstr "НС найдСно"
124 msgstr "НС найдСно"
121
125
122 #: templates/boards/404.html:12
126 #: templates/boards/404.html:12
123 msgid "This page does not exist"
127 msgid "This page does not exist"
124 msgstr "Π­Ρ‚ΠΎΠΉ страницы Π½Π΅ сущСствуСт"
128 msgstr "Π­Ρ‚ΠΎΠΉ страницы Π½Π΅ сущСствуСт"
125
129
126 #: templates/boards/archive.html:9 templates/boards/base.html:51
130 #: templates/boards/archive.html:9 templates/boards/base.html:51
127 msgid "Archive"
131 msgid "Archive"
128 msgstr "Архив"
132 msgstr "Архив"
129
133
130 #: templates/boards/archive.html:39 templates/boards/posting_general.html:64
134 #: templates/boards/archive.html:39 templates/boards/posting_general.html:64
131 msgid "Previous page"
135 msgid "Previous page"
132 msgstr "ΠŸΡ€Π΅Π΄Ρ‹Π΄ΡƒΡ‰Π°Ρ страница"
136 msgstr "ΠŸΡ€Π΅Π΄Ρ‹Π΄ΡƒΡ‰Π°Ρ страница"
133
137
134 #: templates/boards/archive.html:68
138 #: templates/boards/archive.html:68
135 msgid "Open"
139 msgid "Open"
136 msgstr "ΠžΡ‚ΠΊΡ€Ρ‹Ρ‚ΡŒ"
140 msgstr "ΠžΡ‚ΠΊΡ€Ρ‹Ρ‚ΡŒ"
137
141
138 #: templates/boards/archive.html:74 templates/boards/post.html:37
142 #: templates/boards/archive.html:74 templates/boards/post.html:37
139 #: templates/boards/posting_general.html:103 templates/boards/thread.html:69
143 #: templates/boards/posting_general.html:103 templates/boards/thread.html:69
140 msgid "Delete"
144 msgid "Delete"
141 msgstr "Π£Π΄Π°Π»ΠΈΡ‚ΡŒ"
145 msgstr "Π£Π΄Π°Π»ΠΈΡ‚ΡŒ"
142
146
143 #: templates/boards/archive.html:78 templates/boards/post.html:40
147 #: templates/boards/archive.html:78 templates/boards/post.html:40
144 #: templates/boards/posting_general.html:107 templates/boards/thread.html:72
148 #: templates/boards/posting_general.html:107 templates/boards/thread.html:72
145 msgid "Ban IP"
149 msgid "Ban IP"
146 msgstr "Π—Π°Π±Π»ΠΎΠΊΠΈΡ€ΠΎΠ²Π°Ρ‚ΡŒ IP"
150 msgstr "Π—Π°Π±Π»ΠΎΠΊΠΈΡ€ΠΎΠ²Π°Ρ‚ΡŒ IP"
147
151
148 #: templates/boards/archive.html:87 templates/boards/post.html:53
152 #: templates/boards/archive.html:87 templates/boards/post.html:53
149 #: templates/boards/posting_general.html:116
153 #: templates/boards/posting_general.html:116
150 #: templates/boards/posting_general.html:180 templates/boards/thread.html:81
154 #: templates/boards/posting_general.html:180 templates/boards/thread.html:81
151 msgid "Replies"
155 msgid "Replies"
152 msgstr "ΠžΡ‚Π²Π΅Ρ‚Ρ‹"
156 msgstr "ΠžΡ‚Π²Π΅Ρ‚Ρ‹"
153
157
154 #: templates/boards/archive.html:96 templates/boards/posting_general.html:125
158 #: templates/boards/archive.html:96 templates/boards/posting_general.html:125
155 #: templates/boards/thread.html:138 templates/boards/thread_gallery.html:58
159 #: templates/boards/thread.html:138 templates/boards/thread_gallery.html:58
156 msgid "images"
160 msgid "images"
157 msgstr "ΠΈΠ·ΠΎΠ±Ρ€Π°ΠΆΠ΅Π½ΠΈΠΉ"
161 msgstr "ΠΈΠ·ΠΎΠ±Ρ€Π°ΠΆΠ΅Π½ΠΈΠΉ"
158
162
159 #: templates/boards/archive.html:97 templates/boards/thread.html:137
163 #: templates/boards/archive.html:97 templates/boards/thread.html:137
160 #: templates/boards/thread_gallery.html:57
164 #: templates/boards/thread_gallery.html:57
161 msgid "replies"
165 msgid "replies"
162 msgstr "ΠΎΡ‚Π²Π΅Ρ‚ΠΎΠ²"
166 msgstr "ΠΎΡ‚Π²Π΅Ρ‚ΠΎΠ²"
163
167
164 #: templates/boards/archive.html:116 templates/boards/posting_general.html:203
168 #: templates/boards/archive.html:116 templates/boards/posting_general.html:203
165 msgid "Next page"
169 msgid "Next page"
166 msgstr "Π‘Π»Π΅Π΄ΡƒΡŽΡ‰Π°Ρ страница"
170 msgstr "Π‘Π»Π΅Π΄ΡƒΡŽΡ‰Π°Ρ страница"
167
171
168 #: templates/boards/archive.html:121 templates/boards/posting_general.html:208
172 #: templates/boards/archive.html:121 templates/boards/posting_general.html:208
169 msgid "No threads exist. Create the first one!"
173 msgid "No threads exist. Create the first one!"
170 msgstr "НСт Ρ‚Π΅ΠΌ. Π‘ΠΎΠ·Π΄Π°ΠΉΡ‚Π΅ ΠΏΠ΅Ρ€Π²ΡƒΡŽ!"
174 msgstr "НСт Ρ‚Π΅ΠΌ. Π‘ΠΎΠ·Π΄Π°ΠΉΡ‚Π΅ ΠΏΠ΅Ρ€Π²ΡƒΡŽ!"
171
175
172 #: templates/boards/archive.html:130 templates/boards/posting_general.html:235
176 #: templates/boards/archive.html:130 templates/boards/posting_general.html:235
173 msgid "Pages:"
177 msgid "Pages:"
174 msgstr "Π‘Ρ‚Ρ€Π°Π½ΠΈΡ†Ρ‹: "
178 msgstr "Π‘Ρ‚Ρ€Π°Π½ΠΈΡ†Ρ‹: "
175
179
176 #: templates/boards/authors.html:6 templates/boards/authors.html.py:12
180 #: templates/boards/authors.html:6 templates/boards/authors.html.py:12
177 msgid "Authors"
181 msgid "Authors"
178 msgstr "Авторы"
182 msgstr "Авторы"
179
183
180 #: templates/boards/authors.html:25
184 #: templates/boards/authors.html:25
181 msgid "Distributed under the"
185 msgid "Distributed under the"
182 msgstr "РаспространяСтся ΠΏΠΎΠ΄"
186 msgstr "РаспространяСтся ΠΏΠΎΠ΄"
183
187
184 #: templates/boards/authors.html:27
188 #: templates/boards/authors.html:27
185 msgid "license"
189 msgid "license"
186 msgstr "Π»ΠΈΡ†Π΅Π½Π·ΠΈΠ΅ΠΉ"
190 msgstr "Π»ΠΈΡ†Π΅Π½Π·ΠΈΠ΅ΠΉ"
187
191
188 #: templates/boards/authors.html:29
192 #: templates/boards/authors.html:29
189 msgid "Repository"
193 msgid "Repository"
190 msgstr "Π Π΅ΠΏΠΎΠ·ΠΈΡ‚ΠΎΡ€ΠΈΠΉ"
194 msgstr "Π Π΅ΠΏΠΎΠ·ΠΈΡ‚ΠΎΡ€ΠΈΠΉ"
191
195
192 #: templates/boards/base.html:14
196 #: templates/boards/base.html:14
193 msgid "Feed"
197 msgid "Feed"
194 msgstr "Π›Π΅Π½Ρ‚Π°"
198 msgstr "Π›Π΅Π½Ρ‚Π°"
195
199
196 #: templates/boards/base.html:31
200 #: templates/boards/base.html:31
197 msgid "All threads"
201 msgid "All threads"
198 msgstr "ВсС Ρ‚Π΅ΠΌΡ‹"
202 msgstr "ВсС Ρ‚Π΅ΠΌΡ‹"
199
203
200 #: templates/boards/base.html:36
204 #: templates/boards/base.html:36
201 msgid "Tag management"
205 msgid "Tag management"
202 msgstr "Π£ΠΏΡ€Π°Π²Π»Π΅Π½ΠΈΠ΅ Ρ‚Π΅Π³Π°ΠΌΠΈ"
206 msgstr "Π£ΠΏΡ€Π°Π²Π»Π΅Π½ΠΈΠ΅ Ρ‚Π΅Π³Π°ΠΌΠΈ"
203
207
204 #: templates/boards/base.html:38
208 #: templates/boards/base.html:38
205 msgid "Settings"
209 msgid "Settings"
206 msgstr "Настройки"
210 msgstr "Настройки"
207
211
208 #: templates/boards/base.html:50 templates/boards/login.html:6
212 #: templates/boards/base.html:50 templates/boards/login.html:6
209 #: templates/boards/login.html.py:21
213 #: templates/boards/login.html.py:21
210 msgid "Login"
214 msgid "Login"
211 msgstr "Π’Ρ…ΠΎΠ΄"
215 msgstr "Π’Ρ…ΠΎΠ΄"
212
216
213 #: templates/boards/base.html:53
217 #: templates/boards/base.html:53
214 #, python-format
218 #, python-format
215 msgid "Speed: %(ppd)s posts per day"
219 msgid "Speed: %(ppd)s posts per day"
216 msgstr "Π‘ΠΊΠΎΡ€ΠΎΡΡ‚ΡŒ: %(ppd)s сообщСний Π² дСнь"
220 msgstr "Π‘ΠΊΠΎΡ€ΠΎΡΡ‚ΡŒ: %(ppd)s сообщСний Π² дСнь"
217
221
218 #: templates/boards/base.html:55
222 #: templates/boards/base.html:55
219 msgid "Up"
223 msgid "Up"
220 msgstr "Π’Π²Π΅Ρ€Ρ…"
224 msgstr "Π’Π²Π΅Ρ€Ρ…"
221
225
222 #: templates/boards/login.html:15
226 #: templates/boards/login.html:15
223 msgid "User ID"
227 msgid "User ID"
224 msgstr "ID ΠΏΠΎΠ»ΡŒΠ·ΠΎΠ²Π°Ρ‚Π΅Π»Ρ"
228 msgstr "ID ΠΏΠΎΠ»ΡŒΠ·ΠΎΠ²Π°Ρ‚Π΅Π»Ρ"
225
229
226 #: templates/boards/login.html:24
230 #: templates/boards/login.html:24
227 msgid "Insert your user id above"
231 msgid "Insert your user id above"
228 msgstr "Π’ΡΡ‚Π°Π²ΡŒΡ‚Π΅ свой ID ΠΏΠΎΠ»ΡŒΠ·ΠΎΠ²Π°Ρ‚Π΅Π»Ρ Π²Ρ‹ΡˆΠ΅"
232 msgstr "Π’ΡΡ‚Π°Π²ΡŒΡ‚Π΅ свой ID ΠΏΠΎΠ»ΡŒΠ·ΠΎΠ²Π°Ρ‚Π΅Π»Ρ Π²Ρ‹ΡˆΠ΅"
229
233
230 #: templates/boards/posting_general.html:97
234 #: templates/boards/posting_general.html:97
231 msgid "Reply"
235 msgid "Reply"
232 msgstr "ΠžΡ‚Π²Π΅Ρ‚"
236 msgstr "ΠžΡ‚Π²Π΅Ρ‚"
233
237
234 #: templates/boards/posting_general.html:142
238 #: templates/boards/posting_general.html:142
235 #, python-format
239 #, python-format
236 msgid "Skipped %(count)s replies. Open thread to see all replies."
240 msgid "Skipped %(count)s replies. Open thread to see all replies."
237 msgstr "ΠŸΡ€ΠΎΠΏΡƒΡ‰Π΅Π½ΠΎ %(count)s ΠΎΡ‚Π²Π΅Ρ‚ΠΎΠ². ΠžΡ‚ΠΊΡ€ΠΎΠΉΡ‚Π΅ Ρ‚Ρ€Π΅Π΄, Ρ‡Ρ‚ΠΎΠ±Ρ‹ ΡƒΠ²ΠΈΠ΄Π΅Ρ‚ΡŒ всС ΠΎΡ‚Π²Π΅Ρ‚Ρ‹."
241 msgstr "ΠŸΡ€ΠΎΠΏΡƒΡ‰Π΅Π½ΠΎ %(count)s ΠΎΡ‚Π²Π΅Ρ‚ΠΎΠ². ΠžΡ‚ΠΊΡ€ΠΎΠΉΡ‚Π΅ Ρ‚Ρ€Π΅Π΄, Ρ‡Ρ‚ΠΎΠ±Ρ‹ ΡƒΠ²ΠΈΠ΄Π΅Ρ‚ΡŒ всС ΠΎΡ‚Π²Π΅Ρ‚Ρ‹."
238
242
239 #: templates/boards/posting_general.html:214
243 #: templates/boards/posting_general.html:214
240 msgid "Create new thread"
244 msgid "Create new thread"
241 msgstr "Π‘ΠΎΠ·Π΄Π°Ρ‚ΡŒ Π½ΠΎΠ²ΡƒΡŽ Ρ‚Π΅ΠΌΡƒ"
245 msgstr "Π‘ΠΎΠ·Π΄Π°Ρ‚ΡŒ Π½ΠΎΠ²ΡƒΡŽ Ρ‚Π΅ΠΌΡƒ"
242
246
243 #: templates/boards/posting_general.html:218 templates/boards/thread.html:115
247 #: templates/boards/posting_general.html:218 templates/boards/thread.html:115
244 msgid "Post"
248 msgid "Post"
245 msgstr "ΠžΡ‚ΠΏΡ€Π°Π²ΠΈΡ‚ΡŒ"
249 msgstr "ΠžΡ‚ΠΏΡ€Π°Π²ΠΈΡ‚ΡŒ"
246
250
247 #: templates/boards/posting_general.html:222
251 #: templates/boards/posting_general.html:222
248 msgid "Tags must be delimited by spaces. Text or image is required."
252 msgid "Tags must be delimited by spaces. Text or image is required."
249 msgstr ""
253 msgstr ""
250 "Π’Π΅Π³ΠΈ Π΄ΠΎΠ»ΠΆΠ½Ρ‹ Π±Ρ‹Ρ‚ΡŒ Ρ€Π°Π·Π΄Π΅Π»Π΅Π½Ρ‹ ΠΏΡ€ΠΎΠ±Π΅Π»Π°ΠΌΠΈ. ВСкст ΠΈΠ»ΠΈ ΠΈΠ·ΠΎΠ±Ρ€Π°ΠΆΠ΅Π½ΠΈΠ΅ ΠΎΠ±ΡΠ·Π°Ρ‚Π΅Π»ΡŒΠ½Ρ‹."
254 "Π’Π΅Π³ΠΈ Π΄ΠΎΠ»ΠΆΠ½Ρ‹ Π±Ρ‹Ρ‚ΡŒ Ρ€Π°Π·Π΄Π΅Π»Π΅Π½Ρ‹ ΠΏΡ€ΠΎΠ±Π΅Π»Π°ΠΌΠΈ. ВСкст ΠΈΠ»ΠΈ ΠΈΠ·ΠΎΠ±Ρ€Π°ΠΆΠ΅Π½ΠΈΠ΅ ΠΎΠ±ΡΠ·Π°Ρ‚Π΅Π»ΡŒΠ½Ρ‹."
251
255
252 #: templates/boards/posting_general.html:225 templates/boards/thread.html:119
256 #: templates/boards/posting_general.html:225 templates/boards/thread.html:119
253 msgid "Text syntax"
257 msgid "Text syntax"
254 msgstr "Бинтаксис тСкста"
258 msgstr "Бинтаксис тСкста"
255
259
256 #: templates/boards/settings.html:14
260 #: templates/boards/settings.html:14
257 msgid "User:"
261 msgid "User:"
258 msgstr "ΠŸΠΎΠ»ΡŒΠ·ΠΎΠ²Π°Ρ‚Π΅Π»ΡŒ:"
262 msgstr "ΠŸΠΎΠ»ΡŒΠ·ΠΎΠ²Π°Ρ‚Π΅Π»ΡŒ:"
259
263
260 #: templates/boards/settings.html:16
264 #: templates/boards/settings.html:16
261 msgid "You are moderator."
265 msgid "You are moderator."
262 msgstr "Π’Ρ‹ ΠΌΠΎΠ΄Π΅Ρ€Π°Ρ‚ΠΎΡ€."
266 msgstr "Π’Ρ‹ ΠΌΠΎΠ΄Π΅Ρ€Π°Ρ‚ΠΎΡ€."
263
267
264 #: templates/boards/settings.html:19
268 #: templates/boards/settings.html:19
265 msgid "Posts:"
269 msgid "Posts:"
266 msgstr "Π‘ΠΎΠΎΠ±Ρ‰Π΅Π½ΠΈΠΉ:"
270 msgstr "Π‘ΠΎΠΎΠ±Ρ‰Π΅Π½ΠΈΠΉ:"
267
271
268 #: templates/boards/settings.html:20
272 #: templates/boards/settings.html:20
269 msgid "First access:"
273 msgid "First access:"
270 msgstr "ΠŸΠ΅Ρ€Π²Ρ‹ΠΉ доступ:"
274 msgstr "ΠŸΠ΅Ρ€Π²Ρ‹ΠΉ доступ:"
271
275
272 #: templates/boards/settings.html:22
276 #: templates/boards/settings.html:22
273 msgid "Last access:"
277 msgid "Last access:"
274 msgstr "ПослСдний доступ: "
278 msgstr "ПослСдний доступ: "
275
279
276 #: templates/boards/settings.html:31
280 #: templates/boards/settings.html:31
277 msgid "Save"
281 msgid "Save"
278 msgstr "Π‘ΠΎΡ…Ρ€Π°Π½ΠΈΡ‚ΡŒ"
282 msgstr "Π‘ΠΎΡ…Ρ€Π°Π½ΠΈΡ‚ΡŒ"
279
283
280 #: templates/boards/tags.html:27
284 #: templates/boards/tags.html:27
281 msgid "No tags found."
285 msgid "No tags found."
282 msgstr "Π’Π΅Π³ΠΈ Π½Π΅ Π½Π°ΠΉΠ΄Π΅Π½Ρ‹."
286 msgstr "Π’Π΅Π³ΠΈ Π½Π΅ Π½Π°ΠΉΠ΄Π΅Π½Ρ‹."
283
287
284 #: templates/boards/thread.html:19 templates/boards/thread_gallery.html:20
288 #: templates/boards/thread.html:19 templates/boards/thread_gallery.html:20
285 msgid "Normal mode"
289 msgid "Normal mode"
286 msgstr "ΠΠΎΡ€ΠΌΠ°Π»ΡŒΠ½Ρ‹ΠΉ Ρ€Π΅ΠΆΠΈΠΌ"
290 msgstr "ΠΠΎΡ€ΠΌΠ°Π»ΡŒΠ½Ρ‹ΠΉ Ρ€Π΅ΠΆΠΈΠΌ"
287
291
288 #: templates/boards/thread.html:20 templates/boards/thread_gallery.html:21
292 #: templates/boards/thread.html:20 templates/boards/thread_gallery.html:21
289 msgid "Gallery mode"
293 msgid "Gallery mode"
290 msgstr "Π Π΅ΠΆΠΈΠΌ Π³Π°Π»Π΅Ρ€Π΅ΠΈ"
294 msgstr "Π Π΅ΠΆΠΈΠΌ Π³Π°Π»Π΅Ρ€Π΅ΠΈ"
291
295
292 #: templates/boards/thread.html:28
296 #: templates/boards/thread.html:28
293 msgid "posts to bumplimit"
297 msgid "posts to bumplimit"
294 msgstr "сообщСний Π΄ΠΎ Π±Π°ΠΌΠΏΠ»ΠΈΠΌΠΈΡ‚Π°"
298 msgstr "сообщСний Π΄ΠΎ Π±Π°ΠΌΠΏΠ»ΠΈΠΌΠΈΡ‚Π°"
295
299
296 #: templates/boards/thread.html:109
300 #: templates/boards/thread.html:109
297 msgid "Reply to thread"
301 msgid "Reply to thread"
298 msgstr "ΠžΡ‚Π²Π΅Ρ‚ΠΈΡ‚ΡŒ Π² Ρ‚Π΅ΠΌΡƒ"
302 msgstr "ΠžΡ‚Π²Π΅Ρ‚ΠΈΡ‚ΡŒ Π² Ρ‚Π΅ΠΌΡƒ"
299
303
300 #: templates/boards/thread.html:139 templates/boards/thread_gallery.html:59
304 #: templates/boards/thread.html:139 templates/boards/thread_gallery.html:59
301 msgid "Last update: "
305 msgid "Last update: "
302 msgstr "ПослСднСС обновлСниС: "
306 msgstr "ПослСднСС обновлСниС: "
303
307
304 #: templates/boards/rss/post.html:5
308 #: templates/boards/rss/post.html:5
305 msgid "Post image"
309 msgid "Post image"
306 msgstr "Π˜Π·ΠΎΠ±Ρ€Π°ΠΆΠ΅Π½ΠΈΠ΅ сообщСния"
310 msgstr "Π˜Π·ΠΎΠ±Ρ€Π°ΠΆΠ΅Π½ΠΈΠ΅ сообщСния"
307
311
308 #: templates/boards/staticpages/banned.html:6
312 #: templates/boards/staticpages/banned.html:6
309 msgid "Banned"
313 msgid "Banned"
310 msgstr "Π—Π°Π±Π»ΠΎΠΊΠΈΡ€ΠΎΠ²Π°Π½"
314 msgstr "Π—Π°Π±Π»ΠΎΠΊΠΈΡ€ΠΎΠ²Π°Π½"
311
315
312 #: templates/boards/staticpages/banned.html:11
316 #: templates/boards/staticpages/banned.html:11
313 msgid "Your IP address has been banned. Contact the administrator"
317 msgid "Your IP address has been banned. Contact the administrator"
314 msgstr "Π’Π°Ρˆ IP адрСс Π±Ρ‹Π» Π·Π°Π±Π»ΠΎΠΊΠΈΡ€ΠΎΠ²Π°Π½. Π‘Π²ΡΠΆΠΈΡ‚Π΅ΡΡŒ с администратором"
318 msgstr "Π’Π°Ρˆ IP адрСс Π±Ρ‹Π» Π·Π°Π±Π»ΠΎΠΊΠΈΡ€ΠΎΠ²Π°Π½. Π‘Π²ΡΠΆΠΈΡ‚Π΅ΡΡŒ с администратором"
315
319
316 #: templates/boards/staticpages/help.html:6
320 #: templates/boards/staticpages/help.html:6
317 #: templates/boards/staticpages/help.html:10
321 #: templates/boards/staticpages/help.html:10
318 msgid "Syntax"
322 msgid "Syntax"
319 msgstr "Бинтаксис"
323 msgstr "Бинтаксис"
320
324
321 #: templates/boards/staticpages/help.html:11
325 #: templates/boards/staticpages/help.html:11
322 msgid "2 line breaks for a new line."
326 msgid "2 line breaks for a new line."
323 msgstr "2 ΠΏΠ΅Ρ€Π΅Π²ΠΎΠ΄Π° строки ΡΠΎΠ·Π΄Π°ΡŽΡ‚ Π½ΠΎΠ²Ρ‹ΠΉ Π°Π±Π·Π°Ρ†."
327 msgstr "2 ΠΏΠ΅Ρ€Π΅Π²ΠΎΠ΄Π° строки ΡΠΎΠ·Π΄Π°ΡŽΡ‚ Π½ΠΎΠ²Ρ‹ΠΉ Π°Π±Π·Π°Ρ†."
324
328
325 #: templates/boards/staticpages/help.html:12
329 #: templates/boards/staticpages/help.html:12
326 msgid "Italic text"
330 msgid "Italic text"
327 msgstr "ΠšΡƒΡ€ΡΠΈΠ²Π½Ρ‹ΠΉ тСкст"
331 msgstr "ΠšΡƒΡ€ΡΠΈΠ²Π½Ρ‹ΠΉ тСкст"
328
332
329 #: templates/boards/staticpages/help.html:13
333 #: templates/boards/staticpages/help.html:13
330 msgid "Bold text"
334 msgid "Bold text"
331 msgstr "ΠŸΠΎΠ»ΡƒΠΆΠΈΡ€Π½Ρ‹ΠΉ тСкст"
335 msgstr "ΠŸΠΎΠ»ΡƒΠΆΠΈΡ€Π½Ρ‹ΠΉ тСкст"
332
336
333 #: templates/boards/staticpages/help.html:14
337 #: templates/boards/staticpages/help.html:14
334 msgid "Spoiler"
338 msgid "Spoiler"
335 msgstr "Π‘ΠΏΠΎΠΉΠ»Π΅Ρ€"
339 msgstr "Π‘ΠΏΠΎΠΉΠ»Π΅Ρ€"
336
340
337 #: templates/boards/staticpages/help.html:15
341 #: templates/boards/staticpages/help.html:15
338 msgid "Link to a post"
342 msgid "Link to a post"
339 msgstr "Бсылка Π½Π° сообщСниС"
343 msgstr "Бсылка Π½Π° сообщСниС"
340
344
341 #: templates/boards/staticpages/help.html:16
345 #: templates/boards/staticpages/help.html:16
342 msgid "Strikethrough text"
346 msgid "Strikethrough text"
343 msgstr "Π—Π°Ρ‡Π΅Ρ€ΠΊΠ½ΡƒΡ‚Ρ‹ΠΉ тСкст"
347 msgstr "Π—Π°Ρ‡Π΅Ρ€ΠΊΠ½ΡƒΡ‚Ρ‹ΠΉ тСкст"
344
348
345 #: templates/boards/staticpages/help.html:17
349 #: templates/boards/staticpages/help.html:17
346 msgid "You need to new line before:"
350 msgid "You need to new line before:"
347 msgstr "ΠŸΠ΅Ρ€Π΅Π΄ этими Ρ‚Π΅Π³Π°ΠΌΠΈ Π½ΡƒΠΆΠ½Π° новая строка:"
351 msgstr "ΠŸΠ΅Ρ€Π΅Π΄ этими Ρ‚Π΅Π³Π°ΠΌΠΈ Π½ΡƒΠΆΠ½Π° новая строка:"
348
352
349 #: templates/boards/staticpages/help.html:18
353 #: templates/boards/staticpages/help.html:18
350 msgid "Comment"
354 msgid "Comment"
351 msgstr "ΠšΠΎΠΌΠΌΠ΅Π½Ρ‚Π°Ρ€ΠΈΠΉ"
355 msgstr "ΠšΠΎΠΌΠΌΠ΅Π½Ρ‚Π°Ρ€ΠΈΠΉ"
352
356
353 #: templates/boards/staticpages/help.html:19
357 #: templates/boards/staticpages/help.html:19
354 msgid "Quote"
358 msgid "Quote"
355 msgstr "Π¦ΠΈΡ‚Π°Ρ‚Π°"
359 msgstr "Π¦ΠΈΡ‚Π°Ρ‚Π°"
@@ -1,371 +1,386 b''
1 from datetime import datetime, timedelta
1 from datetime import datetime, timedelta
2 from datetime import time as dtime
2 from datetime import time as dtime
3 import os
3 import os
4 from random import random
4 from random import random
5 import time
5 import time
6 import math
6 import math
7 import re
7 import re
8 import hashlib
9
8 from django.core.cache import cache
10 from django.core.cache import cache
9 from django.core.paginator import Paginator
11 from django.core.paginator import Paginator
10
12
11 from django.db import models
13 from django.db import models
12 from django.http import Http404
14 from django.http import Http404
13 from django.utils import timezone
15 from django.utils import timezone
14 from markupfield.fields import MarkupField
16 from markupfield.fields import MarkupField
15
17
16 from neboard import settings
18 from neboard import settings
17 from boards import thumbs
19 from boards import thumbs
18
20
19 MAX_TITLE_LENGTH = 50
21 MAX_TITLE_LENGTH = 50
20
22
21 APP_LABEL_BOARDS = 'boards'
23 APP_LABEL_BOARDS = 'boards'
22
24
23 CACHE_KEY_PPD = 'ppd'
25 CACHE_KEY_PPD = 'ppd'
24
26
25 POSTS_PER_DAY_RANGE = range(7)
27 POSTS_PER_DAY_RANGE = range(7)
26
28
27 BAN_REASON_AUTO = 'Auto'
29 BAN_REASON_AUTO = 'Auto'
28
30
29 IMAGE_THUMB_SIZE = (200, 150)
31 IMAGE_THUMB_SIZE = (200, 150)
30
32
31 TITLE_MAX_LENGTH = 50
33 TITLE_MAX_LENGTH = 50
32
34
33 DEFAULT_MARKUP_TYPE = 'markdown'
35 DEFAULT_MARKUP_TYPE = 'markdown'
34
36
35 NO_PARENT = -1
37 NO_PARENT = -1
36 NO_IP = '0.0.0.0'
38 NO_IP = '0.0.0.0'
37 UNKNOWN_UA = ''
39 UNKNOWN_UA = ''
38 ALL_PAGES = -1
40 ALL_PAGES = -1
39 IMAGES_DIRECTORY = 'images/'
41 IMAGES_DIRECTORY = 'images/'
40 FILE_EXTENSION_DELIMITER = '.'
42 FILE_EXTENSION_DELIMITER = '.'
41
43
42 SETTING_MODERATE = "moderate"
44 SETTING_MODERATE = "moderate"
43
45
44 REGEX_REPLY = re.compile('>>(\d+)')
46 REGEX_REPLY = re.compile('>>(\d+)')
45
47
46
48
47 class PostManager(models.Manager):
49 class PostManager(models.Manager):
48
50
49 def create_post(self, title, text, image=None, thread=None,
51 def create_post(self, title, text, image=None, thread=None,
50 ip=NO_IP, tags=None, user=None):
52 ip=NO_IP, tags=None, user=None):
51 """
53 """
52 Create new post
54 Create new post
53 """
55 """
54
56
55 posting_time = timezone.now()
57 posting_time = timezone.now()
56 if not thread:
58 if not thread:
57 thread = Thread.objects.create(bump_time=posting_time,
59 thread = Thread.objects.create(bump_time=posting_time,
58 last_edit_time=posting_time)
60 last_edit_time=posting_time)
59 else:
61 else:
60 thread.bump()
62 thread.bump()
61 thread.last_edit_time = posting_time
63 thread.last_edit_time = posting_time
62 thread.save()
64 thread.save()
63
65
64 post = self.create(title=title,
66 post = self.create(title=title,
65 text=text,
67 text=text,
66 pub_time=posting_time,
68 pub_time=posting_time,
67 thread_new=thread,
69 thread_new=thread,
68 image=image,
70 image=image,
69 poster_ip=ip,
71 poster_ip=ip,
70 poster_user_agent=UNKNOWN_UA, # TODO Get UA at
72 poster_user_agent=UNKNOWN_UA, # TODO Get UA at
71 # last!
73 # last!
72 last_edit_time=posting_time,
74 last_edit_time=posting_time,
73 user=user)
75 user=user)
74
76
75 thread.replies.add(post)
77 thread.replies.add(post)
76 if tags:
78 if tags:
77 linked_tags = []
79 linked_tags = []
78 for tag in tags:
80 for tag in tags:
79 tag_linked_tags = tag.get_linked_tags()
81 tag_linked_tags = tag.get_linked_tags()
80 if len(tag_linked_tags) > 0:
82 if len(tag_linked_tags) > 0:
81 linked_tags.extend(tag_linked_tags)
83 linked_tags.extend(tag_linked_tags)
82
84
83 tags.extend(linked_tags)
85 tags.extend(linked_tags)
84 map(thread.add_tag, tags)
86 map(thread.add_tag, tags)
85
87
86 self._delete_old_threads()
88 self._delete_old_threads()
87 self.connect_replies(post)
89 self.connect_replies(post)
88
90
89 return post
91 return post
90
92
91 def delete_post(self, post):
93 def delete_post(self, post):
92 """
94 """
93 Delete post and update or delete its thread
95 Delete post and update or delete its thread
94 """
96 """
95
97
96 thread = post.thread_new
98 thread = post.thread_new
97
99
98 if thread.get_opening_post() == self:
100 if thread.get_opening_post() == self:
99 thread.replies.delete()
101 thread.replies.delete()
100
102
101 thread.delete()
103 thread.delete()
102 else:
104 else:
103 thread.last_edit_time = timezone.now()
105 thread.last_edit_time = timezone.now()
104 thread.save()
106 thread.save()
105
107
106 post.delete()
108 post.delete()
107
109
108 def delete_posts_by_ip(self, ip):
110 def delete_posts_by_ip(self, ip):
109 """
111 """
110 Delete all posts of the author with same IP
112 Delete all posts of the author with same IP
111 """
113 """
112
114
113 posts = self.filter(poster_ip=ip)
115 posts = self.filter(poster_ip=ip)
114 map(self.delete_post, posts)
116 map(self.delete_post, posts)
115
117
116 # TODO Move this method to thread manager
118 # TODO Move this method to thread manager
117 def get_threads(self, tag=None, page=ALL_PAGES,
119 def get_threads(self, tag=None, page=ALL_PAGES,
118 order_by='-bump_time', archived=False):
120 order_by='-bump_time', archived=False):
119 if tag:
121 if tag:
120 threads = tag.threads
122 threads = tag.threads
121
123
122 if not threads.exists():
124 if not threads.exists():
123 raise Http404
125 raise Http404
124 else:
126 else:
125 threads = Thread.objects.all()
127 threads = Thread.objects.all()
126
128
127 threads = threads.filter(archived=archived).order_by(order_by)
129 threads = threads.filter(archived=archived).order_by(order_by)
128
130
129 if page != ALL_PAGES:
131 if page != ALL_PAGES:
130 threads = Paginator(threads, settings.THREADS_PER_PAGE).page(
132 threads = Paginator(threads, settings.THREADS_PER_PAGE).page(
131 page).object_list
133 page).object_list
132
134
133 return threads
135 return threads
134
136
135 # TODO Move this method to thread manager
137 # TODO Move this method to thread manager
136 def _delete_old_threads(self):
138 def _delete_old_threads(self):
137 """
139 """
138 Preserves maximum thread count. If there are too many threads,
140 Preserves maximum thread count. If there are too many threads,
139 archive the old ones.
141 archive the old ones.
140 """
142 """
141
143
142 threads = self.get_threads()
144 threads = self.get_threads()
143 thread_count = threads.count()
145 thread_count = threads.count()
144
146
145 if thread_count > settings.MAX_THREAD_COUNT:
147 if thread_count > settings.MAX_THREAD_COUNT:
146 num_threads_to_delete = thread_count - settings.MAX_THREAD_COUNT
148 num_threads_to_delete = thread_count - settings.MAX_THREAD_COUNT
147 old_threads = threads[thread_count - num_threads_to_delete:]
149 old_threads = threads[thread_count - num_threads_to_delete:]
148
150
149 for thread in old_threads:
151 for thread in old_threads:
150 thread.archived = True
152 thread.archived = True
151 thread.last_edit_time = timezone.now()
153 thread.last_edit_time = timezone.now()
152 thread.save()
154 thread.save()
153
155
154 def connect_replies(self, post):
156 def connect_replies(self, post):
155 """
157 """
156 Connect replies to a post to show them as a reflink map
158 Connect replies to a post to show them as a reflink map
157 """
159 """
158
160
159 for reply_number in re.finditer(REGEX_REPLY, post.text.raw):
161 for reply_number in re.finditer(REGEX_REPLY, post.text.raw):
160 post_id = reply_number.group(1)
162 post_id = reply_number.group(1)
161 ref_post = self.filter(id=post_id)
163 ref_post = self.filter(id=post_id)
162 if ref_post.count() > 0:
164 if ref_post.count() > 0:
163 referenced_post = ref_post[0]
165 referenced_post = ref_post[0]
164 referenced_post.referenced_posts.add(post)
166 referenced_post.referenced_posts.add(post)
165 referenced_post.last_edit_time = post.pub_time
167 referenced_post.last_edit_time = post.pub_time
166 referenced_post.save()
168 referenced_post.save()
167
169
168 def get_posts_per_day(self):
170 def get_posts_per_day(self):
169 """
171 """
170 Get average count of posts per day for the last 7 days
172 Get average count of posts per day for the last 7 days
171 """
173 """
172
174
173 today = datetime.now().date()
175 today = datetime.now().date()
174 ppd = cache.get(CACHE_KEY_PPD + str(today))
176 ppd = cache.get(CACHE_KEY_PPD + str(today))
175 if ppd:
177 if ppd:
176 return ppd
178 return ppd
177
179
178 posts_per_days = []
180 posts_per_days = []
179 for i in POSTS_PER_DAY_RANGE:
181 for i in POSTS_PER_DAY_RANGE:
180 day_end = today - timedelta(i + 1)
182 day_end = today - timedelta(i + 1)
181 day_start = today - timedelta(i + 2)
183 day_start = today - timedelta(i + 2)
182
184
183 day_time_start = timezone.make_aware(datetime.combine(day_start,
185 day_time_start = timezone.make_aware(datetime.combine(day_start,
184 dtime()), timezone.get_current_timezone())
186 dtime()), timezone.get_current_timezone())
185 day_time_end = timezone.make_aware(datetime.combine(day_end,
187 day_time_end = timezone.make_aware(datetime.combine(day_end,
186 dtime()), timezone.get_current_timezone())
188 dtime()), timezone.get_current_timezone())
187
189
188 posts_per_days.append(float(self.filter(
190 posts_per_days.append(float(self.filter(
189 pub_time__lte=day_time_end,
191 pub_time__lte=day_time_end,
190 pub_time__gte=day_time_start).count()))
192 pub_time__gte=day_time_start).count()))
191
193
192 ppd = (sum(posts_per_day for posts_per_day in posts_per_days) /
194 ppd = (sum(posts_per_day for posts_per_day in posts_per_days) /
193 len(posts_per_days))
195 len(posts_per_days))
194 cache.set(CACHE_KEY_PPD, ppd)
196 cache.set(CACHE_KEY_PPD, ppd)
195 return ppd
197 return ppd
196
198
197
199
198 class Post(models.Model):
200 class Post(models.Model):
199 """A post is a message."""
201 """A post is a message."""
200
202
201 objects = PostManager()
203 objects = PostManager()
202
204
203 class Meta:
205 class Meta:
204 app_label = APP_LABEL_BOARDS
206 app_label = APP_LABEL_BOARDS
205
207
206 # TODO Save original file name to some field
208 # TODO Save original file name to some field
207 def _update_image_filename(self, filename):
209 def _update_image_filename(self, filename):
208 """Get unique image filename"""
210 """Get unique image filename"""
209
211
210 path = IMAGES_DIRECTORY
212 path = IMAGES_DIRECTORY
211 new_name = str(int(time.mktime(time.gmtime())))
213 new_name = str(int(time.mktime(time.gmtime())))
212 new_name += str(int(random() * 1000))
214 new_name += str(int(random() * 1000))
213 new_name += FILE_EXTENSION_DELIMITER
215 new_name += FILE_EXTENSION_DELIMITER
214 new_name += filename.split(FILE_EXTENSION_DELIMITER)[-1:][0]
216 new_name += filename.split(FILE_EXTENSION_DELIMITER)[-1:][0]
215
217
216 return os.path.join(path, new_name)
218 return os.path.join(path, new_name)
217
219
218 title = models.CharField(max_length=TITLE_MAX_LENGTH)
220 title = models.CharField(max_length=TITLE_MAX_LENGTH)
219 pub_time = models.DateTimeField()
221 pub_time = models.DateTimeField()
220 text = MarkupField(default_markup_type=DEFAULT_MARKUP_TYPE,
222 text = MarkupField(default_markup_type=DEFAULT_MARKUP_TYPE,
221 escape_html=False)
223 escape_html=False)
222
224
223 image_width = models.IntegerField(default=0)
225 image_width = models.IntegerField(default=0)
224 image_height = models.IntegerField(default=0)
226 image_height = models.IntegerField(default=0)
225
227
226 image_pre_width = models.IntegerField(default=0)
228 image_pre_width = models.IntegerField(default=0)
227 image_pre_height = models.IntegerField(default=0)
229 image_pre_height = models.IntegerField(default=0)
228
230
229 image = thumbs.ImageWithThumbsField(upload_to=_update_image_filename,
231 image = thumbs.ImageWithThumbsField(upload_to=_update_image_filename,
230 blank=True, sizes=(IMAGE_THUMB_SIZE,),
232 blank=True, sizes=(IMAGE_THUMB_SIZE,),
231 width_field='image_width',
233 width_field='image_width',
232 height_field='image_height',
234 height_field='image_height',
233 preview_width_field='image_pre_width',
235 preview_width_field='image_pre_width',
234 preview_height_field='image_pre_height')
236 preview_height_field='image_pre_height')
237 image_hash = models.CharField(max_length=36)
235
238
236 poster_ip = models.GenericIPAddressField()
239 poster_ip = models.GenericIPAddressField()
237 poster_user_agent = models.TextField()
240 poster_user_agent = models.TextField()
238
241
239 thread = models.ForeignKey('Post', null=True, default=None)
242 thread = models.ForeignKey('Post', null=True, default=None)
240 thread_new = models.ForeignKey('Thread', null=True, default=None)
243 thread_new = models.ForeignKey('Thread', null=True, default=None)
241 last_edit_time = models.DateTimeField()
244 last_edit_time = models.DateTimeField()
242 user = models.ForeignKey('User', null=True, default=None)
245 user = models.ForeignKey('User', null=True, default=None)
243
246
244 referenced_posts = models.ManyToManyField('Post', symmetrical=False,
247 referenced_posts = models.ManyToManyField('Post', symmetrical=False,
245 null=True,
248 null=True,
246 blank=True, related_name='rfp+')
249 blank=True, related_name='rfp+')
247
250
248 def __unicode__(self):
251 def __unicode__(self):
249 return '#' + str(self.id) + ' ' + self.title + ' (' + \
252 return '#' + str(self.id) + ' ' + self.title + ' (' + \
250 self.text.raw[:50] + ')'
253 self.text.raw[:50] + ')'
251
254
252 def get_title(self):
255 def get_title(self):
253 title = self.title
256 title = self.title
254 if len(title) == 0:
257 if len(title) == 0:
255 title = self.text.rendered
258 title = self.text.rendered
256
259
257 return title
260 return title
258
261
259 def get_sorted_referenced_posts(self):
262 def get_sorted_referenced_posts(self):
260 return self.referenced_posts.order_by('id')
263 return self.referenced_posts.order_by('id')
261
264
262 def is_referenced(self):
265 def is_referenced(self):
263 return self.referenced_posts.all().exists()
266 return self.referenced_posts.all().exists()
264
267
265 def is_opening(self):
268 def is_opening(self):
266 return self.thread_new.get_replies()[0] == self
269 return self.thread_new.get_replies()[0] == self
267
270
271 def save(self, *args, **kwargs):
272 """
273 Save the model and compute the image hash
274 """
275
276 if not self.pk and self.image:
277 md5 = hashlib.md5()
278 for chunk in self.image.chunks():
279 md5.update(chunk)
280 self.image_hash = md5.hexdigest()
281 super(Post, self).save(*args, **kwargs)
282
268
283
269 class Thread(models.Model):
284 class Thread(models.Model):
270
285
271 class Meta:
286 class Meta:
272 app_label = APP_LABEL_BOARDS
287 app_label = APP_LABEL_BOARDS
273
288
274 tags = models.ManyToManyField('Tag')
289 tags = models.ManyToManyField('Tag')
275 bump_time = models.DateTimeField()
290 bump_time = models.DateTimeField()
276 last_edit_time = models.DateTimeField()
291 last_edit_time = models.DateTimeField()
277 replies = models.ManyToManyField('Post', symmetrical=False, null=True,
292 replies = models.ManyToManyField('Post', symmetrical=False, null=True,
278 blank=True, related_name='tre+')
293 blank=True, related_name='tre+')
279 archived = models.BooleanField(default=False)
294 archived = models.BooleanField(default=False)
280
295
281 def get_tags(self):
296 def get_tags(self):
282 """
297 """
283 Get a sorted tag list
298 Get a sorted tag list
284 """
299 """
285
300
286 return self.tags.order_by('name')
301 return self.tags.order_by('name')
287
302
288 def bump(self):
303 def bump(self):
289 """
304 """
290 Bump (move to up) thread
305 Bump (move to up) thread
291 """
306 """
292
307
293 if self.can_bump():
308 if self.can_bump():
294 self.bump_time = timezone.now()
309 self.bump_time = timezone.now()
295
310
296 def get_reply_count(self):
311 def get_reply_count(self):
297 return self.replies.count()
312 return self.replies.count()
298
313
299 def get_images_count(self):
314 def get_images_count(self):
300 return self.replies.filter(image_width__gt=0).count()
315 return self.replies.filter(image_width__gt=0).count()
301
316
302 def can_bump(self):
317 def can_bump(self):
303 """
318 """
304 Check if the thread can be bumped by replying
319 Check if the thread can be bumped by replying
305 """
320 """
306
321
307 if self.archived:
322 if self.archived:
308 return False
323 return False
309
324
310 post_count = self.get_reply_count()
325 post_count = self.get_reply_count()
311
326
312 return post_count < settings.MAX_POSTS_PER_THREAD
327 return post_count < settings.MAX_POSTS_PER_THREAD
313
328
314 def delete_with_posts(self):
329 def delete_with_posts(self):
315 """
330 """
316 Completely delete thread and all its posts
331 Completely delete thread and all its posts
317 """
332 """
318
333
319 if self.replies.count() > 0:
334 if self.replies.count() > 0:
320 self.replies.all().delete()
335 self.replies.all().delete()
321
336
322 self.delete()
337 self.delete()
323
338
324 def get_last_replies(self):
339 def get_last_replies(self):
325 """
340 """
326 Get last replies, not including opening post
341 Get last replies, not including opening post
327 """
342 """
328
343
329 if settings.LAST_REPLIES_COUNT > 0:
344 if settings.LAST_REPLIES_COUNT > 0:
330 reply_count = self.get_reply_count()
345 reply_count = self.get_reply_count()
331
346
332 if reply_count > 0:
347 if reply_count > 0:
333 reply_count_to_show = min(settings.LAST_REPLIES_COUNT,
348 reply_count_to_show = min(settings.LAST_REPLIES_COUNT,
334 reply_count - 1)
349 reply_count - 1)
335 last_replies = self.replies.all().order_by('pub_time')[
350 last_replies = self.replies.all().order_by('pub_time')[
336 reply_count - reply_count_to_show:]
351 reply_count - reply_count_to_show:]
337
352
338 return last_replies
353 return last_replies
339
354
340 def get_replies(self):
355 def get_replies(self):
341 """
356 """
342 Get sorted thread posts
357 Get sorted thread posts
343 """
358 """
344
359
345 return self.replies.all().order_by('pub_time')
360 return self.replies.all().order_by('pub_time')
346
361
347 def add_tag(self, tag):
362 def add_tag(self, tag):
348 """
363 """
349 Connect thread to a tag and tag to a thread
364 Connect thread to a tag and tag to a thread
350 """
365 """
351
366
352 self.tags.add(tag)
367 self.tags.add(tag)
353 tag.threads.add(self)
368 tag.threads.add(self)
354
369
355 def get_opening_post(self):
370 def get_opening_post(self):
356 """
371 """
357 Get first post of the thread
372 Get first post of the thread
358 """
373 """
359
374
360 return self.get_replies()[0]
375 return self.get_replies()[0]
361
376
362 def __unicode__(self):
377 def __unicode__(self):
363 return str(self.get_replies()[0].id)
378 return str(self.get_replies()[0].id)
364
379
365 def get_pub_time(self):
380 def get_pub_time(self):
366 """
381 """
367 Thread does not have its own pub time, so we need to get it from
382 Thread does not have its own pub time, so we need to get it from
368 the opening post
383 the opening post
369 """
384 """
370
385
371 return self.get_opening_post().pub_time
386 return self.get_opening_post().pub_time
@@ -1,216 +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 cStringIO
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 io = cStringIO.StringIO()
55 # PNG and GIF are the same, JPG is JPEG
55 # PNG and GIF are the same, JPG is JPEG
56 if format.upper() == 'JPG':
56 if format.upper() == 'JPG':
57 format = 'JPEG'
57 format = 'JPEG'
58
58
59 image2.save(io, format)
59 image2.save(io, format)
60 return ContentFile(io.getvalue())
60 return ContentFile(io.getvalue())
61
61
62
62
63 class ImageWithThumbsFieldFile(ImageFieldFile):
63 class ImageWithThumbsFieldFile(ImageFieldFile):
64 """
64 """
65 See ImageWithThumbsField for usage example
65 See ImageWithThumbsField for usage example
66 """
66 """
67
67
68 def __init__(self, *args, **kwargs):
68 def __init__(self, *args, **kwargs):
69 super(ImageWithThumbsFieldFile, self).__init__(*args, **kwargs)
69 super(ImageWithThumbsFieldFile, self).__init__(*args, **kwargs)
70 self.sizes = self.field.sizes
70 self.sizes = self.field.sizes
71
71
72 if self.sizes:
72 if self.sizes:
73 def get_size(self, size):
73 def get_size(self, size):
74 if not self:
74 if not self:
75 return ''
75 return ''
76 else:
76 else:
77 split = self.url.rsplit('.', 1)
77 split = self.url.rsplit('.', 1)
78 thumb_url = '%s.%sx%s.%s' % (split[0], w, h, split[1])
78 thumb_url = '%s.%sx%s.%s' % (split[0], w, h, split[1])
79 return thumb_url
79 return thumb_url
80
80
81 for size in self.sizes:
81 for size in self.sizes:
82 (w, h) = size
82 (w, h) = size
83 setattr(self, 'url_%sx%s' % (w, h), get_size(self, size))
83 setattr(self, 'url_%sx%s' % (w, h), get_size(self, size))
84
84
85 def save(self, name, content, save=True):
85 def save(self, name, content, save=True):
86 super(ImageWithThumbsFieldFile, self).save(name, content, save)
86 super(ImageWithThumbsFieldFile, self).save(name, content, save)
87
87
88 if self.sizes:
88 if self.sizes:
89 for size in self.sizes:
89 for size in self.sizes:
90 (w, h) = size
90 (w, h) = size
91 split = self.name.rsplit('.', 1)
91 split = self.name.rsplit('.', 1)
92 thumb_name = '%s.%sx%s.%s' % (split[0], w, h, split[1])
92 thumb_name = '%s.%sx%s.%s' % (split[0], w, h, split[1])
93
93
94 # you can use another thumbnailing function if you like
94 # you can use another thumbnailing function if you like
95 thumb_content = generate_thumb(content, size, split[1])
95 thumb_content = generate_thumb(content, size, split[1])
96
96
97 thumb_name_ = self.storage.save(thumb_name, thumb_content)
97 thumb_name_ = self.storage.save(thumb_name, thumb_content)
98
98
99 if not thumb_name == thumb_name_:
99 if not thumb_name == thumb_name_:
100 raise ValueError(
100 raise ValueError(
101 'There is already a file named %s' % thumb_name)
101 'There is already a file named %s' % thumb_name)
102
102
103 def delete(self, save=True):
103 def delete(self, save=True):
104 name = self.name
104 name = self.name
105 super(ImageWithThumbsFieldFile, self).delete(save)
105 super(ImageWithThumbsFieldFile, self).delete(save)
106 if self.sizes:
106 if self.sizes:
107 for size in self.sizes:
107 for size in self.sizes:
108 (w, h) = size
108 (w, h) = size
109 split = name.rsplit('.', 1)
109 split = name.rsplit('.', 1)
110 thumb_name = '%s.%sx%s.%s' % (split[0], w, h, split[1])
110 thumb_name = '%s.%sx%s.%s' % (split[0], w, h, split[1])
111 try:
111 try:
112 self.storage.delete(thumb_name)
112 self.storage.delete(thumb_name)
113 except:
113 except:
114 pass
114 pass
115
115
116
116
117 class ImageWithThumbsField(ImageField):
117 class ImageWithThumbsField(ImageField):
118 attr_class = ImageWithThumbsFieldFile
118 attr_class = ImageWithThumbsFieldFile
119 """
119 """
120 Usage example:
120 Usage example:
121 ==============
121 ==============
122 photo = ImageWithThumbsField(upload_to='images', sizes=((125,125),(300,200),)
122 photo = ImageWithThumbsField(upload_to='images', sizes=((125,125),(300,200),)
123
123
124 To retrieve image URL, exactly the same way as with ImageField:
124 To retrieve image URL, exactly the same way as with ImageField:
125 my_object.photo.url
125 my_object.photo.url
126 To retrieve thumbnails URL's just add the size to it:
126 To retrieve thumbnails URL's just add the size to it:
127 my_object.photo.url_125x125
127 my_object.photo.url_125x125
128 my_object.photo.url_300x200
128 my_object.photo.url_300x200
129
129
130 Note: The 'sizes' attribute is not required. If you don't provide it,
130 Note: The 'sizes' attribute is not required. If you don't provide it,
131 ImageWithThumbsField will act as a normal ImageField
131 ImageWithThumbsField will act as a normal ImageField
132
132
133 How it works:
133 How it works:
134 =============
134 =============
135 For each size in the 'sizes' atribute of the field it generates a
135 For each size in the 'sizes' atribute of the field it generates a
136 thumbnail with that size and stores it following this format:
136 thumbnail with that size and stores it following this format:
137
137
138 available_filename.[width]x[height].extension
138 available_filename.[width]x[height].extension
139
139
140 Where 'available_filename' is the available filename returned by the storage
140 Where 'available_filename' is the available filename returned by the storage
141 backend for saving the original file.
141 backend for saving the original file.
142
142
143 Following the usage example above: For storing a file called "photo.jpg" it saves:
143 Following the usage example above: For storing a file called "photo.jpg" it saves:
144 photo.jpg (original file)
144 photo.jpg (original file)
145 photo.125x125.jpg (first thumbnail)
145 photo.125x125.jpg (first thumbnail)
146 photo.300x200.jpg (second thumbnail)
146 photo.300x200.jpg (second thumbnail)
147
147
148 With the default storage backend if photo.jpg already exists it will use these filenames:
148 With the default storage backend if photo.jpg already exists it will use these filenames:
149 photo_.jpg
149 photo_.jpg
150 photo_.125x125.jpg
150 photo_.125x125.jpg
151 photo_.300x200.jpg
151 photo_.300x200.jpg
152
152
153 Note: django-thumbs assumes that if filename "any_filename.jpg" is available
153 Note: django-thumbs assumes that if filename "any_filename.jpg" is available
154 filenames with this format "any_filename.[widht]x[height].jpg" will be available, too.
154 filenames with this format "any_filename.[widht]x[height].jpg" will be available, too.
155
155
156 To do:
156 To do:
157 ======
157 ======
158 Add method to regenerate thubmnails
158 Add method to regenerate thubmnails
159
159
160
160
161 """
161 """
162
162
163 preview_width_field = None
164 preview_height_field = None
165
163 def __init__(self, verbose_name=None, name=None, width_field=None,
166 def __init__(self, verbose_name=None, name=None, width_field=None,
164 height_field=None, sizes=None,
167 height_field=None, sizes=None,
165 preview_width_field=None, preview_height_field=None,
168 preview_width_field=None, preview_height_field=None,
166 **kwargs):
169 **kwargs):
167 self.verbose_name = verbose_name
170 self.verbose_name = verbose_name
168 self.name = name
171 self.name = name
169 self.width_field = width_field
172 self.width_field = width_field
170 self.height_field = height_field
173 self.height_field = height_field
171 self.sizes = sizes
174 self.sizes = sizes
172 super(ImageField, self).__init__(**kwargs)
175 super(ImageField, self).__init__(**kwargs)
173
176
174 if sizes is not None and len(sizes) == 1:
177 if sizes is not None and len(sizes) == 1:
175 self.preview_width_field = preview_width_field
178 self.preview_width_field = preview_width_field
176 self.preview_height_field = preview_height_field
179 self.preview_height_field = preview_height_field
177
180
178 def update_dimension_fields(self, instance, force=False, *args, **kwargs):
181 def update_dimension_fields(self, instance, force=False, *args, **kwargs):
179 """
182 """
180 Update original image dimension fields and thumb dimension fields
183 Update original image dimension fields and thumb dimension fields
181 (only if 1 thumb size is defined)
184 (only if 1 thumb size is defined)
182 """
185 """
183
186
184 super(ImageWithThumbsField, self).update_dimension_fields(instance,
187 super(ImageWithThumbsField, self).update_dimension_fields(instance,
185 force, *args,
188 force, *args,
186 **kwargs)
189 **kwargs)
187 thumb_width_field = self.preview_width_field
190 thumb_width_field = self.preview_width_field
188 thumb_height_field = self.preview_height_field
191 thumb_height_field = self.preview_height_field
189
192
190 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 \
191 or len(self.sizes) != 1:
194 or len(self.sizes) != 1:
192 return
195 return
193
196
194 original_width = getattr(instance, self.width_field)
197 original_width = getattr(instance, self.width_field)
195 original_height = getattr(instance, self.height_field)
198 original_height = getattr(instance, self.height_field)
196
199
197 if original_width > 0 and original_height > 0:
200 if original_width > 0 and original_height > 0:
198 thumb_width, thumb_height = self.sizes[0]
201 thumb_width, thumb_height = self.sizes[0]
199
202
200 w_scale = float(thumb_width) / original_width
203 w_scale = float(thumb_width) / original_width
201 h_scale = float(thumb_height) / original_height
204 h_scale = float(thumb_height) / original_height
202 scale_ratio = min(w_scale, h_scale)
205 scale_ratio = min(w_scale, h_scale)
203
206
204 if scale_ratio >= 1:
207 if scale_ratio >= 1:
205 thumb_width_ratio = original_width
208 thumb_width_ratio = original_width
206 thumb_height_ratio = original_height
209 thumb_height_ratio = original_height
207 else:
210 else:
208 thumb_width_ratio = int(original_width * scale_ratio)
211 thumb_width_ratio = int(original_width * scale_ratio)
209 thumb_height_ratio = int(original_height * scale_ratio)
212 thumb_height_ratio = int(original_height * scale_ratio)
210
213
211 setattr(instance, thumb_width_field, thumb_width_ratio)
214 setattr(instance, thumb_width_field, thumb_width_ratio)
212 setattr(instance, thumb_height_field, thumb_height_ratio)
215 setattr(instance, thumb_height_field, thumb_height_ratio)
213
216
214
217
215 from south.modelsinspector import add_introspection_rules
218 from south.modelsinspector import add_introspection_rules
216 add_introspection_rules([], ["^boards\.thumbs\.ImageWithThumbsField"])
219 add_introspection_rules([], ["^boards\.thumbs\.ImageWithThumbsField"])
General Comments 0
You need to be logged in to leave comments. Login now