view forums/forms.py @ 955:71a671dab55d

First commit of whitelisting image hosts. This is behind a feature flag courtesy of waffle.
author Brian Neal <bgneal@gmail.com>
date Wed, 03 Jun 2015 21:13:08 -0500
parents 5366c29d6dce
children
line wrap: on
line source
"""
Forms for the forums application.

"""
from collections import OrderedDict

from django import forms
from django.conf import settings

from core.markup import site_markup
from forums.models import Forum
from forums.models import Topic
from forums.models import Post
from forums.attachments import AttachmentProcessor
import forums.permissions as perms
from forums.signals import notify_new_topic, notify_new_post
from core.html import ImageCheckError
from core.html import image_check


FORUMS_FORM_CSS = {
    'all': (settings.GPP_THIRD_PARTY_CSS['markitup'] +
            settings.GPP_THIRD_PARTY_CSS['jquery-ui'])
}
FORUMS_FORM_JS = (
    settings.GPP_THIRD_PARTY_JS['markitup'] +
    settings.GPP_THIRD_PARTY_JS['jquery-ui'] +
    ['js/jquery.form.min.js', 'js/forums.js']
)


class NewPostForm(forms.Form):
    """Form for creating a new post."""
    body = forms.CharField(label='',
            required=False,
            widget=forms.Textarea(attrs={'class': 'markItUp smileyTarget'}))
    topic_id = forms.IntegerField(widget=forms.HiddenInput)
    topic = None

    class Media:
        css = FORUMS_FORM_CSS
        js = FORUMS_FORM_JS

    def __init__(self, *args, **kwargs):
        super(NewPostForm, self).__init__(*args, **kwargs)
        attachments = args[0].getlist('attachment') if len(args) else []
        self.attach_proc = AttachmentProcessor(attachments)

    def clean_body(self):
        data = self.cleaned_data['body']
        if not data and not self.attach_proc.has_attachments():
            raise forms.ValidationError("This field is required.")
        return data

    def clean_topic_id(self):
        id = self.cleaned_data['topic_id']
        try:
            self.topic = Topic.objects.select_related().get(pk=id)
        except Topic.DoesNotExist:
            raise forms.ValidationError('invalid topic')
        return id

    def save(self, user, ip=None):
        """
        Creates a new post from the form data and supplied arguments.
        """
        post = Post(topic=self.topic, user=user, body=self.cleaned_data['body'],
                user_ip=ip)
        post.save()
        self.attach_proc.save_attachments(post)
        notify_new_post(post)
        return post


class NewTopicForm(forms.Form):
    """
    Form for creating a new topic and 1st post to that topic.
    Superusers and moderators can also create the topic as a sticky or initially
    locked.
    """
    name = forms.CharField(label='Subject', max_length=255,
            widget=forms.TextInput(attrs={'style': 'width:70%'}))
    body = forms.CharField(label='', required=False,
            widget=forms.Textarea(attrs={'class': 'markItUp smileyTarget'}))
    user = None
    forum = None
    has_mod_fields = False

    class Media:
        css = FORUMS_FORM_CSS
        js = FORUMS_FORM_JS

    def __init__(self, user, forum, *args, **kwargs):
        super(NewTopicForm, self).__init__(*args, **kwargs)
        self.user = user
        self.forum = forum

        if perms.can_moderate(forum, user):
            self.fields['sticky'] = forms.BooleanField(required=False)
            self.fields['locked'] = forms.BooleanField(required=False)
            self.has_mod_fields = True

        attachments = args[0].getlist('attachment') if len(args) else []
        self.attach_proc = AttachmentProcessor(attachments)

        # If this form is being POSTed, and the user is trying to add 
        # attachments, create hidden fields to list the Oembed ids. In
        # case the form isn't valid, the client-side javascript will know
        # which Oembed media to ask for when the form is displayed with
        # errors.
        if self.attach_proc.has_attachments():
            pks = self.attach_proc.get_ids()
            self.fields['attachment'] = forms.MultipleChoiceField(label='',
                    widget=forms.MultipleHiddenInput(),
                    choices=[(v, v) for v in pks])

    def clean_body(self):
        data = self.cleaned_data['body']
        if not data and not self.attach_proc.has_attachments():
            raise forms.ValidationError("This field is required.")
        return data

    def save(self, ip=None, html=None):
        """
        Creates the new Topic and first Post from the form data and supplied
        arguments.
        """
        topic = Topic(forum=self.forum,
                name=self.cleaned_data['name'],
                user=self.user,
                sticky=self.has_mod_fields and self.cleaned_data['sticky'],
                locked=self.has_mod_fields and self.cleaned_data['locked'])
        topic.save()

        post = Post(topic=topic,
                user=self.user,
                body=self.cleaned_data['body'],
                user_ip=ip)
        post.save(html=html)

        self.attach_proc.save_attachments(post)

        notify_new_topic(topic)
        notify_new_post(post)

        return topic


class NewTopicFormS3(NewTopicForm):
    """Form for ensuring image sources come from a white-listed set of
    sources.
    """
    def clean_body(self):
        body_data = self.cleaned_data['body']
        self.body_html = site_markup(body_data)
        try:
            image_check(self.body_html)
        except ImageCheckError as ex:
            raise forms.ValidationError(str(ex))
        return body_data

    def save(self, ip=None):
        return super(NewTopicFormS3, self).save(ip, html=self.body_html)


class PostForm(forms.ModelForm):
    """
    Form for editing an existing post or a new, non-quick post.
    """
    body = forms.CharField(label='',
            required=False,
            widget=forms.Textarea(attrs={'class': 'markItUp smileyTarget'}))

    class Meta:
        model = Post
        fields = ('body', )

    class Media:
        css = FORUMS_FORM_CSS
        js = FORUMS_FORM_JS

    def __init__(self, *args, **kwargs):
        topic_name = kwargs.pop('topic_name', None)
        super(PostForm, self).__init__(*args, **kwargs)

        if topic_name is not None:
            # this is a "first post", add a field for a topic name
            new_fields = OrderedDict(name=forms.CharField(
                            label='Subject', max_length=255,
                            widget=forms.TextInput(attrs={
                                'class': 'title',
                                'style': 'width: 90%',
                            })))
            new_fields.update(self.fields)
            self.fields = new_fields
            self.initial['name'] = topic_name

        attachments = args[0].getlist('attachment') if len(args) else []
        self.attach_proc = AttachmentProcessor(attachments)

        # If this form is being used to edit an existing post, and that post
        # has attachments, create a hidden post_id field. The client-side
        # AJAX will use this as a cue to retrieve the HTML for the embedded
        # media.
        if 'instance' in kwargs:
            post = kwargs['instance']
            if post.attachments.count():
                self.fields['post_id'] = forms.CharField(label='',
                        widget=forms.HiddenInput(attrs={'value': post.id}))

    def clean_body(self):
        data = self.cleaned_data['body']
        if not data and not self.attach_proc.has_attachments():
            raise forms.ValidationError('This field is required.')
        return data

    def save(self, *args, **kwargs):
        commit = kwargs.get('commit', False)
        post = super(PostForm, self).save(*args, **kwargs)

        # Are we saving a "first post"?
        if 'name' in self.cleaned_data:
            post.topic.name = self.cleaned_data['name']
            if commit:
                post.topic.save()
        return post


class MoveTopicForm(forms.Form):
    """
    Form for a moderator to move a topic to a forum.
    """
    forums = forms.ModelChoiceField(label='Move to forum',
          queryset=Forum.objects.none())

    def __init__(self, user, *args, **kwargs):
        hide_label = kwargs.pop('hide_label', False)
        required = kwargs.pop('required', True)
        super(MoveTopicForm, self).__init__(*args, **kwargs)
        self.fields['forums'].queryset = \
            Forum.objects.forums_for_user(user).order_by('name')
        if hide_label:
            self.fields['forums'].label = ''
        self.fields['forums'].required = required


class SplitTopicForm(forms.Form):
    """
    Form for a moderator to split posts from a topic to a new topic.
    """
    name = forms.CharField(label='New topic title', max_length=255,
            widget=forms.TextInput(attrs={'size': 64}))
    forums = forms.ModelChoiceField(label='Forum for new topic',
          queryset=Forum.objects.none())
    post_ids = []
    split_at = False

    def __init__(self, user, *args, **kwargs):
        super(SplitTopicForm, self).__init__(*args, **kwargs)
        self.fields['forums'].queryset = \
            Forum.objects.forums_for_user(user).order_by('name')

    def clean(self):
        self.post_ids = self.data.getlist('post_ids')
        if len(self.post_ids) == 0:
            raise forms.ValidationError('Please select some posts')

        self.split_at = 'split-at' in self.data
        if self.split_at and len(self.post_ids) > 1:
            raise forms.ValidationError('Please select only one post to split the topic at')

        return self.cleaned_data