changeset 285:8fd4984d5c3b

This is a first rough commit for #95, adding the ability to embed YouTube videos in forum posts. Some more polish and testing needs to happen at this point. I wanted to get all these changes off my hard drive and into the repository.
author Brian Neal <bgneal@gmail.com>
date Thu, 14 Oct 2010 02:39:35 +0000 (2010-10-14)
parents df2c81f705a8
children 72fd300685d5
files gpp/forums/attachments.py gpp/forums/forms.py gpp/forums/models.py gpp/forums/templatetags/forum_tags.py gpp/forums/tools.py gpp/forums/urls.py gpp/forums/views/attachments.py gpp/forums/views/main.py gpp/oembed/__init__.py gpp/oembed/admin.py gpp/oembed/core.py gpp/oembed/models.py gpp/oembed/urls.py gpp/oembed/views.py gpp/settings.py gpp/templates/forums/display_post.html gpp/templates/forums/edit_post.html gpp/templates/forums/new_post.html gpp/templates/forums/new_topic.html gpp/templates/forums/show_form.html gpp/templates/forums/topic.html gpp/urls.py media/css/base.css media/icons/television_add.png media/icons/television_delete.png media/js/forums.js
diffstat 25 files changed, 578 insertions(+), 9 deletions(-) [+]
line wrap: on
line diff
--- /dev/null	Thu Jan 01 00:00:00 1970 +0000
+++ b/gpp/forums/attachments.py	Thu Oct 14 02:39:35 2010 +0000
@@ -0,0 +1,45 @@
+"""
+This module contains a class for handling attachments on forum posts.
+"""
+from oembed.models import Oembed
+from forums.models import Attachment
+
+
+class AttachmentProcessor(object):
+    """
+    This class is aggregated by various form classes to handle
+    attachments on forum posts. New posts can receive attachments and edited
+    posts can have their attachments replaced, augmented, or deleted.
+
+    """
+    def __init__(self, ids):
+        """
+        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 = []
+        for pk in ids:
+            try:
+                pk = int(pk)
+            except ValueError:
+                continue
+            self.pks.append(pk)
+
+        self.embeds = []
+        if self.pks:
+            self.embeds = Oembed.objects.in_bulk(self.pks)
+
+    def save_attachments(self, post):
+        """
+        Create and save attachments to the supplied post object.
+        Any existing attachments on the post are removed first.
+
+        """
+        post.attachments.clear()
+
+        for n, pk in enumerate(self.pks):
+            attachment = Attachment(post=post, embed=self.embeds[pk], order=n)
+            attachment.save()
--- a/gpp/forums/forms.py	Mon Oct 04 01:01:29 2010 +0000
+++ b/gpp/forums/forms.py	Thu Oct 14 02:39:35 2010 +0000
@@ -7,6 +7,7 @@
 from forums.models import Forum
 from forums.models import Topic
 from forums.models import Post
+from forums.attachments import AttachmentProcessor
 
 
 class NewPostForm(forms.Form):
@@ -25,6 +26,11 @@
                 settings.GPP_THIRD_PARTY_JS['jquery-ui'] +
                 ('js/forums.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_topic_id(self):
         id = self.cleaned_data['topic_id']
         try:
@@ -40,6 +46,7 @@
         post = Post(topic=self.topic, user=user, body=self.cleaned_data['body'],
                 user_ip=ip)
         post.save()
+        self.attach_proc.save_attachments(post)
         return post
 
 
@@ -76,6 +83,9 @@
             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)
+
     def save(self, ip=None):
         """
         Creates the new Topic and first Post from the form data and supplied
@@ -93,6 +103,9 @@
                 body=self.cleaned_data['body'],
                 user_ip=ip)
         post.save()
+
+        self.attach_proc.save_attachments(post)
+
         return topic
 
 
--- a/gpp/forums/models.py	Mon Oct 04 01:01:29 2010 +0000
+++ b/gpp/forums/models.py	Thu Oct 14 02:39:35 2010 +0000
@@ -8,6 +8,7 @@
 from django.contrib.auth.models import User, Group
 
 from core.markup import site_markup
+from oembed.models import Oembed
 
 
 class Category(models.Model):
@@ -248,6 +249,7 @@
     body = models.TextField()
     html = models.TextField()
     user_ip = models.IPAddressField(blank=True, default='', null=True)
+    attachments = models.ManyToManyField(Oembed, through='Attachment')
 
     class Meta:
         ordering = ('creation_date', )
@@ -365,3 +367,18 @@
     def touch(self):
         self.last_visit = datetime.datetime.now()
 
+
+class Attachment(models.Model):
+    """
+    This model is a "through" table for the M2M relationship between forum
+    posts and Oembed objects.
+    """
+    post = models.ForeignKey(Post)
+    embed = models.ForeignKey(Oembed)
+    order = models.IntegerField()
+
+    class Meta:
+        ordering = ('order', )
+
+    def __unicode__(self):
+        return u'Post %d, %s' % (self.post.pk, self.embed.title)
--- a/gpp/forums/templatetags/forum_tags.py	Mon Oct 04 01:01:29 2010 +0000
+++ b/gpp/forums/templatetags/forum_tags.py	Thu Oct 14 02:39:35 2010 +0000
@@ -100,16 +100,25 @@
 
 
 @register.inclusion_tag('forums/show_form.html')
-def show_form(legend_text, form, submit_value, is_ajax, media_url):
+def show_form(legend_text, form, submit_value, is_ajax, post=None):
     """
     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,
-        'MEDIA_URL': media_url,
+        'post_id': post_id,
+        'MEDIA_URL': settings.MEDIA_URL,
     }
 
 
--- a/gpp/forums/tools.py	Mon Oct 04 01:01:29 2010 +0000
+++ b/gpp/forums/tools.py	Thu Oct 14 02:39:35 2010 +0000
@@ -15,6 +15,10 @@
     """
     posts = Post.objects.filter(user=user).select_related()
 
+    # delete attachments
+    for post in posts:
+        post.attachments.clear()
+
     # build a set of topics and forums affected by the post deletions
 
     topics = set(post.topic for post in posts)
--- a/gpp/forums/urls.py	Mon Oct 04 01:01:29 2010 +0000
+++ b/gpp/forums/urls.py	Thu Oct 14 02:39:35 2010 +0000
@@ -48,3 +48,7 @@
     url(r'^spammer/nailed/(\d+)/$', 'spammer_nailed', name='forums-spammer_nailed'),
     url(r'^stranger/(\d+)/$', 'stranger', name='forums-stranger'),
 )
+
+urlpatterns += patterns('forums.views.attachments',
+    url(r'^fetch_attachments/$', 'fetch_attachments', name='forums-fetch_attachments'),
+)
--- /dev/null	Thu Jan 01 00:00:00 1970 +0000
+++ b/gpp/forums/views/attachments.py	Thu Oct 14 02:39:35 2010 +0000
@@ -0,0 +1,35 @@
+"""
+This module contains views for working with post attachments.
+"""
+from django.http import HttpResponse
+from django.http import HttpResponseForbidden
+from django.http import HttpResponseBadRequest
+from django.http import HttpResponseNotFound
+import django.utils.simplejson as json
+
+from forums.models import Post
+
+
+def fetch_attachments(request):
+    """
+    This view is the target of an AJAX GET request to retrieve the
+    attachment embed data for a given forum post.
+
+    """
+    if not request.user.is_authenticated():
+        return HttpResponseForbidden('Please login or register.')
+
+    post_id = request.GET.get('pid')
+    if post_id is None:
+        return HttpResponseBadRequest('Missing post ID.')
+
+    try:
+        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]
+
+    return HttpResponse(json.dumps(data), content_type='application/json')
+
--- a/gpp/forums/views/main.py	Mon Oct 04 01:01:29 2010 +0000
+++ b/gpp/forums/views/main.py	Thu Oct 14 02:39:35 2010 +0000
@@ -23,7 +23,7 @@
 from core.paginator import DiggPaginator
 from core.functions import email_admins
 from forums.models import Forum, Topic, Post, FlaggedPost, TopicLastVisit, \
-        ForumLastVisit
+        ForumLastVisit, Attachment
 from forums.forms import NewTopicForm, NewPostForm, PostForm, MoveTopicForm, \
         SplitTopicForm
 from forums.unread import get_forum_unread_status, get_topic_unread_status, \
@@ -32,6 +32,7 @@
 from bio.models import UserProfile
 import antispam
 import antispam.utils
+from forums.attachments import AttachmentProcessor
 
 #######################################################################
 
@@ -184,6 +185,16 @@
 
     for post in page.object_list:
         post.user_profile = user_profiles[post.user.id]
+        post.attach_list = []
+
+    # Attach any attachments
+    post_ids = [post.pk for post in page.object_list]
+    attachments = Attachment.objects.filter(post__in=post_ids).select_related(
+            'embed').order_by('order')
+
+    post_dict = dict((post.pk, post) for post in page.object_list)
+    for item in attachments:
+        post_dict[item.post.id].attach_list.append(item.embed)
 
     last_page = page_num == paginator.num_pages
 
@@ -283,8 +294,10 @@
         post = form.save(request.user, request.META.get("REMOTE_ADDR", ""))
         post.unread = True
         post.user_profile = request.user.get_profile()
+        post.attach_list = post.attachments.all()
         _bump_post_count(request.user)
         _update_last_visit(request.user, form.topic)
+
         return render_to_response('forums/display_post.html', {
             'post': post,
             'can_moderate': _can_moderate(form.topic.forum, request.user),
@@ -359,6 +372,11 @@
             post = form.save(commit=False)
             post.touch()
             post.save()
+
+            # Save any attachments
+            attach_proc = AttachmentProcessor(request.POST.getlist('attachment'))
+            attach_proc.save_attachments(post)
+
             return HttpResponseRedirect(post.get_absolute_url())
     else:
         form = PostForm(instance=post)
@@ -439,6 +457,9 @@
         forum.last_post_pre_delete()
         forum.save()
 
+    # delete any attachments
+    post.attachments.clear()
+
     # Should be safe to delete the post now:
     post.delete()
 
@@ -459,6 +480,11 @@
     topic.subscribers.clear()
     topic.bookmarkers.clear()
 
+    # delete all attachments
+    posts = Post.objects.filter(topic=topic)
+    for post in posts:
+        post.attachments.clear()
+
     # It should be safe to just delete the topic now. This will
     # automatically delete all posts in the topic.
     topic.delete()
@@ -484,6 +510,11 @@
                 post.user = request.user
                 post.user_ip = request.META.get("REMOTE_ADDR", "")
                 post.save()
+
+                # Save any attachments
+                attach_proc = AttachmentProcessor(request.POST.getlist('attachment'))
+                attach_proc.save_attachments(post)
+
                 _bump_post_count(request.user)
                 _update_last_visit(request.user, topic)
                 return HttpResponseRedirect(post.get_absolute_url())
--- /dev/null	Thu Jan 01 00:00:00 1970 +0000
+++ b/gpp/oembed/admin.py	Thu Oct 14 02:39:35 2010 +0000
@@ -0,0 +1,24 @@
+"""
+Admin site definitions for the oembed application.
+"""
+from django.contrib import admin
+
+from oembed.models import Provider
+from oembed.models import Oembed
+
+
+class ProviderAdmin(admin.ModelAdmin):
+    list_display = ('name', 'api_endpoint', 'format')
+    list_filter = ('format', )
+    search_fields = ('name', )
+
+
+class OembedAdmin(admin.ModelAdmin):
+    date_hierarchy = 'date_added'
+    list_display = ('__unicode__', 'type', 'url', 'date_added')
+    list_filter = ('type', )
+    search_fields = ('title', )
+
+
+admin.site.register(Provider, ProviderAdmin)
+admin.site.register(Oembed, OembedAdmin)
--- /dev/null	Thu Jan 01 00:00:00 1970 +0000
+++ b/gpp/oembed/core.py	Thu Oct 14 02:39:35 2010 +0000
@@ -0,0 +1,57 @@
+"""
+This module contains core functionality for the oembed application.
+"""
+from __future__ import with_statement
+import urllib
+import urllib2
+import gzip
+try:
+    from cStringIO import StringIO
+except ImportError:
+    from StringIO import StringIO
+
+import django.utils.simplejson as json
+
+
+USER_AGENT = 'gremmies python oembed'
+
+
+def get_oembed(api_endpoint, url, format='json', **opts):
+    """
+    Perform the GET request to retrieve the embedded media data from the given
+    API endpoint for the given URL. Return the result as a Python dictionary.
+
+    format specifies the response format, and should be 'json' or 'xml'.
+    opts are any additional GET options that should be present in the GET
+    request.
+
+    """
+    opts['url'] = url
+    opts['format'] = format
+    api_url = "%s?%s" % (api_endpoint, urllib.urlencode(opts))
+
+    headers = {
+        'User-Agent': USER_AGENT, 
+        'Accept-Encoding': 'gzip',
+    }
+    request = urllib2.Request(api_url, headers=headers)
+
+    opener = urllib2.build_opener()
+    f = opener.open(request)
+    headers = f.info()
+    result = f.read()
+    f.close()
+
+    if headers.get('content-encoding') == 'gzip':
+        with gzip.GzipFile(fileobj=StringIO(result)) as f:
+            result = f.read()
+
+    return json.loads(result)
+
+if __name__ == "__main__":
+    try:
+        print get_oembed("http://www.youtube.com/oembed",
+                #"http://www.youtube.com/watch?v=7_IMzJldOf4")
+                "http://www.youtube.com/watch?v=99999999999")
+    except urllib2.HTTPError, e:
+        print e
--- /dev/null	Thu Jan 01 00:00:00 1970 +0000
+++ b/gpp/oembed/models.py	Thu Oct 14 02:39:35 2010 +0000
@@ -0,0 +1,58 @@
+"""
+Models for the oembed application.
+"""
+import datetime
+
+from django.db import models
+
+
+class Provider(models.Model):
+    """
+    This model described an oEmbed provider.
+    """
+    JSON, XML = range(2)
+    FORMAT_CHOICES = (
+        (JSON, "json"),
+        (XML, "xml"),
+    )
+
+    name = models.CharField(max_length=128)
+    api_endpoint = models.URLField(max_length=255, verify_exists=False,
+            verbose_name='API endpoint')
+    url_regex = models.CharField(max_length=255, verbose_name='URL regex')
+    format = models.IntegerField(choices=FORMAT_CHOICES)
+
+    def __unicode__(self):
+        return self.name
+
+
+class Oembed(models.Model):
+    """
+    This model represents stored embedded content retrieved from an oEmbed
+    provider.
+    """
+    PHOTO, VIDEO, LINK, RICH = range(4)
+    MEDIA_TYPE_CHOICES = (
+        (PHOTO, "photo"),
+        (VIDEO, "video"),
+        (LINK, "link"),
+        (RICH, "rich"),
+    )
+
+    url = models.URLField(max_length=255, verify_exists=False, db_index=True)
+    type = models.IntegerField(choices=MEDIA_TYPE_CHOICES)
+    title = models.CharField(max_length=255, blank=True, default='')
+    width = models.IntegerField()
+    height = models.IntegerField()
+    html = models.TextField()
+    date_added = models.DateTimeField()
+
+    def __unicode__(self):
+        return self.title or self.url
+
+    def save(self, *args, **kwargs):
+        if not self.pk:
+            self.date_added = datetime.datetime.now()
+
+        super(Oembed, self).save(*args, **kwargs)
+
--- /dev/null	Thu Jan 01 00:00:00 1970 +0000
+++ b/gpp/oembed/urls.py	Thu Oct 14 02:39:35 2010 +0000
@@ -0,0 +1,8 @@
+"""
+URLs for the oembed application.
+"""
+from django.conf.urls.defaults import *
+
+urlpatterns = patterns('oembed.views',
+    url(r'^fetch/$', 'fetch_media', name='oembed-fetch_media'),
+)
--- /dev/null	Thu Jan 01 00:00:00 1970 +0000
+++ b/gpp/oembed/views.py	Thu Oct 14 02:39:35 2010 +0000
@@ -0,0 +1,65 @@
+"""
+Views for the oembed application.
+"""
+import re
+import urllib2
+
+from django.http import HttpResponse
+from django.http import HttpResponseBadRequest
+from django.http import HttpResponseForbidden
+import django.utils.simplejson as json
+
+from oembed.models import Provider
+from oembed.models import Oembed
+from oembed.core import get_oembed
+
+
+def fetch_media(request):
+    """
+    This view returns the HTML media of an embeddable resource.
+    This view is the target of an AJAX request. 
+    """
+    if not request.user.is_authenticated():
+        return HttpResponseForbidden('Please login or register.')
+
+    url = request.POST.get('q')
+
+    if not url:
+        return HttpResponseBadRequest('Please provide a valid URL.')
+
+    # Is this already in our database?
+    try:
+        oembed = Oembed.objects.get(url=url)
+    except Oembed.DoesNotExist:
+        pass
+    else:
+        data = dict(id=oembed.id, embed=oembed.html)
+        return HttpResponse(json.dumps(data), content_type='application/json')
+
+    # It isn't in the database, try to find it from our providers
+    providers = Provider.objects.all()
+    for provider in providers:
+        if re.match(provider.url_regex, url):
+            try:
+                data = get_oembed(provider.api_endpoint, url)
+            except IOError, e:
+                return HttpResponseBadRequest(
+                    "Sorry, we could not retrieve your video (%s)" % e)
+
+            if 'type' not in data or data['type'] != 'video':
+                return HttpResponseBadRequest(
+                    "Hey, this doesn't look like a video..??")
+
+            oembed = Oembed(url=url,
+                    type=Oembed.VIDEO,
+                    title=data.get('title', ''),
+                    width=int(data.get('width', 0)),
+                    height=int(data.get('height', 0)),
+                    html=data.get('html', ''))
+            oembed.save()
+
+            data = dict(id=oembed.id, embed=oembed.html)
+            return HttpResponse(json.dumps(data),
+                    content_type='application/json')
+
+    return HttpBadRequest("Sorry, we couldn't find that video.")
--- a/gpp/settings.py	Mon Oct 04 01:01:29 2010 +0000
+++ b/gpp/settings.py	Thu Oct 14 02:39:35 2010 +0000
@@ -144,6 +144,7 @@
     'membermap',
     'messages',
     'news',
+    'oembed',
     'podcast',
     'polls',
     'potd',
--- a/gpp/templates/forums/display_post.html	Mon Oct 04 01:01:29 2010 +0000
+++ b/gpp/templates/forums/display_post.html	Thu Oct 14 02:39:35 2010 +0000
@@ -38,6 +38,13 @@
          <p class="small quiet">Last edited: {{ post.update_date|date:"M d, Y H:i:s" }}</p>
          {% endif %}
       </div>
+      {% if post.attach_list %}
+      <div>
+         {% for item in post.attach_list %}
+         <div class="forum-attachment">{{ item.html|safe }}</div>
+         {% endfor %}
+      </div>
+      {% endif %}
       <div class="forum-post-info-tools">
       {% if can_reply %}
          <a href="{% url forums-new_post topic_id=post.topic.id %}?quote={{ post.id }}"><img src="{{ MEDIA_URL }}icons/comment.png" alt="Reply with quote" title="Reply with quote" /></a>
--- a/gpp/templates/forums/edit_post.html	Mon Oct 04 01:01:29 2010 +0000
+++ b/gpp/templates/forums/edit_post.html	Thu Oct 14 02:39:35 2010 +0000
@@ -11,6 +11,6 @@
 </table>
 
 <a name="forum-reply-form"></a>
-{% show_form "Edit Post" form "Update Post" 0 MEDIA_URL %}
+{% show_form "Edit Post" form "Update Post" 0 post %}
 </div>
 {% endblock %}
--- a/gpp/templates/forums/new_post.html	Mon Oct 04 01:01:29 2010 +0000
+++ b/gpp/templates/forums/new_post.html	Thu Oct 14 02:39:35 2010 +0000
@@ -8,7 +8,7 @@
 {% if can_post %}
 <div class="forum-block">
 <a name="forum-reply-form"></a>
-{% show_form "New Post" form "Submit Post" 0 MEDIA_URL %}
+{% show_form "New Post" form "Submit Post" 0 %}
 {% else %}
    {% if topic.locked %}
    <p>This topic is locked.</p>
--- a/gpp/templates/forums/new_topic.html	Mon Oct 04 01:01:29 2010 +0000
+++ b/gpp/templates/forums/new_topic.html	Thu Oct 14 02:39:35 2010 +0000
@@ -5,5 +5,5 @@
 {% block content %}
 {% forum_navigation forum "New Topic" %}
 
-{% show_form "New Topic" form "Submit" 0 MEDIA_URL %}
+{% show_form "New Topic" form "Submit" 0 %}
 {% endblock %}
--- a/gpp/templates/forums/show_form.html	Mon Oct 04 01:01:29 2010 +0000
+++ b/gpp/templates/forums/show_form.html	Thu Oct 14 02:39:35 2010 +0000
@@ -5,5 +5,12 @@
 {{ form.as_p }}
 {% comment_dialogs %}
 <input type="submit" value="{{ submit_value }}" {% if is_ajax %}id="forums-reply-post"{% endif %} />
+
+<br />
+<br />
+<div id="attachment">
+{% if post_id %}<input type="hidden" name="post_id" value="{{ post_id }}" />{% endif %}
+</div>
+
 </fieldset>
 </form>
--- a/gpp/templates/forums/topic.html	Mon Oct 04 01:01:29 2010 +0000
+++ b/gpp/templates/forums/topic.html	Thu Oct 14 02:39:35 2010 +0000
@@ -49,7 +49,7 @@
 
 {% if last_page and can_reply %}
 <a name="forum-reply-form"></a>
-{% show_form "Reply to Topic" form "Submit Reply" 1 MEDIA_URL %}
+{% show_form "Reply to Topic" form "Submit Reply" 1 %}
 {% endif %}
 
 {% if user.is_authenticated %}
--- a/gpp/urls.py	Mon Oct 04 01:01:29 2010 +0000
+++ b/gpp/urls.py	Thu Oct 14 02:39:35 2010 +0000
@@ -39,6 +39,7 @@
    (r'^member_map/', include('membermap.urls')),
    (r'^messages/', include('messages.urls')),
    (r'^news/', include('news.urls')),
+   (r'^oembed/', include('oembed.urls')),
    (r'^podcast/', include('podcast.urls')),
    (r'^polls/', include('polls.urls')),
    (r'^potd/', include('potd.urls')),
--- a/media/css/base.css	Mon Oct 04 01:01:29 2010 +0000
+++ b/media/css/base.css	Thu Oct 14 02:39:35 2010 +0000
@@ -384,3 +384,12 @@
 }
 h2.forum-nav {font-size:1.2em;margin-top:1em;margin-bottom:0.2em;}
 h3.forum-nav {font-size:2em;line-height:1;margin-bottom:1em;margin-left:1em;}
+#attachment div {
+   margin: 1.0em 1.5em;
+}
+#attachment div span.link {
+   margin: 1em;
+}
+div.forum-attachment {
+   margin: 1.0em 1.5em;
+}
Binary file media/icons/television_add.png has changed
Binary file media/icons/television_delete.png has changed
--- a/media/js/forums.js	Mon Oct 04 01:01:29 2010 +0000
+++ b/media/js/forums.js	Thu Oct 14 02:39:35 2010 +0000
@@ -8,13 +8,21 @@
          return false;
       }
       $(this).attr('disabled', 'disabled').val('Posting reply...');
+
+      var attachments = new Array()
+      $('#attachment div input').each(function(index) {
+         attachments[index] = $(this).val();
+      });
+
       $.ajax({
          url: '/forums/quick-reply/', 
          type: 'POST',
          data: {
-         body : postText.val(),
-         topic_id : $('#id_topic_id').val()
+            body : postText.val(),
+            topic_id : $('#id_topic_id').val(),
+            attachment : attachments
          },
+         traditional: true,
          dataType: 'html',
          success: function (data, textStatus) {
             postText.val('');
@@ -23,11 +31,13 @@
             lastTr.hide();
             lastTr.fadeIn(3000);
             postButton.removeAttr('disabled').val('Submit Reply');
+            initAttachments();
          },
          error: function (xhr, textStatus, ex) {
             alert('Oops, an error occurred. ' + xhr.statusText + ' - ' + 
                xhr.responseText);
             postButton.removeAttr('disabled').val('Submit Reply');
+            initAttachments();
          }
          });
       return false;
@@ -82,4 +92,168 @@
          return confirm('Are you sure you want to delete this topic?\n' +
             'WARNING: all posts will be lost.');
    });
+
+   var vid = 0;
+   var vidDiv = $('#attachment');
+
+   function clearAttachments()
+   {
+      $('#attachment div').remove();
+      $('#attach-another').remove();
+   }
+
+   function initAttachments()
+   {
+      clearAttachments();
+
+      var post_input = $('#attachment input');
+      if (post_input.length == 1)
+      {
+         post_id = post_input.val();
+         post_input.replaceWith('<img src="/media/icons/ajax_busy.gif" alt="Busy" />');
+         $.ajax({
+            url: '/forums/fetch_attachments/', 
+            type: 'GET',
+            data: {
+               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;
+               });
+            },
+            error: function (xhr, textStatus, ex) {
+               alert('Oops, an error occurred. ' + xhr.statusText + ' - ' + 
+                  xhr.responseText);
+            }
+         });
+      }
+      else
+      {
+         vid = 0;
+         var s = '<div id="init-add">' +
+            '<img src="/media/icons/television_add.png" alt="Add" /> ' +
+            '<a href="#">Attach Video</a></div>';
+         vidDiv.prepend(s);
+         $('#attachment a').click(function () {
+            $('#init-add').remove();
+            addVideo();
+            return false;
+         });
+      }
+   }
+
+   function relabelAttachLink()
+   {
+      var another = $('#attach-another');
+      var n = $('#attachment div').length;
+      if (n == 0)
+      {
+         another.html("Attach a video");
+      }
+      else
+      {
+         another.html("Attach another video");
+      }
+   }
+
+   function addVideo()
+   {
+      var id = "video-" + vid;
+
+      var fakeForm = '<div id="' + id + '">' +
+         '<img src="/media/icons/television_add.png" alt="Attach" class="r" /> ' +
+         '<input type="text" size="45" class="r" /> <button type="button" class="r">Attach</button> ' +
+         '<a href="#" class="r">Remove</a><br /></div>';
+
+      var n = $('#attachment div').length;
+
+      var another = $('#attach-another');
+      if (n == 0)
+      {
+         if (another.length > 0)
+         {
+            another.before(fakeForm);
+         }
+         else
+         {
+            vidDiv.append(fakeForm);
+         }
+      }
+      else
+      {
+         $('#attachment div:last').after(fakeForm);
+      }
+
+      $('#' + id + ' a').click(function() {
+         $('#' + id).remove();
+         relabelAttachLink();
+         return false;
+      });
+
+      var vidText = $('#' + id + ' input');
+
+      $('#' + id + ' button').click(function() {
+         $.ajax({
+            url: '/oembed/fetch/', 
+            type: 'POST',
+            data: {
+               q : vidText.val()
+            },
+            dataType: 'json',
+            success: function (data, textStatus) {
+               $('#' + id + " .r").remove();
+               var myDiv = $('#' + id);
+               var html = '<span class="link">' +
+                  '<img src="/media/icons/television_delete.png" alt="Remove" /> ' +
+                  '<a href="#">Remove</a></span>' +
+                  '<input type="hidden" name="attachment" value="' + data.id + '" />';
+               myDiv.prepend(html);
+               myDiv.prepend(data.embed);
+               $('#' + id + ' a').click(function() {
+                  myDiv.remove();
+                  relabelAttachLink();
+                  return false;
+               });
+            },
+            error: function (xhr, textStatus, ex) {
+               alert('Oops, an error occurred. ' + xhr.statusText + ' - ' + 
+                  xhr.responseText);
+            }
+         });
+      });
+
+      if (vid == 0)
+      {
+         $('#video-0').after('<a id="attach-another" href="#">Attach another video</a>');
+         $('#attach-another').click(function() {
+            addVideo();
+            relabelAttachLink();
+            return false;
+         });
+      }
+      ++vid;
+   }
+
+   initAttachments();
 });