Permalink
Browse files

Implemented drag & drop

  • Loading branch information...
1 parent 4889a1b commit 6d07766f56c71d2f6fccbc88ceda0fb293d498e5 @samluescher committed Jan 30, 2012
View
@@ -11,7 +11,7 @@ Key features
* Supports various media types (images, audio, video, archives etc).
* Extension system, enabling you to easily add special processing for different
media types and extend the admin interface.
-* Speedy AJAX-enhanced admin interface.
+* Speedy AJAX-enhanced admin interface with drag & drop and dynamic resizing.
* Upload queue with progress indicators (using SWFUpload).
* Add metadata to all media to improve accessibility of your web sites.
* Integration with `Django CMS <http://www.django-cms.org>`_. Plugins include:
View
@@ -13,7 +13,7 @@ Key features:
* Supports various media types (images, audio, video, archives etc).
* Extension system, enabling you to easily add special processing for different
media types and extend the admin interface.
-* Speedy AJAX-enhanced admin interface.
+* Speedy AJAX-enhanced admin interface with drag & drop and dynamic resizing.
* Upload queue with progress indicators (using SWFUpload).
* Add metadata to all media to improve accessibility of your web sites.
* Integration with `Django CMS <http://www.django-cms.org>`_. Plugins include:
@@ -6,6 +6,7 @@
from django import forms
from django.utils.translation import ungettext, ugettext as _
from django.template import RequestContext
+from django.core.urlresolvers import reverse
from django.shortcuts import render_to_response
from django.contrib.admin import helpers
from django.http import HttpResponse, HttpResponseRedirect
@@ -40,7 +41,10 @@ def filenode_admin_action(modeladmin, request, queryset, form_class, extra_conte
'verbose_name': FileNode._meta.verbose_name,
'verbose_name_plural': FileNode._meta.verbose_name_plural
})
- return HttpResponseRedirect(redirect_node.get_admin_url())
+
+ return HttpResponseRedirect(reverse('admin:media_tree_filenode_folder_expand', args=(redirect_node.pk,)))
+ #return HttpResponseRedirect(redirect_node.get_admin_url())
+
if not execute:
if not issubclass(form_class, FileNodeActionsWithUserForm):
form = form_class(queryset, initial=form_initial)
@@ -71,11 +71,12 @@ def move_node(self, node, target):
# Reload object because tree attributes may be out of date
node = node.__class__.objects.get(pk=node.pk)
descendant_count = node.get_descendants().count()
- node.parent = target
- node.attach_user(self.user, change=True)
- node.save()
- self.success_count += 1 + descendant_count
+ if node.parent != target:
+ node.parent = target
+ node.attach_user(self.user, change=True)
+ node.save()
+ self.success_count += 1 + descendant_count
return node
except InvalidMove, e:
self.errors[NON_FIELD_ERRORS] = ErrorList(e)
@@ -85,6 +85,19 @@ class FileNodeAdmin(MPTTModelAdmin):
You can also extend the admin interface in many different fashions to suit
your custom requirements. Please refer to :ref:`extending` for more
information about extending Media Tree.
+
+ Special features:
+
+ * The AJAX-enhanced interface allows you to expand your folder tree without
+ page reloads.
+ * The file listing supports drag & drop. Drag files and folders to another
+ folder to move them. Hold the Alt key to copy them.
+ * You can set up an upload queue, which enables you to upload large files
+ and monitor the corresponding progress bars.
+ * Drag the slider above the file listing to dynamically resize thumbnails.
+ * You can select files and execute various special actions on them, for
+ instance download the selection as a ZIP archive.
+
"""
change_list_template = 'admin/media_tree/filenode/mptt_change_list.html'
@@ -268,15 +281,12 @@ def admin_preview(self, node, icons_only=False):
admin_preview.allow_tags = True
def admin_link(self, node, include_preview=False):
- return '<a href="%s">%s<span class="name">%s</span></a>' % (
+ return '<a class="node-link" href="%s">%s<span class="name">%s</span></a>' % (
node.get_admin_url(), self.admin_preview(node) if include_preview else '', node.name)
def node_tools(self, node):
tools = ''
tools += '<li><a class="changelink" href="%s">%s</a></li>' % (reverse('admin:media_tree_filenode_change', args=(node.pk,)), capfirst(ugettext('change')))
- #if node.is_folder():
- # tools += '<li><a class="add-folder" href="%s?folder_id=%s">%s</a></li>' % (
- # reverse('admin:media_tree_filenode_add_folder', args=()), str(node.pk), ugettext('add folder'))
return '<ul class="node-tools">%s</ul>' % tools
node_tools.short_description = ''
node_tools.allow_tags = True
View
@@ -1,17 +0,0 @@
-Django Media Tree
-=================
-
-@TODO: installer package with proper dependencies
-
-A Django app for managing your site media (images, audio, video, archives etc.) and organizing it in one central, tree-based location.
-
-__Features__:
-
-* Enables you to organize all of your site media in nested folders
-* Supports various media types (images, audio, video, archives etc.)
-* Multi-upload queue with progress indicators (using SWFUpload)
-* Add metadata to all media for accessible web sites
-* Integration with [Django CMS](http://www.django-cms.org). Plugins include: Image, slideshow, gallery, download list – roll your own!
-* Plugin interface based on Django's admin actions, allowing you to write your own batch-processing actions
-* Extension interface for custom media types with extra fields, based on signals
-* Uses mptt to efficiently model a file-and-folder system, allowing you to select whole folder trees with little database load
@@ -254,5 +254,29 @@ ul.node-tools li:last-child {
}
.queue {
- background: #F8F8F8;
+ background: #F0F0F0;
+}
+
+
+.drag-helper {
+ background: white;
+ border: 1px #CCC solid;
+ padding: .25em;
+ border-radius: 4px;
+ -moz-box-shadow: 0px 3px 7px #666;
+ -webkit-box-shadow: 0px 3px 7px #666;
+ box-shadow: 0px 3px 7px #666;
+}
+
+.drag-helper.copy {
+ padding-left: 1.5em;
+ background: white url(../img/icons/copy.png) no-repeat .4em center;
+}
+
+.drag-helper td {
+ border: none;
+}
+
+.drop-hover {
+ background: #D8D8D8;
}
@@ -24,6 +24,21 @@ jQuery(function($) {
return row;
}
+
+ var makeForm = function(action, hiddenFields) {
+ if (!hiddenFields) {
+ hiddenFields = {};
+ }
+ hiddenFields['csrfmiddlewaretoken'] = $('input[name=csrfmiddlewaretoken]').val();
+ hiddenHtml = '';
+ for (key in hiddenFields) {
+ hiddenHtml += '<input type="hidden" name="' + key + '" value="' + hiddenFields[key] + '" />';
+ }
+ return $(
+ '<form action="' + action + '" method="POST">'
+ +'<div style="display: none">' + hiddenHtml + '</div>'
+ +'</form>');
+ };
$('#object-tool-add-folder').click(function(event) {
event.preventDefault();
@@ -35,16 +50,18 @@ jQuery(function($) {
var targetFolder = $('#changelist').data('targetFolder');
cols[1] = $(
- '<td><form action="' + $('#object-tool-add-folder').attr('href') + '" method="POST">'
- +'<span style="white-space: nowrap;"><input type="text" id="add-folder-name" name="name" value="'+gettext('New folder')+'"/>'
- +'&nbsp;<input type="submit" class="button" value="'+gettext('Save')+'" /></span>'
- +'<input type="hidden" name="parent" value="' + (targetFolder ? targetFolder.id : '') + '" />'
- +'<input type="hidden" name="csrfmiddlewaretoken" />'
- +(targetFolder ? '<input type="hidden" name="folder_id" value="' + targetFolder.id + '" />' : '')
- +'</form></td>'
+ '<td></td>'
);
-
- cols[1].find('input[name=csrfmiddlewaretoken]').val($('input[name=csrfmiddlewaretoken]').val())
+
+ var form = makeForm($('#object-tool-add-folder').attr('href'));
+ form.append(
+ '<span style="white-space: nowrap;"><input type="text" id="add-folder-name" name="name" value="'+gettext('New folder')+'"/>'
+ +'&nbsp;<input type="submit" class="button" value="'+gettext('Save')+'" /></span>'
+ +'<input type="hidden" name="parent" value="' + (targetFolder ? targetFolder.id : '') + '" />'
+ +(targetFolder ? '<input type="hidden" name="folder_id" value="' + targetFolder.id + '" />' : '')
+ );
+
+ cols[1].append(form);
if (targetFolder && targetFolder.row) {
// TODO: Copy padding, but add indent
@@ -178,7 +195,44 @@ jQuery(function($) {
$(this).trigger('update', [$('tr', this), true]);
});
+ $.fn.setUpdateReq = function(req) {
+ $(this).abortUpdateReq();
+ $(this).data('updateReq', req);
+ };
+
+ $.fn.abortUpdateReq = function(req) {
+ var req = $(this).data('updateReq');
+ if (req) {
+ req.abort();
+ }
+ };
+
+ $.addUserMessage = function(messageText, messageId, messageClass) {
+ if (!messageClass) {
+ messageClass = 'info';
+ }
+ var defaultMessageId = 'default-message';
+ if (!messageId) {
+ messageId = defaultMessageId;
+ }
+ $('#' + defaultMessageId).remove();
+ var message = $('<li id="'+messageId+'" class="' + messageClass + '">'+messageText+'</li>')
+ var currentMessage = $('#'+messageId);
+ if (currentMessage.length > 0) {
+ currentMessage.replaceWith(message);
+ } else {
+ var messageList = $('ul.messagelist');
+ if (messageList.length == 0) {
+ $('#content').before('<ul class="messagelist"></ul>');
+ var messageList = $('ul.messagelist');
+ }
+ messageList.append(message);
+ }
+ }
+
$('#changelist').bind('update', function(e, updatedRows, isInitial) {
+ var _changelist = this;
+
// Django calls actions() when document ready. Since the ChangeList
// was updated, actions() needs to be called again:
django.jQuery("tr input.action-select").actions();
@@ -196,6 +250,76 @@ jQuery(function($) {
}
});
+ // Set up drag & drop
+
+ var rowSel = '#changelist tbody tr';
+ rows.each(function() {
+ $(this).draggable({
+ helper: function(event, ui) {
+ var copyDrag = event.altKey;
+ $(this).data('copyDrag', copyDrag);
+ var helper = $(
+ '<div class="drag-helper collapsed' + (copyDrag ? ' copy' : '') + '">'
+ + '<table><tr><td></td></tr></table>'
+ + '</div>');
+ $('td', helper).append($(this).closest('tr').find('.node-link').clone());
+ return helper;
+ },
+ handle: '.node-link',
+ appendTo: 'body',
+ }).disableSelection();
+
+ if ($('.node', this).is('.folder')) {
+ $(this).droppable({
+ drop: function(event, ui) {
+ var inputName = '_selected_action';
+ var inputSel = 'input[name=' + inputName + ']';
+ var action = $(ui.draggable).data('copyDrag') ? 'copy_selected' : 'move_selected';
+ var fields = {
+ action: action,
+ target_node: $(inputSel, this).val(),
+ execute: 1
+ };
+ fields[inputName] = $(inputSel, ui.draggable).val();
+ var form = makeForm('', fields);
+
+ // form.submit();
+ // instead:
+ var row = $(this);
+ row.addClass('loading');
+ $(_changelist).setUpdateReq($.ajax({
+ type: 'post',
+ data: form.serialize(),
+ success: function(data) {
+ row.removeClass('loading');
+ var newChangelist = $(data).find('#changelist');
+ if (newChangelist.length) {
+ // update table
+ $('#changelist').updateChangelist(newChangelist.html());
+ // display messages
+ $('.messagelist li', data).each(function() {
+ $.addUserMessage($(this).text(), null, this.className);
+ });
+ } else {
+ // if the result is no changelist, the form did not validate.
+ // display error messages:
+ $('fieldset .errorlist li', data).each(function() {
+ $.addUserMessage($(this).text(), null, 'error');
+ });
+ }
+ },
+ error: function() {
+ row.removeClass('loading');
+ }
+ }));
+
+ },
+ accept: rowSel,
+ hoverClass: 'drop-hover'
+ });
+ }
+ });
+
if (!isInitial) {
/*$('#changelist .paginator').text(ngettext('%i media object',
'%i media objects', rows.length).replace('%i', rows.length));*/
@@ -207,18 +207,7 @@ jQuery(function($) {
,addUserMessage: function(messageText, messageId)
{
- var message = $('<li id="'+messageId+'">'+messageText+'</li>')
- var currentMessage = $('#'+messageId);
- if (currentMessage.length > 0) {
- currentMessage.replaceWith(message);
- } else {
- var messageList = $('ul.messagelist');
- if (messageList.length == 0) {
- $('#content').before('<ul class="messagelist"></ul>');
- var messageList = $('ul.messagelist');
- }
- messageList.append(message);
- }
+ $.addUserMessage(messageText, messageId);
}
,queueComplete: function(numFilesUploaded) {
@@ -231,7 +220,7 @@ jQuery(function($) {
var message = gettext('loading...');
manager.addUserMessage(message, 'swfupload-queue-message');
- $.get(window.location.href, null, function(data, textStatus) {
+ $('#changelist').setUpdateReq($.get(window.location.href, null, function(data, textStatus) {
stats = _this.getStats();
if (stats.files_queued == 0) {
// reload changelist contents
@@ -244,7 +233,7 @@ jQuery(function($) {
stats.successful_uploads = 0;
_this.setStats(stats);
}
- });
+ }));
} else {
var message = gettext('There were errors during upload.');
manager.addUserMessage(message, 'swfupload-queue-message');
@@ -134,11 +134,7 @@
img.height(Math.round(curSize[1] * ratio));
});
- var req = $('#changelist').data('resizeReq');
- if (req) {
- req.abort();
- }
- $('#changelist').data('resizeReq', $.get('?thumbnail_size=' + sizeKey, function(data) {
+ $('#changelist').setUpdateReq($.get('?thumbnail_size=' + sizeKey, function(data) {
var newChangeList = $('#changelist', data);
nodes.each(function() {
var img = $('.thumbnail img', this);
@@ -162,4 +158,5 @@
<input class="default popup-select-button" type="submit" value="Select" disabled="disabled" />
</div>
{% endif %}
+
{% endblock %}

0 comments on commit 6d07766

Please sign in to comment.