Customized FilterSelectMultiple widget for Tags to allow one-click adding of tags...
authorStephen Burrows <stephen.r.burrows@gmail.com>
Fri, 5 Nov 2010 19:07:29 +0000 (15:07 -0400)
committerStephen Burrows <stephen.r.burrows@gmail.com>
Fri, 5 Nov 2010 19:23:21 +0000 (15:23 -0400)
admin/base.py
admin/widgets.py
contrib/penfield/admin.py
media/admin/js/TagCreation.js [new file with mode: 0644]

index cb814b7..0413dde 100644 (file)
@@ -1,8 +1,12 @@
 from django.conf import settings
 from django.contrib import admin
 from django.contrib.contenttypes import generic
+from django.http import HttpResponse
+from django.utils import simplejson as json
+from django.utils.html import escape
 from philo.models import Tag, Attribute
 from philo.forms import AttributeForm, AttributeInlineFormSet
+from philo.admin.widgets import TagFilteredSelectMultiple
 
 
 COLLAPSE_CLASSES = ('collapse', 'collapse-closed', 'closed',)
@@ -33,5 +37,31 @@ class TagAdmin(admin.ModelAdmin):
        list_display = ('name', 'slug')
        prepopulated_fields = {"slug": ("name",)}
        search_fields = ["name"]
+       
+       def response_add(self, request, obj, post_url_continue='../%s/'):
+               # If it's an ajax request, return a json response containing the necessary information.
+               if request.is_ajax():
+                       return HttpResponse(json.dumps({'pk': escape(obj._get_pk_val()), 'unicode': escape(obj)}))
+               return super(TagAdmin, self).response_add(request, obj, post_url_continue)
+
+
+class AddTagAdmin(admin.ModelAdmin):
+       def formfield_for_manytomany(self, db_field, request=None, **kwargs):
+               """
+               Get a form Field for a ManyToManyField.
+               """
+               # If it uses an intermediary model that isn't auto created, don't show
+               # a field in admin.
+               if not db_field.rel.through._meta.auto_created:
+                       return None
+               
+               if db_field.rel.to == Tag and db_field.name in (list(self.filter_vertical) + list(self.filter_horizontal)):
+                       opts = Tag._meta
+                       if request.user.has_perm(opts.app_label + '.' + opts.get_add_permission()):
+                               kwargs['widget'] = TagFilteredSelectMultiple(db_field.verbose_name, (db_field.name in self.filter_vertical))
+                               return db_field.formfield(**kwargs)
+               
+               return super(AddTagAdmin, self).formfield_for_manytomany(db_field, request, **kwargs)
+
 
 admin.site.register(Tag, TagAdmin)
\ No newline at end of file
index f8799fe..7a47c63 100644 (file)
@@ -1,5 +1,6 @@
 from django import forms
 from django.conf import settings
+from django.contrib.admin.widgets import FilteredSelectMultiple
 from django.utils.translation import ugettext as _
 from django.utils.safestring import mark_safe
 from django.utils.text import truncate_words
@@ -30,4 +31,35 @@ class ModelLookupWidget(forms.TextInput):
                                output += '&nbsp;<strong>%s</strong>' % escape(truncate_words(value_object, 14))
                        except value_class.DoesNotExist:
                                pass
-               return mark_safe(output)
\ No newline at end of file
+               return mark_safe(output)
+
+
+class TagFilteredSelectMultiple(FilteredSelectMultiple):
+       """
+       A SelectMultiple with a JavaScript filter interface.
+
+       Note that the resulting JavaScript assumes that the jsi18n
+       catalog has been loaded in the page
+       """
+       class Media:
+               js = (settings.ADMIN_MEDIA_PREFIX + "js/core.js",
+                         settings.ADMIN_MEDIA_PREFIX + "js/SelectBox.js",
+                         settings.ADMIN_MEDIA_PREFIX + "js/SelectFilter2.js")
+               
+               if 'staticmedia' in settings.INSTALLED_APPS:
+                       import staticmedia
+                       js += (staticmedia.url('admin/js/TagCreation.js'),)
+               else:
+                       js += (settings.ADMIN_MEDIA_PREFIX + "js/TagCreation.js",)
+
+       def render(self, name, value, attrs=None, choices=()):
+               if attrs is None: attrs = {}
+               attrs['class'] = 'selectfilter'
+               if self.is_stacked: attrs['class'] += 'stacked'
+               output = [super(FilteredSelectMultiple, self).render(name, value, attrs, choices)]
+               output.append(u'<script type="text/javascript">addEvent(window, "load", function(e) {')
+               # TODO: "id_" is hard-coded here. This should instead use the correct
+               # API to determine the ID dynamically.
+               output.append(u'SelectFilter.init("id_%s", "%s", %s, "%s"); tagCreation.init("id_%s"); });</script>\n' % \
+                       (name, self.verbose_name.replace('"', '\\"'), int(self.is_stacked), settings.ADMIN_MEDIA_PREFIX, name))
+               return mark_safe(u''.join(output))
\ No newline at end of file
index 85888aa..5faf4ef 100644 (file)
@@ -1,5 +1,5 @@
 from django.contrib import admin
-from philo.admin import EntityAdmin
+from philo.admin import EntityAdmin, AddTagAdmin
 from philo.contrib.penfield.models import BlogEntry, Blog, BlogView, Newsletter, NewsletterArticle, NewsletterIssue, NewsletterView
 
 
@@ -12,7 +12,7 @@ class BlogAdmin(TitledAdmin):
        pass
 
 
-class BlogEntryAdmin(TitledAdmin):
+class BlogEntryAdmin(TitledAdmin, AddTagAdmin):
        filter_horizontal = ['tags']
 
 
@@ -24,8 +24,8 @@ class NewsletterAdmin(TitledAdmin):
        pass
 
 
-class NewsletterArticleAdmin(TitledAdmin):
-       pass
+class NewsletterArticleAdmin(TitledAdmin, AddTagAdmin):
+       filter_horizontal = TitledAdmin.filter_horizontal + ('tags', 'authors')
 
 
 class NewsletterIssueAdmin(TitledAdmin):
diff --git a/media/admin/js/TagCreation.js b/media/admin/js/TagCreation.js
new file mode 100644 (file)
index 0000000..31f2910
--- /dev/null
@@ -0,0 +1,78 @@
+var tagCreation = window.tagCreation;
+
+(function($) {
+       tagCreation = {
+               'cache': {},
+               'addTagFromSlug': function(triggeringLink) {
+                       var id = triggeringLink.id.replace(/^ajax_add_/, '') + '_input';
+                       var slug = document.getElementById(id).value;
+       
+                       var name = slug.split(' ');
+                       for(var i=0;i<name.length;i++) {
+                               name[i] = name[i].substr(0,1).toUpperCase() + name[i].substr(1);
+                       }
+                       name = name.join(' ');
+                       slug = name.toLowerCase().replace(/ /g, '-').replace(/[^\w-]/g, '');
+       
+                       var href = triggeringLink.href;
+                       var data = {
+                               'name': name,
+                               'slug': slug
+                       };
+                       $.post(href, data, function(data){
+                               newId = html_unescape(data.pk);
+                               newRepr = html_unescape(data.unicode);
+                               var toId = id.replace(/_input$/, '_to');
+                               elem = document.getElementById(toId);
+                               var o = new Option(newRepr, newId);
+                               SelectBox.add_to_cache(toId, o);
+                               SelectBox.redisplay(toId);
+                       }, "json")
+               },
+               'init': function(id) {
+                       tagCreation.cache[id] = {}
+                       var input = tagCreation.cache[id].input = document.getElementById(id + '_input');
+                       var select = tagCreation.cache[id].select = document.getElementById(id + '_from');
+                       var addLinkTemplate = document.getElementById('add_' + input.id.replace(/_input$/, '')).cloneNode(true);
+                       var addLink = tagCreation.cache[id].addLink = document.createElement('A');
+                       addLink.id = 'ajax_add_' + id;
+                       addLink.className = addLinkTemplate.className;
+                       addLink.href = addLinkTemplate.href;
+                       addLink.appendChild($(addLinkTemplate).children()[0].cloneNode(false));
+                       addLink.innerHTML += " <span style='vertical-align:text-top;'>Add this tag</span>"
+                       addLink.style.marginLeft = "20px";
+                       addLink.style.display = "block";
+                       addLink.style.backgroundPosition = "10px 5px";
+                       addLink.style.width = "120px";
+                       $(input).after(addLink);
+                       if (window.grappelli) {
+                               addLink.parentNode.style.backgroundPosition = "6px 8px";
+                       } else {
+                               addLink.style.marginTop = "5px";
+                       }
+                       tagCreation.toggleButton(id);
+                       addEvent(input, 'keyup', function() {
+                               tagCreation.toggleButton(id);
+                       })
+                       addEvent(addLink, 'click', function(e) {
+                               e.preventDefault();
+                               tagCreation.addTagFromSlug(addLink);
+                       })
+               },
+               'toggleButton': function(id) {
+                       var addLink = tagCreation.cache[id].addLink;
+                       var select = $(tagCreation.cache[id].select);
+                       if (select[0].options.length == 0) {
+                               if (addLink.style.display == 'none') {
+                                       addLink.style.display = 'block';
+                                       select.height(select.height() - $(addLink).outerHeight(false))
+                               }
+                       } else {
+                               if (addLink.style.display == 'block') {
+                                       select[0].style.height = null;
+                                       addLink.style.display = 'none';
+                               }
+                       }
+               }
+       }
+}(django.jQuery))
\ No newline at end of file