diff --git a/categories/editor.py b/categories/editor.py new file mode 100644 index 0000000..914aef7 --- /dev/null +++ b/categories/editor.py @@ -0,0 +1,218 @@ +import re + +import django +from django import template +from django.conf import settings +from django.contrib.admin.options import IncorrectLookupParameters +from django.contrib.admin.templatetags import admin_list +from django.contrib.admin.util import unquote +from django.core.exceptions import ImproperlyConfigured +from django.db import connection, transaction, models +from django.http import HttpResponseRedirect, HttpResponse +from django.shortcuts import render_to_response +from django.utils import dateformat, simplejson +from django.utils.html import escape, conditional_escape +from django.utils.encoding import force_unicode, smart_str, smart_unicode +from django.utils.functional import update_wrapper +from django.utils.safestring import mark_safe +from django.utils.text import capfirst +from django.utils.translation import get_date_formats, get_partial_date_formats, ugettext as _ + +EDITOR_ADMIN_MEDIA = getattr(settings, 'EDITOR_ADMIN_MEDIA', '/media/editor/') +FRONTEND_EDITING_MATCHER = re.compile(r'(\d+)/(\w+)/(\d+)') + +class TreeEditorMixin(object): + actions = None # TreeEditorMixin does not like the checkbox column + + def changelist_view(self, request, extra_context=None): + # handle AJAX requests + if request.is_ajax(): + cmd = request.POST.get('__cmd') + if cmd=='save_tree': + return self._save_tree(request) + elif cmd=='delete_item': + return self._delete_item(request) + + return HttpResponse('Oops. AJAX request not understood.') + + from django.contrib.admin.views.main import ChangeList, ERROR_FLAG + opts = self.model._meta + app_label = opts.app_label + + if not self.has_change_permission(request, None): + raise PermissionDenied + try: + if django.VERSION[0] < 1 or (django.VERSION[0] == 1 and django.VERSION[1] < 1): + self.changelist = ChangeList(request, self.model, self.list_display, + self.list_display_links, self.list_filter, self.date_hierarchy, + self.search_fields, self.list_select_related, self.list_per_page, + self) + else: + self.changelist = ChangeList(request, self.model, self.list_display, + self.list_display_links, self.list_filter, self.date_hierarchy, + self.search_fields, self.list_select_related, self.list_per_page, + self.list_editable, self) + except IncorrectLookupParameters: + # Wacky lookup parameters were given, so redirect to the main + # changelist page, without parameters, and pass an 'invalid=1' + # parameter via the query string. If wacky parameters were given and + # the 'invalid=1' parameter was already in the query string, something + # is screwed up with the database, so display an error page. + if ERROR_FLAG in request.GET.keys(): + return render_to_response('admin/invalid_setup.html', {'title': _('Database error')}) + return HttpResponseRedirect(request.path + '?' + ERROR_FLAG + '=1') + + context = { + 'EDITOR_ADMIN_MEDIA': EDITOR_ADMIN_MEDIA, + 'title': self.changelist.title, + 'is_popup': self.changelist.is_popup, + 'cl': self.changelist, + 'has_add_permission': self.has_add_permission(request), + 'root_path': self.admin_site.root_path, + 'app_label': app_label, + 'object_list': self.model._tree_manager.all(), + 'tree_editor': self, + + 'result_headers': list(admin_list.result_headers(self.changelist)), + } + context.update(extra_context or {}) + return render_to_response([ + 'admin/%s/%s/tree_editor.html' % (app_label, opts.object_name.lower()), + 'admin/%s/tree_editor.html' % app_label, + 'admin/editor/tree_editor.html', + ], context, context_instance=template.RequestContext(request)) + + def object_list(self): + for item in self.model._tree_manager.all().select_related(): + yield item, unicode(item), _properties(self.changelist, item) + + def _save_tree(self, request): + itemtree = simplejson.loads(request.POST['tree']) + # 0 = tree_id, 1 = parent_id, 2 = left, 3 = right, 4 = level, 5 = item_id + sql = "UPDATE %s SET %s=%%s, %s_id=%%s, %s=%%s, %s=%%s, %s=%%s WHERE %s=%%s" % ( + self.model._meta.db_table, + self.model._meta.tree_id_attr, + self.model._meta.parent_attr, + self.model._meta.left_attr, + self.model._meta.right_attr, + self.model._meta.level_attr, + self.model._meta.pk.column) + + connection.cursor().executemany(sql, itemtree) + transaction.commit_unless_managed() + + return HttpResponse("OK", mimetype="text/plain") + + def _delete_item(self, request): + item_id = request.POST['item_id'] + obj = self.model._default_manager.get(pk=unquote(item_id)) + obj.delete() + return HttpResponse("OK", mimetype="text/plain") + + + +# copied from django.contrib.admin.templatetags.admin_list and slightly modified +def _boolean_icon(field_val): + BOOLEAN_MAPPING = {True: 'yes', False: 'no', None: 'unknown'} + return mark_safe(u'%s' % (settings.ADMIN_MEDIA_PREFIX, BOOLEAN_MAPPING[field_val], field_val)) + + +def _properties(cl, result): + first = True + pk = cl.lookup_opts.pk.attname + EMPTY_CHANGELIST_VALUE = '(None)' + + for field_name in cl.list_display[1:]: + try: + f = cl.lookup_opts.get_field(field_name) + except models.FieldDoesNotExist: + try: + if callable(field_name): + attr = field_name + value = attr(result) + elif hasattr(cl.model_admin, field_name) and \ + not field_name == '__str__' and not field_name == '__unicode__': + attr = getattr(cl.model_admin, field_name) + value = attr(result) + else: + attr = getattr(result, field_name) + if callable(attr): + value = attr() + else: + value = attr + allow_tags = getattr(attr, 'allow_tags', False) + boolean = getattr(attr, 'boolean', False) + if boolean: + allow_tags = True + result_repr = _boolean_icon(value) + else: + result_repr = smart_unicode(value) + except (AttributeError, models.ObjectDoesNotExist): + result_repr = EMPTY_CHANGELIST_VALUE + else: + # Strip HTML tags in the resulting text, except if the + # function has an "allow_tags" attribute set to True. + if not allow_tags: + result_repr = escape(result_repr) + else: + result_repr = mark_safe(result_repr) + else: + field_val = getattr(result, f.attname) + + if isinstance(f.rel, models.ManyToOneRel): + if field_val is not None: + result_repr = escape(getattr(result, f.name)) + else: + result_repr = EMPTY_CHANGELIST_VALUE + # Dates and times are special: They're formatted in a certain way. + elif isinstance(f, models.DateField) or isinstance(f, models.TimeField): + if field_val: + (date_format, datetime_format, time_format) = get_date_formats() + if isinstance(f, models.DateTimeField): + result_repr = capfirst(dateformat.format(field_val, datetime_format)) + elif isinstance(f, models.TimeField): + result_repr = capfirst(dateformat.time_format(field_val, time_format)) + else: + result_repr = capfirst(dateformat.format(field_val, date_format)) + else: + result_repr = EMPTY_CHANGELIST_VALUE + # Booleans are special: We use images. + elif isinstance(f, models.BooleanField) or isinstance(f, models.NullBooleanField): + result_repr = _boolean_icon(field_val) + # DecimalFields are special: Zero-pad the decimals. + elif isinstance(f, models.DecimalField): + if field_val is not None: + result_repr = ('%%.%sf' % f.decimal_places) % field_val + else: + result_repr = EMPTY_CHANGELIST_VALUE + # Fields with choices are special: Use the representation + # of the choice. + elif f.flatchoices: + result_repr = dict(f.flatchoices).get(field_val, EMPTY_CHANGELIST_VALUE) + else: + result_repr = escape(field_val) + if force_unicode(result_repr) == '': + result_repr = mark_safe(' ') + # If list_display_links not defined, add the link tag to the first field + if (first and not cl.list_display_links) or field_name in cl.list_display_links: + table_tag = {True:'th', False:'td'}[first] + first = False + url = cl.url_for_result(result) + # Convert the pk to something that can be used in Javascript. + # Problem cases are long ints (23L) and non-ASCII strings. + if cl.to_field: + attr = str(cl.to_field) + else: + attr = pk + value = result.serializable_value(attr) + result_id = repr(force_unicode(value))[1:] + yield mark_safe(u'<%s>%s' % \ + (table_tag, url, (cl.is_popup and ' onclick="opener.dismissRelatedLookupPopup(window, %s); return false;"' % result_id or ''), conditional_escape(result_repr), table_tag)) + else: + # By default the fields come from ModelAdmin.list_editable, but if we pull + # the fields out of the form instead of list_editable custom admins + # can provide fields on a per request basis + result_repr = conditional_escape(result_repr) + yield mark_safe(u'%s' % (result_repr)) + + first = False