changeset 286:72fd300685d5

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.
author Brian Neal <bgneal@gmail.com>
date Sat, 23 Oct 2010 20:19:46 +0000 (2010-10-23)
parents 8fd4984d5c3b
children 47a7138fcccb
files gpp/forums/attachments.py gpp/forums/forms.py gpp/forums/templatetags/forum_tags.py gpp/forums/views/attachments.py gpp/forums/views/main.py gpp/oembed/urls.py gpp/oembed/views.py gpp/smiley/__init__.py gpp/templates/forums/edit_post.html gpp/templates/forums/show_form.html media/js/forums.js
diffstat 11 files changed, 177 insertions(+), 69 deletions(-) [+]
line wrap: on
line diff
--- 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
--- 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
--- 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,
     }
 
--- 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]
 
--- 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()
--- 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'),
 )
--- 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')
--- 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
--- 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 @@
 </table>
 
 <a name="forum-reply-form"></a>
-{% show_form "Edit Post" form "Update Post" 0 post %}
+{% show_form "Edit Post" form "Update Post" 0 %}
 </div>
 {% endblock %}
--- 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 %}
-<form action="." method="post">{% csrf_token %}
+<form action="." method="post" id="forums_post_form">{% csrf_token %}
 <fieldset>
 <legend>{{ legend_text }}</legend>
 {{ form.as_p }}
@@ -8,9 +8,10 @@
 
 <br />
 <br />
-<div id="attachment">
-{% if post_id %}<input type="hidden" name="post_id" value="{{ post_id }}" />{% endif %}
-</div>
+<fieldset>
+<legend>Video Attachments</legend>
+<div id="attachment"></div>
+</fieldset>
 
 </fieldset>
 </form>
--- 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 = '<div id="video-' + index + '">' + value.html +
+            '<span class="link">' +
+            '<img src="/media/icons/television_delete.png" alt="Remove" /> ' +
+            '<a href="#">Remove</a></span>' +
+            '<input type="hidden" name="attachment" value="' + value.id + '" />';
+            '</div>';
+         vidDiv.append(html);
+         $('#video-' + index + ' a').click(function() {
+            $('#video-' + index).remove();
+            relabelAttachLink();
+            return false;
+         });
+      });
+      vid = data.length;
+      $('#video-' + (vid-1)).after('<a id="attach-another" href="#">Attach another video</a>');
+      $('#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('<img src="/media/icons/ajax_busy.gif" alt="Busy" />');
+         vidDiv.prepend('<img src="/media/icons/ajax_busy.gif" alt="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 = '<div id="video-' + index + '">' + value.html +
-                     '<span class="link">' +
-                     '<img src="/media/icons/television_delete.png" alt="Remove" /> ' +
-                     '<a href="#">Remove</a></span>' +
-                     '<input type="hidden" name="attachment" value="' + value.id + '" />';
-                     '</div>';
-                  vidDiv.append(html);
-                  $('#video-' + index + ' a').click(function() {
-                     $('#video-' + index).remove();
-                     relabelAttachLink();
-                     return false;
-                  });
-               });
-               vid = data.length;
-               $('#video-' + (vid-1)).after('<a id="attach-another" href="#">Attach another video</a>');
-               $('#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('<img src="/media/icons/ajax_busy.gif" alt="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');
             }
          });
       });