annotate forums/forms.py @ 973:6f55c086db1e

Guess file extension based on content-type. When downloading a file, and no path is supplied to store it, guess the file extension using mimetypes and the content-type header. Also supply a unit test for the HotLinkImageForm.
author Brian Neal <bgneal@gmail.com>
date Thu, 01 Oct 2015 19:44:45 -0500
parents 4619290d171d
children 21c592cac71c
rev   line source
bgneal@83 1 """
bgneal@83 2 Forms for the forums application.
bgneal@469 3
bgneal@83 4 """
bgneal@951 5 from collections import OrderedDict
bgneal@951 6
bgneal@83 7 from django import forms
bgneal@95 8 from django.conf import settings
bgneal@83 9
bgneal@110 10 from forums.models import Forum
bgneal@83 11 from forums.models import Topic
bgneal@83 12 from forums.models import Post
bgneal@285 13 from forums.attachments import AttachmentProcessor
bgneal@460 14 import forums.permissions as perms
bgneal@469 15 from forums.signals import notify_new_topic, notify_new_post
bgneal@963 16 from core.html import ImageCheckError
bgneal@963 17 from core.html import image_check
bgneal@963 18 from core.markup import site_markup
bgneal@83 19
bgneal@83 20
bgneal@722 21 FORUMS_FORM_CSS = {
bgneal@722 22 'all': (settings.GPP_THIRD_PARTY_CSS['markitup'] +
bgneal@722 23 settings.GPP_THIRD_PARTY_CSS['jquery-ui'])
bgneal@722 24 }
bgneal@722 25 FORUMS_FORM_JS = (
bgneal@722 26 settings.GPP_THIRD_PARTY_JS['markitup'] +
bgneal@722 27 settings.GPP_THIRD_PARTY_JS['jquery-ui'] +
bgneal@722 28 ['js/jquery.form.min.js', 'js/forums.js']
bgneal@722 29 )
bgneal@722 30
bgneal@722 31
bgneal@106 32 class NewPostForm(forms.Form):
bgneal@86 33 """Form for creating a new post."""
bgneal@286 34 body = forms.CharField(label='',
bgneal@286 35 required=False,
bgneal@135 36 widget=forms.Textarea(attrs={'class': 'markItUp smileyTarget'}))
bgneal@89 37 topic_id = forms.IntegerField(widget=forms.HiddenInput)
bgneal@89 38 topic = None
bgneal@83 39
bgneal@89 40 class Media:
bgneal@722 41 css = FORUMS_FORM_CSS
bgneal@722 42 js = FORUMS_FORM_JS
bgneal@89 43
bgneal@285 44 def __init__(self, *args, **kwargs):
bgneal@285 45 super(NewPostForm, self).__init__(*args, **kwargs)
bgneal@285 46 attachments = args[0].getlist('attachment') if len(args) else []
bgneal@285 47 self.attach_proc = AttachmentProcessor(attachments)
bgneal@285 48
bgneal@286 49 def clean_body(self):
bgneal@963 50 body = self.cleaned_data['body']
bgneal@963 51 self.body_html = None
bgneal@963 52 if not body and not self.attach_proc.has_attachments():
bgneal@963 53 raise forms.ValidationError("Please enter some text")
bgneal@963 54
bgneal@963 55 if body:
bgneal@963 56 self.body_html = site_markup(body)
bgneal@963 57 try:
bgneal@963 58 image_check(self.body_html)
bgneal@963 59 except ImageCheckError as ex:
bgneal@963 60 raise forms.ValidationError(str(ex))
bgneal@963 61
bgneal@963 62 return body
bgneal@286 63
bgneal@89 64 def clean_topic_id(self):
bgneal@89 65 id = self.cleaned_data['topic_id']
bgneal@89 66 try:
bgneal@102 67 self.topic = Topic.objects.select_related().get(pk=id)
bgneal@89 68 except Topic.DoesNotExist:
bgneal@963 69 raise forms.ValidationError('Invalid topic')
bgneal@286 70 return id
bgneal@89 71
bgneal@89 72 def save(self, user, ip=None):
bgneal@86 73 """
bgneal@86 74 Creates a new post from the form data and supplied arguments.
bgneal@86 75 """
bgneal@89 76 post = Post(topic=self.topic, user=user, body=self.cleaned_data['body'],
bgneal@89 77 user_ip=ip)
bgneal@963 78 post.save(html=self.body_html)
bgneal@285 79 self.attach_proc.save_attachments(post)
bgneal@469 80 notify_new_post(post)
bgneal@89 81 return post
bgneal@83 82
bgneal@83 83
bgneal@83 84 class NewTopicForm(forms.Form):
bgneal@102 85 """
bgneal@102 86 Form for creating a new topic and 1st post to that topic.
bgneal@102 87 Superusers and moderators can also create the topic as a sticky or initially
bgneal@102 88 locked.
bgneal@102 89 """
bgneal@95 90 name = forms.CharField(label='Subject', max_length=255,
bgneal@673 91 widget=forms.TextInput(attrs={'style': 'width:70%'}))
bgneal@286 92 body = forms.CharField(label='', required=False,
bgneal@135 93 widget=forms.Textarea(attrs={'class': 'markItUp smileyTarget'}))
bgneal@102 94 user = None
bgneal@102 95 forum = None
bgneal@102 96 has_mod_fields = False
bgneal@83 97
bgneal@95 98 class Media:
bgneal@722 99 css = FORUMS_FORM_CSS
bgneal@722 100 js = FORUMS_FORM_JS
bgneal@95 101
bgneal@102 102 def __init__(self, user, forum, *args, **kwargs):
bgneal@102 103 super(NewTopicForm, self).__init__(*args, **kwargs)
bgneal@102 104 self.user = user
bgneal@102 105 self.forum = forum
bgneal@102 106
bgneal@460 107 if perms.can_moderate(forum, user):
bgneal@102 108 self.fields['sticky'] = forms.BooleanField(required=False)
bgneal@102 109 self.fields['locked'] = forms.BooleanField(required=False)
bgneal@102 110 self.has_mod_fields = True
bgneal@102 111
bgneal@285 112 attachments = args[0].getlist('attachment') if len(args) else []
bgneal@285 113 self.attach_proc = AttachmentProcessor(attachments)
bgneal@285 114
bgneal@286 115 # If this form is being POSTed, and the user is trying to add
bgneal@286 116 # attachments, create hidden fields to list the Oembed ids. In
bgneal@286 117 # case the form isn't valid, the client-side javascript will know
bgneal@286 118 # which Oembed media to ask for when the form is displayed with
bgneal@286 119 # errors.
bgneal@286 120 if self.attach_proc.has_attachments():
bgneal@286 121 pks = self.attach_proc.get_ids()
bgneal@286 122 self.fields['attachment'] = forms.MultipleChoiceField(label='',
bgneal@286 123 widget=forms.MultipleHiddenInput(),
bgneal@286 124 choices=[(v, v) for v in pks])
bgneal@286 125
bgneal@286 126 def clean_body(self):
bgneal@963 127 body = self.cleaned_data['body']
bgneal@963 128 self.body_html = None
bgneal@963 129 if not body and not self.attach_proc.has_attachments():
bgneal@286 130 raise forms.ValidationError("This field is required.")
bgneal@963 131
bgneal@963 132 if body:
bgneal@963 133 self.body_html = site_markup(body)
bgneal@963 134 try:
bgneal@963 135 image_check(self.body_html)
bgneal@963 136 except ImageCheckError as ex:
bgneal@963 137 raise forms.ValidationError(str(ex))
bgneal@963 138
bgneal@963 139 return body
bgneal@286 140
bgneal@102 141 def save(self, ip=None):
bgneal@83 142 """
bgneal@83 143 Creates the new Topic and first Post from the form data and supplied
bgneal@86 144 arguments.
bgneal@83 145 """
bgneal@102 146 topic = Topic(forum=self.forum,
bgneal@83 147 name=self.cleaned_data['name'],
bgneal@102 148 user=self.user,
bgneal@102 149 sticky=self.has_mod_fields and self.cleaned_data['sticky'],
bgneal@102 150 locked=self.has_mod_fields and self.cleaned_data['locked'])
bgneal@83 151 topic.save()
bgneal@83 152
bgneal@83 153 post = Post(topic=topic,
bgneal@102 154 user=self.user,
bgneal@83 155 body=self.cleaned_data['body'],
bgneal@83 156 user_ip=ip)
bgneal@963 157 post.save(html=self.body_html)
bgneal@285 158
bgneal@285 159 self.attach_proc.save_attachments(post)
bgneal@285 160
bgneal@469 161 notify_new_topic(topic)
bgneal@469 162 notify_new_post(post)
bgneal@469 163
bgneal@83 164 return topic
bgneal@106 165
bgneal@106 166
bgneal@106 167 class PostForm(forms.ModelForm):
bgneal@106 168 """
bgneal@108 169 Form for editing an existing post or a new, non-quick post.
bgneal@106 170 """
bgneal@286 171 body = forms.CharField(label='',
bgneal@286 172 required=False,
bgneal@135 173 widget=forms.Textarea(attrs={'class': 'markItUp smileyTarget'}))
bgneal@106 174
bgneal@106 175 class Meta:
bgneal@106 176 model = Post
bgneal@106 177 fields = ('body', )
bgneal@106 178
bgneal@106 179 class Media:
bgneal@722 180 css = FORUMS_FORM_CSS
bgneal@722 181 js = FORUMS_FORM_JS
bgneal@110 182
bgneal@286 183 def __init__(self, *args, **kwargs):
bgneal@295 184 topic_name = kwargs.pop('topic_name', None)
bgneal@286 185 super(PostForm, self).__init__(*args, **kwargs)
bgneal@286 186
bgneal@951 187 if topic_name is not None:
bgneal@951 188 # this is a "first post", add a field for a topic name
bgneal@951 189 new_fields = OrderedDict(name=forms.CharField(
bgneal@951 190 label='Subject', max_length=255,
bgneal@951 191 widget=forms.TextInput(attrs={
bgneal@951 192 'class': 'title',
bgneal@951 193 'style': 'width: 90%',
bgneal@951 194 })))
bgneal@951 195 new_fields.update(self.fields)
bgneal@951 196 self.fields = new_fields
bgneal@295 197 self.initial['name'] = topic_name
bgneal@295 198
bgneal@286 199 attachments = args[0].getlist('attachment') if len(args) else []
bgneal@286 200 self.attach_proc = AttachmentProcessor(attachments)
bgneal@295 201
bgneal@286 202 # If this form is being used to edit an existing post, and that post
bgneal@286 203 # has attachments, create a hidden post_id field. The client-side
bgneal@286 204 # AJAX will use this as a cue to retrieve the HTML for the embedded
bgneal@286 205 # media.
bgneal@286 206 if 'instance' in kwargs:
bgneal@286 207 post = kwargs['instance']
bgneal@286 208 if post.attachments.count():
bgneal@286 209 self.fields['post_id'] = forms.CharField(label='',
bgneal@286 210 widget=forms.HiddenInput(attrs={'value': post.id}))
bgneal@286 211
bgneal@286 212 def clean_body(self):
bgneal@963 213 body = self.cleaned_data['body']
bgneal@963 214 self.body_html = None
bgneal@963 215 if not body and not self.attach_proc.has_attachments():
bgneal@286 216 raise forms.ValidationError('This field is required.')
bgneal@963 217
bgneal@963 218 if body:
bgneal@963 219 self.body_html = site_markup(body)
bgneal@963 220 try:
bgneal@963 221 image_check(self.body_html)
bgneal@963 222 except ImageCheckError as ex:
bgneal@963 223 raise forms.ValidationError(str(ex))
bgneal@963 224
bgneal@963 225 return body
bgneal@286 226
bgneal@295 227 def save(self, *args, **kwargs):
bgneal@295 228 commit = kwargs.get('commit', False)
bgneal@295 229 post = super(PostForm, self).save(*args, **kwargs)
bgneal@295 230
bgneal@295 231 # Are we saving a "first post"?
bgneal@295 232 if 'name' in self.cleaned_data:
bgneal@295 233 post.topic.name = self.cleaned_data['name']
bgneal@295 234 if commit:
bgneal@295 235 post.topic.save()
bgneal@295 236 return post
bgneal@295 237
bgneal@110 238
bgneal@110 239 class MoveTopicForm(forms.Form):
bgneal@111 240 """
bgneal@111 241 Form for a moderator to move a topic to a forum.
bgneal@111 242 """
bgneal@286 243 forums = forms.ModelChoiceField(label='Move to forum',
bgneal@111 244 queryset=Forum.objects.none())
bgneal@110 245
bgneal@111 246 def __init__(self, user, *args, **kwargs):
bgneal@286 247 hide_label = kwargs.pop('hide_label', False)
bgneal@111 248 required = kwargs.pop('required', True)
bgneal@111 249 super(MoveTopicForm, self).__init__(*args, **kwargs)
bgneal@111 250 self.fields['forums'].queryset = \
bgneal@111 251 Forum.objects.forums_for_user(user).order_by('name')
bgneal@111 252 if hide_label:
bgneal@111 253 self.fields['forums'].label = ''
bgneal@111 254 self.fields['forums'].required = required
bgneal@110 255
bgneal@115 256
bgneal@115 257 class SplitTopicForm(forms.Form):
bgneal@115 258 """
bgneal@115 259 Form for a moderator to split posts from a topic to a new topic.
bgneal@115 260 """
bgneal@115 261 name = forms.CharField(label='New topic title', max_length=255,
bgneal@115 262 widget=forms.TextInput(attrs={'size': 64}))
bgneal@286 263 forums = forms.ModelChoiceField(label='Forum for new topic',
bgneal@115 264 queryset=Forum.objects.none())
bgneal@115 265 post_ids = []
bgneal@115 266 split_at = False
bgneal@115 267
bgneal@115 268 def __init__(self, user, *args, **kwargs):
bgneal@115 269 super(SplitTopicForm, self).__init__(*args, **kwargs)
bgneal@115 270 self.fields['forums'].queryset = \
bgneal@115 271 Forum.objects.forums_for_user(user).order_by('name')
bgneal@115 272
bgneal@115 273 def clean(self):
bgneal@115 274 self.post_ids = self.data.getlist('post_ids')
bgneal@115 275 if len(self.post_ids) == 0:
bgneal@115 276 raise forms.ValidationError('Please select some posts')
bgneal@115 277
bgneal@115 278 self.split_at = 'split-at' in self.data
bgneal@115 279 if self.split_at and len(self.post_ids) > 1:
bgneal@115 280 raise forms.ValidationError('Please select only one post to split the topic at')
bgneal@115 281
bgneal@115 282 return self.cleaned_data