##// END OF EJS Templates
Bumped version and updated changelog
Bumped version and updated changelog

File last commit:

r990:28175b48 default
r991:97eb1846 2.5.0 default
Show More
forms.py
332 lines | 10.5 KiB | text/x-python | PythonLexer
import re
import time
from django import forms
from django.core.files.uploadedfile import SimpleUploadedFile
from django.forms.util import ErrorList
from django.utils.translation import ugettext_lazy as _
import requests
from boards.mdx_neboard import formatters
from boards.models.post import TITLE_MAX_LENGTH
from boards.models import Tag
from neboard import settings
import boards.settings as board_settings
CONTENT_TYPE_IMAGE = (
'image/jpeg',
'image/png',
'image/gif',
'image/bmp',
)
REGEX_TAGS = re.compile(r'^[\w\s\d]+$', re.UNICODE)
VETERAN_POSTING_DELAY = 5
ATTRIBUTE_PLACEHOLDER = 'placeholder'
LAST_POST_TIME = 'last_post_time'
LAST_LOGIN_TIME = 'last_login_time'
TEXT_PLACEHOLDER = _('Type message here. Use formatting panel for more advanced usage.')
TAGS_PLACEHOLDER = _('tag1 several_words_tag')
LABEL_TITLE = _('Title')
LABEL_TEXT = _('Text')
LABEL_TAG = _('Tag')
LABEL_SEARCH = _('Search')
TAG_MAX_LENGTH = 20
IMAGE_DOWNLOAD_CHUNK_BYTES = 100000
HTTP_RESULT_OK = 200
class FormatPanel(forms.Textarea):
"""
Panel for text formatting. Consists of buttons to add different tags to the
form text area.
"""
def render(self, name, value, attrs=None):
output = '<div id="mark-panel">'
for formatter in formatters:
output += '<span class="mark_btn"' + \
' onClick="addMarkToMsg(\'' + formatter.format_left + \
'\', \'' + formatter.format_right + '\')">' + \
formatter.preview_left + formatter.name + \
formatter.preview_right + '</span>'
output += '</div>'
output += super(FormatPanel, self).render(name, value, attrs=None)
return output
class PlainErrorList(ErrorList):
def __unicode__(self):
return self.as_text()
def as_text(self):
return ''.join(['(!) %s ' % e for e in self])
class NeboardForm(forms.Form):
"""
Form with neboard-specific formatting.
"""
def as_div(self):
"""
Returns this form rendered as HTML <as_div>s.
"""
return self._html_output(
# TODO Do not show hidden rows in the list here
normal_row='<div class="form-row"><div class="form-label">'
'%(label)s'
'</div></div>'
'<div class="form-row"><div class="form-input">'
'%(field)s'
'</div></div>'
'<div class="form-row">'
'%(help_text)s'
'</div>',
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',
errors_on_separate_row=True)
def as_json_errors(self):
errors = []
for name, field in list(self.fields.items()):
if self[name].errors:
errors.append({
'field': name,
'errors': self[name].errors.as_text(),
})
return errors
class PostForm(NeboardForm):
title = forms.CharField(max_length=TITLE_MAX_LENGTH, required=False,
label=LABEL_TITLE)
text = forms.CharField(
widget=FormatPanel(attrs={ATTRIBUTE_PLACEHOLDER: TEXT_PLACEHOLDER}),
required=False, label=LABEL_TEXT)
image = forms.ImageField(required=False, label=_('Image'),
widget=forms.ClearableFileInput(
attrs={'accept': 'image/*'}))
image_url = forms.CharField(required=False, label=_('Image URL'),
widget=forms.TextInput(
attrs={ATTRIBUTE_PLACEHOLDER:
'http://example.com/image.png'}))
# This field is for spam prevention only
email = forms.CharField(max_length=100, required=False, label=_('e-mail'),
widget=forms.TextInput(attrs={
'class': 'form-email'}))
session = None
need_to_ban = False
def clean_title(self):
title = self.cleaned_data['title']
if title:
if len(title) > TITLE_MAX_LENGTH:
raise forms.ValidationError(_('Title must have less than %s '
'characters') %
str(TITLE_MAX_LENGTH))
return title
def clean_text(self):
text = self.cleaned_data['text'].strip()
if text:
if len(text) > board_settings.MAX_TEXT_LENGTH:
raise forms.ValidationError(_('Text must have less than %s '
'characters') %
str(board_settings
.MAX_TEXT_LENGTH))
return text
def clean_image(self):
image = self.cleaned_data['image']
self._validate_image(image)
return image
def clean_image_url(self):
url = self.cleaned_data['image_url']
image = None
if url:
image = self._get_image_from_url(url)
if not image:
raise forms.ValidationError(_('Invalid URL'))
self._validate_image(image)
return image
def clean(self):
cleaned_data = super(PostForm, self).clean()
if not self.session:
raise forms.ValidationError('Humans have sessions')
if cleaned_data['email']:
self.need_to_ban = True
raise forms.ValidationError('A human cannot enter a hidden field')
if not self.errors:
self._clean_text_image()
if not self.errors and self.session:
self._validate_posting_speed()
return cleaned_data
def get_image(self):
"""
Gets image from file or URL.
"""
image = self.cleaned_data['image']
return image if image else self.cleaned_data['image_url']
def _clean_text_image(self):
text = self.cleaned_data.get('text')
image = self.get_image()
if (not text) and (not image):
error_message = _('Either text or image must be entered.')
self._errors['text'] = self.error_class([error_message])
def _validate_image(self, image):
if image:
if image.size > board_settings.MAX_IMAGE_SIZE:
raise forms.ValidationError(
_('Image must be less than %s bytes')
% str(board_settings.MAX_IMAGE_SIZE))
def _validate_posting_speed(self):
can_post = True
posting_delay = settings.POSTING_DELAY
if board_settings.LIMIT_POSTING_SPEED and LAST_POST_TIME in \
self.session:
now = time.time()
last_post_time = self.session[LAST_POST_TIME]
current_delay = int(now - last_post_time)
if current_delay < posting_delay:
error_message = _('Wait %s seconds after last posting') % str(
posting_delay - current_delay)
self._errors['text'] = self.error_class([error_message])
can_post = False
if can_post:
self.session[LAST_POST_TIME] = time.time()
def _get_image_from_url(self, url: str) -> SimpleUploadedFile:
"""
Gets an image file from URL.
"""
img_temp = None
try:
# Verify content headers
response_head = requests.head(url, verify=False)
content_type = response_head.headers['content-type'].split(';')[0]
if content_type in CONTENT_TYPE_IMAGE:
length_header = response_head.headers.get('content-length')
if length_header:
length = int(length_header)
if length > board_settings.MAX_IMAGE_SIZE:
raise forms.ValidationError(
_('Image must be less than %s bytes')
% str(board_settings.MAX_IMAGE_SIZE))
# Get the actual content into memory
response = requests.get(url, verify=False, stream=True)
# Download image, stop if the size exceeds limit
size = 0
content = b''
for chunk in response.iter_content(IMAGE_DOWNLOAD_CHUNK_BYTES):
size += len(chunk)
if size > board_settings.MAX_IMAGE_SIZE:
# TODO Dedup this code into a method
raise forms.ValidationError(
_('Image must be less than %s bytes')
% str(board_settings.MAX_IMAGE_SIZE))
content += chunk
if response.status_code == HTTP_RESULT_OK and content:
# Set a dummy file name that will be replaced
# anyway, just keep the valid extension
filename = 'image.' + content_type.split('/')[1]
img_temp = SimpleUploadedFile(filename, content,
content_type)
except Exception:
# Just return no image
pass
return img_temp
class ThreadForm(PostForm):
tags = forms.CharField(
widget=forms.TextInput(attrs={ATTRIBUTE_PLACEHOLDER: TAGS_PLACEHOLDER}),
max_length=100, label=_('Tags'), required=True)
def clean_tags(self):
tags = self.cleaned_data['tags'].strip()
if not tags or not REGEX_TAGS.match(tags):
raise forms.ValidationError(
_('Inappropriate characters in tags.'))
required_tag_exists = False
for tag in tags.split():
tag_model = Tag.objects.filter(name=tag.strip().lower(),
required=True)
if tag_model.exists():
required_tag_exists = True
break
if not required_tag_exists:
raise forms.ValidationError(_('Need at least 1 required tag.'))
return tags
def clean(self):
cleaned_data = super(ThreadForm, self).clean()
return cleaned_data
class SettingsForm(NeboardForm):
theme = forms.ChoiceField(choices=settings.THEMES,
label=_('Theme'))
username = forms.CharField(label=_('User name'), required=False)
class SearchForm(NeboardForm):
query = forms.CharField(max_length=500, label=LABEL_SEARCH, required=False)