forms.py
355 lines
| 10.6 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
|
r716 | from boards.models import User, 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
|
r642 | user = User.objects.get(id=self.session['user_id']) | |
if user.is_veteran(): | |||
posting_delay = VETERAN_POSTING_DELAY | |||
else: | |||
posting_delay = settings.POSTING_DELAY | |||
neko259
|
r153 | if LAST_POST_TIME in self.session: | |
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
|
r205 | class ModeratorSettingsForm(SettingsForm): | |
moderate = forms.BooleanField(required=False, label=_('Enable moderation ' | |||
'panel')) | |||
class LoginForm(NeboardForm): | |||
neko259
|
r211 | ||
neko259
|
r144 | user_id = forms.CharField() | |
neko259
|
r211 | session = None | |
neko259
|
r144 | def clean_user_id(self): | |
user_id = self.cleaned_data['user_id'] | |||
if user_id: | |||
users = User.objects.filter(user_id=user_id) | |||
if len(users) == 0: | |||
neko259
|
r205 | raise forms.ValidationError(_('No such user found')) | |
neko259
|
r144 | ||
return user_id | |||
neko259
|
r211 | 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) | |||
neko259
|
r333 | if current_delay < board_settings.LOGIN_TIMEOUT: | |
neko259
|
r211 | error_message = _('Wait %s minutes after last login') % str( | |
neko259
|
r333 | (board_settings.LOGIN_TIMEOUT - current_delay) / 60) | |
neko259
|
r211 | self._errors['user_id'] = self.error_class([error_message]) | |
can_post = False | |||
if can_post: | |||
self.session[LAST_LOGIN_TIME] = time.time() | |||
neko259
|
r144 | def clean(self): | |
neko259
|
r211 | if not self.session: | |
raise forms.ValidationError('Humans have sessions') | |||
self._validate_login_speed() | |||
neko259
|
r144 | cleaned_data = super(LoginForm, self).clean() | |
neko259
|
r271 | return cleaned_data | |
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): | |||
query = forms.CharField(max_length=500, label=LABEL_SEARCH, required=False) |