forms.py
342 lines
| 10.2 KiB
| text/x-python
|
PythonLexer
/ boards / forms.py
neko259
|
r69 | import re | ||
neko259
|
r527 | import time | ||
import hashlib | ||||
Ilyas
|
r78 | from captcha.fields import CaptchaField | ||
Ilyas
|
r14 | from django import forms | ||
neko259
|
r76 | from django.forms.util import ErrorList | ||
neko259
|
r205 | from django.utils.translation import ugettext_lazy as _ | ||
neko259
|
r527 | |||
neko259
|
r438 | from boards.mdx_neboard import formatters | ||
neko259
|
r386 | from boards.models.post import TITLE_MAX_LENGTH | ||
neko259
|
r728 | from boards.models import PostImage | ||
neko259
|
r35 | from neboard import settings | ||
wnc_21
|
r95 | from boards import utils | ||
neko259
|
r333 | import boards.settings as board_settings | ||
neko259
|
r76 | |||
neko259
|
r642 | VETERAN_POSTING_DELAY = 5 | ||
neko259
|
r517 | ATTRIBUTE_PLACEHOLDER = 'placeholder' | ||
LAST_POST_TIME = 'last_post_time' | ||||
LAST_LOGIN_TIME = 'last_login_time' | ||||
TEXT_PLACEHOLDER = _('''Type message here. You can reply to message >>123 like | ||||
this. 2 new lines are required to start new paragraph.''') | ||||
neko259
|
r521 | TAGS_PLACEHOLDER = _('tag1 several_words_tag') | ||
neko259
|
r211 | |||
neko259
|
r527 | ERROR_IMAGE_DUPLICATE = _('Such image was already posted') | ||
LABEL_TITLE = _('Title') | ||||
LABEL_TEXT = _('Text') | ||||
neko259
|
r566 | LABEL_TAG = _('Tag') | ||
neko259
|
r718 | LABEL_SEARCH = _('Search') | ||
neko259
|
r566 | |||
TAG_MAX_LENGTH = 20 | ||||
REGEX_TAG = ur'^[\w\d]+$' | ||||
neko259
|
r527 | |||
neko259
|
r153 | |||
neko259
|
r438 | class FormatPanel(forms.Textarea): | ||
def render(self, name, value, attrs=None): | ||||
output = '<div id="mark-panel">' | ||||
for formatter in formatters: | ||||
output += u'<span class="mark_btn"' + \ | ||||
u' onClick="addMarkToMsg(\'' + formatter.format_left + \ | ||||
'\', \'' + formatter.format_right + '\')">' + \ | ||||
formatter.preview_left + formatter.name + \ | ||||
formatter.preview_right + u'</span>' | ||||
output += '</div>' | ||||
output += super(FormatPanel, self).render(name, value, attrs=None) | ||||
return output | ||||
neko259
|
r76 | class PlainErrorList(ErrorList): | ||
def __unicode__(self): | ||||
return self.as_text() | ||||
def as_text(self): | ||||
return ''.join([u'(!) %s ' % e for e in self]) | ||||
neko259
|
r16 | |||
neko259
|
r205 | class NeboardForm(forms.Form): | ||
neko259
|
r426 | def as_div(self): | ||
neko259
|
r438 | """ | ||
Returns this form rendered as HTML <as_div>s. | ||||
""" | ||||
neko259
|
r205 | return self._html_output( | ||
neko259
|
r425 | # TODO Do not show hidden rows in the list here | ||
neko259
|
r680 | normal_row='<div class="form-row"><div class="form-label">' | ||
neko259
|
r205 | '%(label)s' | ||
neko259
|
r680 | '</div></div>' | ||
'<div class="form-row"><div class="form-input">' | ||||
neko259
|
r205 | '%(field)s' | ||
neko259
|
r680 | '</div></div>' | ||
'<div class="form-row">' | ||||
neko259
|
r205 | '%(help_text)s' | ||
'</div>', | ||||
neko259
|
r426 | error_row='<div class="form-row">' | ||
'<div class="form-label"></div>' | ||||
'<div class="form-errors">%s</div>' | ||||
'</div>', | ||||
row_ender='</div>', | ||||
help_text_html='%s', | ||||
neko259
|
r205 | errors_on_separate_row=True) | ||
neko259
|
r533 | def as_json_errors(self): | ||
errors = [] | ||||
for name, field in self.fields.items(): | ||||
if self[name].errors: | ||||
errors.append({ | ||||
'field': name, | ||||
'errors': self[name].errors.as_text(), | ||||
}) | ||||
return errors | ||||
neko259
|
r438 | |||
neko259
|
r205 | class PostForm(NeboardForm): | ||
neko259
|
r76 | |||
neko259
|
r232 | title = forms.CharField(max_length=TITLE_MAX_LENGTH, required=False, | ||
neko259
|
r527 | label=LABEL_TITLE) | ||
neko259
|
r517 | text = forms.CharField( | ||
widget=FormatPanel(attrs={ATTRIBUTE_PLACEHOLDER: TEXT_PLACEHOLDER}), | ||||
neko259
|
r527 | required=False, label=LABEL_TEXT) | ||
neko259
|
r675 | image = forms.ImageField(required=False, label=_('Image'), | ||
neko259
|
r721 | widget=forms.ClearableFileInput( | ||
attrs={'accept': 'image/*'})) | ||||
neko259
|
r29 | |||
neko259
|
r207 | # This field is for spam prevention only | ||
neko259
|
r232 | email = forms.CharField(max_length=100, required=False, label=_('e-mail'), | ||
widget=forms.TextInput(attrs={ | ||||
'class': 'form-email'})) | ||||
neko259
|
r207 | |||
neko259
|
r153 | session = None | ||
neko259
|
r271 | need_to_ban = False | ||
neko259
|
r153 | |||
neko259
|
r76 | def clean_title(self): | ||
title = self.cleaned_data['title'] | ||||
if title: | ||||
if len(title) > TITLE_MAX_LENGTH: | ||||
neko259
|
r211 | raise forms.ValidationError(_('Title must have less than %s ' | ||
'characters') % | ||||
str(TITLE_MAX_LENGTH)) | ||||
neko259
|
r76 | return title | ||
neko259
|
r29 | def clean_text(self): | ||
neko259
|
r678 | text = self.cleaned_data['text'].strip() | ||
neko259
|
r29 | if text: | ||
neko259
|
r333 | if len(text) > board_settings.MAX_TEXT_LENGTH: | ||
neko259
|
r211 | raise forms.ValidationError(_('Text must have less than %s ' | ||
'characters') % | ||||
neko259
|
r333 | str(board_settings | ||
.MAX_TEXT_LENGTH)) | ||||
neko259
|
r29 | return text | ||
def clean_image(self): | ||||
image = self.cleaned_data['image'] | ||||
if image: | ||||
neko259
|
r721 | if image.size > board_settings.MAX_IMAGE_SIZE: | ||
neko259
|
r333 | raise forms.ValidationError( | ||
_('Image must be less than %s bytes') | ||||
% str(board_settings.MAX_IMAGE_SIZE)) | ||||
neko259
|
r527 | |||
md5 = hashlib.md5() | ||||
for chunk in image.chunks(): | ||||
md5.update(chunk) | ||||
image_hash = md5.hexdigest() | ||||
neko259
|
r693 | if PostImage.objects.filter(hash=image_hash).exists(): | ||
neko259
|
r527 | raise forms.ValidationError(ERROR_IMAGE_DUPLICATE) | ||
neko259
|
r29 | return image | ||
def clean(self): | ||||
cleaned_data = super(PostForm, self).clean() | ||||
neko259
|
r211 | if not self.session: | ||
raise forms.ValidationError('Humans have sessions') | ||||
if cleaned_data['email']: | ||||
neko259
|
r271 | self.need_to_ban = True | ||
neko259
|
r211 | raise forms.ValidationError('A human cannot enter a hidden field') | ||
neko259
|
r207 | |||
neko259
|
r151 | if not self.errors: | ||
self._clean_text_image() | ||||
neko259
|
r77 | |||
neko259
|
r153 | if not self.errors and self.session: | ||
self._validate_posting_speed() | ||||
neko259
|
r76 | return cleaned_data | ||
def _clean_text_image(self): | ||||
text = self.cleaned_data.get('text') | ||||
image = self.cleaned_data.get('image') | ||||
neko259
|
r29 | |||
if (not text) and (not image): | ||||
neko259
|
r205 | error_message = _('Either text or image must be entered.') | ||
neko259
|
r77 | self._errors['text'] = self.error_class([error_message]) | ||
neko259
|
r153 | |||
def _validate_posting_speed(self): | ||||
can_post = True | ||||
neko259
|
r648 | # TODO Remove this, it's only for test | ||
if not 'user_id' in self.session: | ||||
return | ||||
neko259
|
r728 | posting_delay = settings.POSTING_DELAY | ||
neko259
|
r642 | |||
neko259
|
r725 | if board_settings.LIMIT_POSTING_SPEED and LAST_POST_TIME in \ | ||
self.session: | ||||
neko259
|
r153 | now = time.time() | ||
last_post_time = self.session[LAST_POST_TIME] | ||||
current_delay = int(now - last_post_time) | ||||
neko259
|
r642 | if current_delay < posting_delay: | ||
neko259
|
r211 | error_message = _('Wait %s seconds after last posting') % str( | ||
neko259
|
r642 | posting_delay - current_delay) | ||
neko259
|
r153 | self._errors['text'] = self.error_class([error_message]) | ||
can_post = False | ||||
if can_post: | ||||
self.session[LAST_POST_TIME] = time.time() | ||||
neko259
|
r29 | |||
class ThreadForm(PostForm): | ||||
neko259
|
r232 | |||
neko259
|
r71 | regex_tags = re.compile(ur'^[\w\s\d]+$', re.UNICODE) | ||
neko259
|
r232 | |||
neko259
|
r517 | tags = forms.CharField( | ||
widget=forms.TextInput(attrs={ATTRIBUTE_PLACEHOLDER: TAGS_PLACEHOLDER}), | ||||
neko259
|
r679 | max_length=100, label=_('Tags'), required=True) | ||
neko259
|
r31 | |||
def clean_tags(self): | ||||
neko259
|
r678 | tags = self.cleaned_data['tags'].strip() | ||
neko259
|
r69 | |||
neko259
|
r679 | if not tags or not self.regex_tags.match(tags): | ||
raise forms.ValidationError( | ||||
_('Inappropriate characters in tags.')) | ||||
neko259
|
r31 | |||
return tags | ||||
def clean(self): | ||||
cleaned_data = super(ThreadForm, self).clean() | ||||
neko259
|
r35 | return cleaned_data | ||
wnc_21
|
r95 | class PostCaptchaForm(PostForm): | ||
captcha = CaptchaField() | ||||
def __init__(self, *args, **kwargs): | ||||
self.request = kwargs['request'] | ||||
del kwargs['request'] | ||||
super(PostCaptchaForm, self).__init__(*args, **kwargs) | ||||
def clean(self): | ||||
cleaned_data = super(PostCaptchaForm, self).clean() | ||||
success = self.is_valid() | ||||
utils.update_captcha_access(self.request, success) | ||||
if success: | ||||
return cleaned_data | ||||
else: | ||||
neko259
|
r205 | raise forms.ValidationError(_("Captcha validation failed")) | ||
wnc_21
|
r95 | |||
Ilyas
|
r78 | class ThreadCaptchaForm(ThreadForm): | ||
captcha = CaptchaField() | ||||
wnc_21
|
r95 | def __init__(self, *args, **kwargs): | ||
self.request = kwargs['request'] | ||||
del kwargs['request'] | ||||
super(ThreadCaptchaForm, self).__init__(*args, **kwargs) | ||||
def clean(self): | ||||
cleaned_data = super(ThreadCaptchaForm, self).clean() | ||||
success = self.is_valid() | ||||
utils.update_captcha_access(self.request, success) | ||||
if success: | ||||
return cleaned_data | ||||
else: | ||||
neko259
|
r205 | raise forms.ValidationError(_("Captcha validation failed")) | ||
wnc_21
|
r95 | |||
Ilyas
|
r78 | |||
neko259
|
r205 | class SettingsForm(NeboardForm): | ||
theme = forms.ChoiceField(choices=settings.THEMES, | ||||
label=_('Theme')) | ||||
neko259
|
r144 | |||
neko259
|
r566 | class AddTagForm(NeboardForm): | ||
tag = forms.CharField(max_length=TAG_MAX_LENGTH, label=LABEL_TAG) | ||||
method = forms.CharField(widget=forms.HiddenInput(), initial='add_tag') | ||||
def clean_tag(self): | ||||
tag = self.cleaned_data['tag'] | ||||
regex_tag = re.compile(REGEX_TAG, re.UNICODE) | ||||
if not regex_tag.match(tag): | ||||
raise forms.ValidationError(_('Inappropriate characters in tags.')) | ||||
return tag | ||||
def clean(self): | ||||
cleaned_data = super(AddTagForm, self).clean() | ||||
return cleaned_data | ||||
neko259
|
r718 | |||
class SearchForm(NeboardForm): | ||||
neko259
|
r729 | query = forms.CharField(max_length=500, label=LABEL_SEARCH, required=False) | ||
class LoginForm(NeboardForm): | ||||
password = forms.CharField() | ||||
session = None | ||||
def clean_password(self): | ||||
password = self.cleaned_data['password'] | ||||
if board_settings.MASTER_PASSWORD != password: | ||||
raise forms.ValidationError(_('Invalid master password')) | ||||
return password | ||||
def _validate_login_speed(self): | ||||
can_post = True | ||||
if LAST_LOGIN_TIME in self.session: | ||||
now = time.time() | ||||
last_login_time = self.session[LAST_LOGIN_TIME] | ||||
current_delay = int(now - last_login_time) | ||||
if current_delay < board_settings.LOGIN_TIMEOUT: | ||||
error_message = _('Wait %s minutes after last login') % str( | ||||
(board_settings.LOGIN_TIMEOUT - current_delay) / 60) | ||||
self._errors['password'] = self.error_class([error_message]) | ||||
can_post = False | ||||
if can_post: | ||||
self.session[LAST_LOGIN_TIME] = time.time() | ||||
def clean(self): | ||||
self._validate_login_speed() | ||||
cleaned_data = super(LoginForm, self).clean() | ||||
return cleaned_data | ||||