##// END OF EJS Templates
Made tag name field unique. Added index to "required" field for tag
Made tag name field unique. Added index to "required" field for tag

File last commit:

r982:c170e0ea default
r983:c3e4aa3e default
Show More
forms.py
331 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'))
class SearchForm(NeboardForm):
query = forms.CharField(max_length=500, label=LABEL_SEARCH, required=False)