# HG changeset patch # User Brian Neal # Date 1287865186 0 # Node ID 72fd300685d54302260d1a75d8db6b2389bb625b # Parent 8fd4984d5c3bf30f42a460c454812b2171a4fe92 For #95. You can now make posts with no text in the body if you have attachments. And now if you create a new topic with an attachment, and the POST fails (say you forgot the topic title), we will now re-attach attachments. Also fixed a bug in the smiley code that would arise if it was asked to markup an empty string. diff -r 8fd4984d5c3b -r 72fd300685d5 gpp/forums/attachments.py --- a/gpp/forums/attachments.py Thu Oct 14 02:39:35 2010 +0000 +++ b/gpp/forums/attachments.py Sat Oct 23 20:19:46 2010 +0000 @@ -17,7 +17,7 @@ This class is constructed with a list of Oembed ids. We retrieve the actual Oembed objects associated with these keys for use in subsequent operations. - + """ # ensure all ids are integers self.pks = [] @@ -43,3 +43,17 @@ for n, pk in enumerate(self.pks): attachment = Attachment(post=post, embed=self.embeds[pk], order=n) attachment.save() + + def has_attachments(self): + """ + Return true if we have valid pending attachments. + + """ + return len(self.embeds) > 0 + + def get_ids(self): + """ + Return the list of Oembed ids. + + """ + return self.pks diff -r 8fd4984d5c3b -r 72fd300685d5 gpp/forums/forms.py --- a/gpp/forums/forms.py Thu Oct 14 02:39:35 2010 +0000 +++ b/gpp/forums/forms.py Sat Oct 23 20:19:46 2010 +0000 @@ -12,7 +12,8 @@ class NewPostForm(forms.Form): """Form for creating a new post.""" - body = forms.CharField(label='', + body = forms.CharField(label='', + required=False, widget=forms.Textarea(attrs={'class': 'markItUp smileyTarget'})) topic_id = forms.IntegerField(widget=forms.HiddenInput) topic = None @@ -22,7 +23,7 @@ 'all': (settings.GPP_THIRD_PARTY_CSS['markitup'] + settings.GPP_THIRD_PARTY_CSS['jquery-ui']), } - js = (settings.GPP_THIRD_PARTY_JS['markitup'] + + js = (settings.GPP_THIRD_PARTY_JS['markitup'] + settings.GPP_THIRD_PARTY_JS['jquery-ui'] + ('js/forums.js', )) @@ -31,13 +32,19 @@ 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 + return id def save(self, user, ip=None): """ @@ -58,7 +65,7 @@ """ name = forms.CharField(label='Subject', max_length=255, widget=forms.TextInput(attrs={'size': 64})) - body = forms.CharField(label='', + body = forms.CharField(label='', required=False, widget=forms.Textarea(attrs={'class': 'markItUp smileyTarget'})) user = None forum = None @@ -69,7 +76,7 @@ 'all': (settings.GPP_THIRD_PARTY_CSS['markitup'] + settings.GPP_THIRD_PARTY_CSS['jquery-ui']), } - js = (settings.GPP_THIRD_PARTY_JS['markitup'] + + js = (settings.GPP_THIRD_PARTY_JS['markitup'] + settings.GPP_THIRD_PARTY_JS['jquery-ui'] + ('js/forums.js', )) @@ -86,6 +93,23 @@ 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): """ Creates the new Topic and first Post from the form data and supplied @@ -113,7 +137,8 @@ """ Form for editing an existing post or a new, non-quick post. """ - body = forms.CharField(label='', + body = forms.CharField(label='', + required=False, widget=forms.Textarea(attrs={'class': 'markItUp smileyTarget'})) class Meta: @@ -125,20 +150,42 @@ 'all': (settings.GPP_THIRD_PARTY_CSS['markitup'] + settings.GPP_THIRD_PARTY_CSS['jquery-ui']), } - js = (settings.GPP_THIRD_PARTY_JS['markitup'] + + js = (settings.GPP_THIRD_PARTY_JS['markitup'] + settings.GPP_THIRD_PARTY_JS['jquery-ui'] + ('js/forums.js', )) + def __init__(self, *args, **kwargs): + super(PostForm, self).__init__(*args, **kwargs) + + 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 + class MoveTopicForm(forms.Form): """ Form for a moderator to move a topic to a forum. """ - forums = forms.ModelChoiceField(label='Move to 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) + hide_label = kwargs.pop('hide_label', False) required = kwargs.pop('required', True) super(MoveTopicForm, self).__init__(*args, **kwargs) self.fields['forums'].queryset = \ @@ -154,7 +201,7 @@ """ name = forms.CharField(label='New topic title', max_length=255, widget=forms.TextInput(attrs={'size': 64})) - forums = forms.ModelChoiceField(label='Forum for new topic', + forums = forms.ModelChoiceField(label='Forum for new topic', queryset=Forum.objects.none()) post_ids = [] split_at = False diff -r 8fd4984d5c3b -r 72fd300685d5 gpp/forums/templatetags/forum_tags.py --- a/gpp/forums/templatetags/forum_tags.py Thu Oct 14 02:39:35 2010 +0000 +++ b/gpp/forums/templatetags/forum_tags.py Sat Oct 23 20:19:46 2010 +0000 @@ -100,24 +100,15 @@ @register.inclusion_tag('forums/show_form.html') -def show_form(legend_text, form, submit_value, is_ajax, post=None): +def show_form(legend_text, form, submit_value, is_ajax): """ This tag displays the common HTML for a forum form. - If post is not None, then we are editing an existing post. We must get the - post id into the template if the post has attachments. AJAX is used to - retrieve the attachments. """ - post_id = None - if post is not None: - if post.attachments.count() > 0: - post_id = post.id - return { 'legend_text': legend_text, 'form': form, 'submit_value': submit_value, 'is_ajax': is_ajax, - 'post_id': post_id, 'MEDIA_URL': settings.MEDIA_URL, } diff -r 8fd4984d5c3b -r 72fd300685d5 gpp/forums/views/attachments.py --- a/gpp/forums/views/attachments.py Thu Oct 14 02:39:35 2010 +0000 +++ b/gpp/forums/views/attachments.py Sat Oct 23 20:19:46 2010 +0000 @@ -27,7 +27,7 @@ post = Post.objects.get(pk=post_id) except Post.DoesNotExist: return HttpResponseNotFound("That post doesn't exist.") - + embeds = post.attachments.all().select_related('embed') data = [{'id': embed.id, 'html': embed.html} for embed in embeds] diff -r 8fd4984d5c3b -r 72fd300685d5 gpp/forums/views/main.py --- a/gpp/forums/views/main.py Thu Oct 14 02:39:35 2010 +0000 +++ b/gpp/forums/views/main.py Sat Oct 23 20:19:46 2010 +0000 @@ -68,7 +68,7 @@ """ for topic in topics: if topic.post_count > POSTS_PER_PAGE: - pp = DiggPaginator(range(topic.post_count), POSTS_PER_PAGE, + pp = DiggPaginator(range(topic.post_count), POSTS_PER_PAGE, body=2, tail=3, margin=1) topic.page_range = pp.page(1).page_range else: @@ -142,7 +142,7 @@ page_nav = render_to_string('forums/pagination.html', {'page': page}) can_moderate = _can_moderate(forum, request.user) - + return render_to_response('forums/forum_index.html', { 'forum': forum, 'feed': feed, @@ -252,7 +252,7 @@ kwargs={'tid': topic.pk})) else: form = NewTopicForm(request.user, forum) - + return render_to_response('forums/new_topic.html', { 'forum': forum, 'form': form, @@ -305,7 +305,7 @@ }, context_instance=RequestContext(request)) - return HttpResponseBadRequest("Invalid post."); + return HttpResponseBadRequest("Oops, did you forget some text?"); def goto_post(request, post_id): @@ -374,8 +374,7 @@ post.save() # Save any attachments - attach_proc = AttachmentProcessor(request.POST.getlist('attachment')) - attach_proc.save_attachments(post) + form.attach_proc.save_attachments(post) return HttpResponseRedirect(post.get_absolute_url()) else: @@ -512,8 +511,7 @@ post.save() # Save any attachments - attach_proc = AttachmentProcessor(request.POST.getlist('attachment')) - attach_proc.save_attachments(post) + form.attach_proc.save_attachments(post) _bump_post_count(request.user) _update_last_visit(request.user, topic) @@ -651,7 +649,7 @@ if form.is_valid(): _bulk_move(topic_ids, forum, form.cleaned_data['forums']) return HttpResponseRedirect(url) - + if form is None: form = MoveTopicForm(request.user, hide_label=True) @@ -803,7 +801,7 @@ def _user_posts(request, target_user, req_user, page_title): - """Displays a list of posts made by the target user. + """Displays a list of posts made by the target user. req_user is the user trying to view the posts. Only the forums req_user can see are searched. """ @@ -981,7 +979,7 @@ for post in posts: post.topic = new_topic post.save() - + topic.post_count_update() topic.save() new_topic.post_count_update() diff -r 8fd4984d5c3b -r 72fd300685d5 gpp/oembed/urls.py --- a/gpp/oembed/urls.py Thu Oct 14 02:39:35 2010 +0000 +++ b/gpp/oembed/urls.py Sat Oct 23 20:19:46 2010 +0000 @@ -5,4 +5,5 @@ urlpatterns = patterns('oembed.views', url(r'^fetch/$', 'fetch_media', name='oembed-fetch_media'), + url(r'^fetch_saved/$', 'fetch_saved_media', name='oembed-fetch_saved_media'), ) diff -r 8fd4984d5c3b -r 72fd300685d5 gpp/oembed/views.py --- a/gpp/oembed/views.py Thu Oct 14 02:39:35 2010 +0000 +++ b/gpp/oembed/views.py Sat Oct 23 20:19:46 2010 +0000 @@ -16,8 +16,8 @@ def fetch_media(request): """ - This view returns the HTML media of an embeddable resource. - This view is the target of an AJAX request. + This view returns the HTML media of an embeddable resource as + JSON. This view is the target of an AJAX request. """ if not request.user.is_authenticated(): return HttpResponseForbidden('Please login or register.') @@ -63,3 +63,25 @@ content_type='application/json') return HttpBadRequest("Sorry, we couldn't find that video.") + + +def fetch_saved_media(request): + """ + This view returns the HTML embed information for previously saved Oembed + objects as JSON. This view is the target of an AJAX request. + """ + if not request.user.is_authenticated(): + return HttpResponseForbidden('Please login or register.') + + embed_ids = request.GET.getlist('embeds') + if not embed_ids: + return HttpResponseBadRequest('Missing embed list.') + + embeds = Oembed.objects.in_bulk(embed_ids) + + # build results in order + results = [] + for pk in embeds: + results.append(dict(id=pk, html=embeds[pk].html)) + + return HttpResponse(json.dumps(results), content_type='application/json') diff -r 8fd4984d5c3b -r 72fd300685d5 gpp/smiley/__init__.py --- a/gpp/smiley/__init__.py Thu Oct 14 02:39:35 2010 +0000 +++ b/gpp/smiley/__init__.py Sat Oct 23 20:19:46 2010 +0000 @@ -22,6 +22,9 @@ Converts and returns the supplied text with the HTML version of the smileys. """ + if not value: + return u'' + if not autoescape or isinstance(value, SafeData): esc = lambda x: x else: @@ -49,6 +52,9 @@ Returns a string copy of the input s that has the smiley codes replaced with Markdown for smiley images. """ + if not s: + return u'' + for regex, repl in self.regexes: s = regex.sub(repl, s) return s diff -r 8fd4984d5c3b -r 72fd300685d5 gpp/templates/forums/edit_post.html --- a/gpp/templates/forums/edit_post.html Thu Oct 14 02:39:35 2010 +0000 +++ b/gpp/templates/forums/edit_post.html Sat Oct 23 20:19:46 2010 +0000 @@ -11,6 +11,6 @@ -{% show_form "Edit Post" form "Update Post" 0 post %} +{% show_form "Edit Post" form "Update Post" 0 %} {% endblock %} diff -r 8fd4984d5c3b -r 72fd300685d5 gpp/templates/forums/show_form.html --- a/gpp/templates/forums/show_form.html Thu Oct 14 02:39:35 2010 +0000 +++ b/gpp/templates/forums/show_form.html Sat Oct 23 20:19:46 2010 +0000 @@ -1,5 +1,5 @@ {% load core_tags %} -
{% csrf_token %} +{% csrf_token %}
{{ legend_text }} {{ form.as_p }} @@ -8,9 +8,10 @@

-
-{% if post_id %}{% endif %} -
+
+Video Attachments +
+
diff -r 8fd4984d5c3b -r 72fd300685d5 media/js/forums.js --- a/media/js/forums.js Thu Oct 14 02:39:35 2010 +0000 +++ b/media/js/forums.js Sat Oct 23 20:19:46 2010 +0000 @@ -3,10 +3,6 @@ var postButton = $('#forums-reply-post'); postButton.click(function () { var text = $.trim(postText.val()); - if (text.length == 0) { - alert('Please enter some text.'); - return false; - } $(this).attr('disabled', 'disabled').val('Posting reply...'); var attachments = new Array() @@ -102,15 +98,42 @@ $('#attach-another').remove(); } + function processEmbeds(data, textStatus) + { + vidDiv.find('img').remove(); + $.each(data, function(index, value) { + var html = '
' + value.html + + '' + + 'Remove ' + + 'Remove' + + ''; + '
'; + vidDiv.append(html); + $('#video-' + index + ' a').click(function() { + $('#video-' + index).remove(); + relabelAttachLink(); + return false; + }); + }); + vid = data.length; + $('#video-' + (vid-1)).after('Attach another video'); + $('#attach-another').click(function() { + addVideo(); + relabelAttachLink(); + return false; + }); + } + function initAttachments() { clearAttachments(); - var post_input = $('#attachment input'); + var post_input = $('#id_post_id'); + var attachments = $("#forums_post_form input:hidden[name='attachment']"); if (post_input.length == 1) { post_id = post_input.val(); - post_input.replaceWith('Busy'); + vidDiv.prepend('Busy'); $.ajax({ url: '/forums/fetch_attachments/', type: 'GET', @@ -118,31 +141,33 @@ pid : post_id }, dataType: 'json', - success: function (data, textStatus) { - $('#attachment img').remove(); - $.each(data, function(index, value) { - var html = '
' + value.html + - '' + - 'Remove ' + - 'Remove' + - ''; - '
'; - vidDiv.append(html); - $('#video-' + index + ' a').click(function() { - $('#video-' + index).remove(); - relabelAttachLink(); - return false; - }); - }); - vid = data.length; - $('#video-' + (vid-1)).after('Attach another video'); - $('#attach-another').click(function() { - addVideo(); - relabelAttachLink(); - return false; - }); + success: processEmbeds, + error: function (xhr, textStatus, ex) { + vidDiv.find('img').remove(); + alert('Oops, an error occurred. ' + xhr.statusText + ' - ' + + xhr.responseText); + } + }); + } + else if (attachments.length > 0) + { + vidDiv.prepend('Busy'); + var embeds = new Array(); + attachments.each(function(index) { + embeds[index] = $(this).val(); + }); + attachments.remove(); + $.ajax({ + url: '/oembed/fetch_saved/', + type: 'GET', + data: { + embeds: embeds }, + traditional: true, + dataType: 'json', + success: processEmbeds, error: function (xhr, textStatus, ex) { + vidDiv.find('img').remove(); alert('Oops, an error occurred. ' + xhr.statusText + ' - ' + xhr.responseText); } @@ -214,6 +239,8 @@ var vidText = $('#' + id + ' input'); $('#' + id + ' button').click(function() { + var button = $(this); + button.attr('disabled', 'disabled'); $.ajax({ url: '/oembed/fetch/', type: 'POST', @@ -239,6 +266,7 @@ error: function (xhr, textStatus, ex) { alert('Oops, an error occurred. ' + xhr.statusText + ' - ' + xhr.responseText); + button.removeAttr('disabled'); } }); });