From 4107cdd125c6f4e9840dbc48a63c03d65e4da8e0 Mon Sep 17 00:00:00 2001 From: Corey Oordt Date: Thu, 18 Aug 2011 10:31:22 -0400 Subject: [PATCH 1/8] Changed the DatabaseError import to be more compatible --- categories/migration.py | 3 +-- 1 file changed, 1 insertion(+), 2 deletions(-) diff --git a/categories/migration.py b/categories/migration.py index 18ef603..9d39ee2 100644 --- a/categories/migration.py +++ b/categories/migration.py @@ -1,5 +1,4 @@ -from django.db import models -from django.db.utils import DatabaseError +from django.db import models, DatabaseError from south.db import db from south.signals import post_migrate From 5a745254466024d3d6a932e74d152a4ed069beb9 Mon Sep 17 00:00:00 2001 From: Corey Oordt Date: Thu, 18 Aug 2011 10:31:52 -0400 Subject: [PATCH 2/8] Ensure that the slug is always within the 50 characters it needs to be. --- categories/management/commands/import_categories.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/categories/management/commands/import_categories.py b/categories/management/commands/import_categories.py index 1435636..76dc605 100644 --- a/categories/management/commands/import_categories.py +++ b/categories/management/commands/import_categories.py @@ -29,7 +29,7 @@ class Command(BaseCommand): """ return Category.objects.create( name=string.strip(), - slug=slugify(string.strip()), + slug=slugify(string.strip())[:49], parent=parent, order=order ) From e7fad278d12048dc743ee4ca2034f5d43b687d5d Mon Sep 17 00:00:00 2001 From: Corey Oordt Date: Fri, 19 Aug 2011 10:04:18 -0400 Subject: [PATCH 3/8] Refactored the editor to become Django 1.1.1 compatible and some PEP8 formatting. --- editor/tree_editor.py | 267 ++++++++++++++++++++++++++++++++---------- 1 file changed, 205 insertions(+), 62 deletions(-) diff --git a/editor/tree_editor.py b/editor/tree_editor.py index e84c0e5..c5a4332 100644 --- a/editor/tree_editor.py +++ b/editor/tree_editor.py @@ -1,62 +1,95 @@ -from django.conf import settings as django_settings from django.contrib import admin -from django.contrib.admin.util import unquote +from django.db.models.query import QuerySet from django.contrib.admin.views.main import ChangeList -from django.http import Http404, HttpResponse, HttpResponseRedirect, HttpResponseBadRequest -from django.utils import simplejson -from django.utils.safestring import mark_safe +from django.http import HttpResponseRedirect from django.utils.translation import ugettext_lazy as _ +from django.contrib.admin.options import IncorrectLookupParameters +from django import template +from django.shortcuts import render_to_response + +import django import settings +class TreeEditorQuerySet(QuerySet): + """ + The TreeEditorQuerySet is a special query set used only in the TreeEditor + ChangeList page. The only difference to a regular QuerySet is that it + will enforce: + + (a) The result is ordered in correct tree order so that + the TreeAdmin works all right. + + (b) It ensures that all ancestors of selected items are included + in the result set, so the resulting tree display actually + makes sense. + """ + def iterator(self): + qs = self + # Reaching into the bowels of query sets to find out whether the qs is + # actually filtered and we need to do the INCLUDE_ANCESTORS dance at all. + # INCLUDE_ANCESTORS is quite expensive, so don't do it if not needed. + is_filtered = bool(qs.query.where.children) + if is_filtered: + include_pages = set() + # Order by 'rght' will return the tree deepest nodes first; + # this cuts down the number of queries considerably since all ancestors + # will already be in include_pages when they are checked, thus not + # trigger additional queries. + for p in super(TreeEditorQuerySet, self.order_by('rght')).iterator(): + if p.parent_id and p.parent_id not in include_pages and \ + p.id not in include_pages: + ancestor_id_list = p.get_ancestors().values_list('id', flat=True) + include_pages.update(ancestor_id_list) + + if include_pages: + qs = qs | self.model._default_manager.filter(id__in=include_pages) + + qs = qs.distinct() + + for obj in super(TreeEditorQuerySet, qs).iterator(): + yield obj + + def __getitem__(self, index): + return self # Don't even try to slice + + def get(self, *args, **kwargs): + """ + Quick and dirty hack to fix change_view and delete_view; they use + self.queryset(request).get(...) to get the object they should work + with. Our modifications to the queryset when INCLUDE_ANCESTORS is + enabled make get() fail often with a MultipleObjectsReturned + exception. + """ + return self.model._default_manager.get(*args, **kwargs) + class TreeChangeList(ChangeList): - def get_ordering(self): - if isinstance(self.model_admin, TreeEditor): - return '', '' - return super(ChangeList, self).get_ordering() - -def _build_tree_structure(cls): - """ - Build an in-memory representation of the item tree, trying to keep - database accesses down to a minimum. The returned dictionary looks like - this (as json dump): - - {"6": {"id": 6, "children": [7, 8, 10], "parent": null, "descendants": [7, 12, 13, 8, 10]}, - "7": {"id": 7, "children": [12], "parent": 6, "descendants": [12, 13]}, - "8": {"id": 8, "children": [], "parent": 6, "descendants": []}, - ... - - """ - all_nodes = { } - def add_as_descendant(n, p): - if not n: return - all_nodes[n.id]['descendants'].append(p.id) - add_as_descendant(n.parent, p) - - for p in cls.objects.order_by('tree_id', 'lft'): - all_nodes[p.id] = { 'id': p.id, 'children' : [ ], 'descendants' : [ ], 'parent' : p.parent_id } - if(p.parent_id): - all_nodes[p.parent_id]['children'].append(p.id) - add_as_descendant(p.parent, p) - - return all_nodes + def _get_default_ordering(self): + return '', '' #('tree_id', 'lft') + + def get_ordering(self, request=None): + return '', '' #('tree_id', 'lft') + + def get_query_set(self): + qs = super(TreeChangeList, self).get_query_set() + return qs.order_by('tree_id', 'lft') class TreeEditor(admin.ModelAdmin): - list_per_page = 10000 # We can't have pagination + list_per_page = 999999999 # We can't have pagination class Media: css = {'all':(settings.MEDIA_PATH + "jquery.treeTable.css",)} js = [] - + js.extend((settings.MEDIA_PATH + "jquery.treeTable.js",)) - + def __init__(self, *args, **kwargs): super(TreeEditor, self).__init__(*args, **kwargs) - + self.list_display = list(self.list_display) if 'action_checkbox' in self.list_display: self.list_display.remove('action_checkbox') - + opts = self.model._meta self.change_list_template = [ 'admin/%s/%s/editor/tree_editor.html' % (opts.app_label, opts.object_name.lower()), @@ -70,6 +103,129 @@ class TreeEditor(admin.ModelAdmin): """ return TreeChangeList + def old_changelist_view(self, request, extra_context=None): + "The 'change list' admin view for this model." + from django.contrib.admin.views.main import ERROR_FLAG + from django.core.exceptions import PermissionDenied + from django.utils.encoding import force_unicode + from django.utils.translation import ungettext + opts = self.model._meta + app_label = opts.app_label + if not self.has_change_permission(request, None): + raise PermissionDenied + + # Check actions to see if any are available on this changelist + actions = self.get_actions(request) + + # Remove action checkboxes if there aren't any actions available. + list_display = list(self.list_display) + if not actions: + try: + list_display.remove('action_checkbox') + except ValueError: + pass + + try: + cl = TreeChangeList(request, self.model, 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') + + # If the request was POSTed, this might be a bulk action or a bulk edit. + # Try to look up an action first, but if this isn't an action the POST + # will fall through to the bulk edit check, below. + if actions and request.method == 'POST': + response = self.response_action(request, queryset=cl.get_query_set()) + if response: + return response + + # If we're allowing changelist editing, we need to construct a formset + # for the changelist given all the fields to be edited. Then we'll + # use the formset to validate/process POSTed data. + formset = cl.formset = None + + # Handle POSTed bulk-edit data. + if request.method == "POST" and self.list_editable: + FormSet = self.get_changelist_formset(request) + formset = cl.formset = FormSet( + request.POST, request.FILES, queryset=cl.result_list + ) + if formset.is_valid(): + changecount = 0 + for form in formset.forms: + if form.has_changed(): + obj = self.save_form(request, form, change=True) + self.save_model(request, obj, form, change=True) + form.save_m2m() + change_msg = self.construct_change_message(request, form, None) + self.log_change(request, obj, change_msg) + changecount += 1 + + if changecount: + if changecount == 1: + name = force_unicode(opts.verbose_name) + else: + name = force_unicode(opts.verbose_name_plural) + msg = ungettext( + "%(count)s %(name)s was changed successfully.", + "%(count)s %(name)s were changed successfully.", + changecount) % {'count': changecount, + 'name': name, + 'obj': force_unicode(obj)} + self.message_user(request, msg) + + return HttpResponseRedirect(request.get_full_path()) + + # Handle GET -- construct a formset for display. + elif self.list_editable: + FormSet = self.get_changelist_formset(request) + formset = cl.formset = FormSet(queryset=cl.result_list) + + # Build the list of media to be used by the formset. + if formset: + media = self.media + formset.media + else: + media = self.media + + # Build the action form and populate it with available actions. + if actions: + action_form = self.action_form(auto_id=None) + action_form.fields['action'].choices = self.get_action_choices(request) + else: + action_form = None + + context = { + 'title': cl.title, + 'is_popup': cl.is_popup, + 'cl': cl, + 'media': media, + 'has_add_permission': self.has_add_permission(request), + 'root_path': self.admin_site.root_path, + 'app_label': app_label, + 'action_form': action_form, + 'actions_on_top': self.actions_on_top, + 'actions_on_bottom': self.actions_on_bottom, + } + context.update(extra_context or {}) + context_instance = template.RequestContext( + request, current_app=self.admin_site.name + ) + return render_to_response(self.change_list_template or [ + 'admin/%s/%s/change_list.html' % (app_label, opts.object_name.lower()), + 'admin/%s/change_list.html' % app_label, + 'admin/change_list.html' + ], context, context_instance=context_instance) + def changelist_view(self, request, extra_context=None, *args, **kwargs): """ Handle the changelist view, the django view for the model instances @@ -78,30 +234,17 @@ class TreeEditor(admin.ModelAdmin): extra_context = extra_context or {} extra_context['EDITOR_MEDIA_PATH'] = settings.MEDIA_PATH extra_context['EDITOR_TREE_INITIAL_STATE'] = settings.TREE_INITIAL_STATE - extra_context['tree_structure'] = mark_safe(simplejson.dumps( - _build_tree_structure(self.model))) - - return super(TreeEditor, self).changelist_view(request, extra_context, *args, **kwargs) - - def _move_node(self, request): - cut_item = self.model._tree_manager.get(pk=request.POST.get('cut_item')) - pasted_on = self.model._tree_manager.get(pk=request.POST.get('pasted_on')) - position = request.POST.get('position') - - if position in ('last-child', 'left'): - self.model._tree_manager.move_node(cut_item, pasted_on, position) - - # Ensure that model save has been run - source = self.model._tree_manager.get(pk=request.POST.get('cut_item')) - source.save() - - return HttpResponse('OK') - return HttpResponse('FAIL') - + if django.VERSION[2] >= 2: + return super(TreeEditor, self).changelist_view( + request, extra_context, *args, **kwargs) + else: + return self.old_changelist_view(request, extra_context) + def queryset(self, request): """ Returns a QuerySet of all model instances that can be edited by the admin site. This is used by changelist_view. """ - # Use default ordering, always - return self.model._default_manager.get_query_set() + qs = self.model._default_manager.get_query_set() + qs.__class__ = TreeEditorQuerySet + return qs From 4d4793db36fcb3f38e407341f01214b58af5add8 Mon Sep 17 00:00:00 2001 From: Corey Oordt Date: Fri, 19 Aug 2011 10:04:39 -0400 Subject: [PATCH 4/8] PEP 8 formatting --- categories/admin.py | 20 +++++++++++--------- 1 file changed, 11 insertions(+), 9 deletions(-) diff --git a/categories/admin.py b/categories/admin.py index c3463ed..7a4b0ce 100644 --- a/categories/admin.py +++ b/categories/admin.py @@ -1,9 +1,7 @@ -from django.conf import settings from django.contrib import admin from django import forms from django.template.defaultfilters import slugify -from mptt.forms import TreeNodeChoiceField from editor.tree_editor import TreeEditor from genericcollection import GenericCollectionTabularInline @@ -62,30 +60,34 @@ class CategoryAdminForm(forms.ModelForm): kwargs['parent__pk'] = int(self.cleaned_data['parent'].id) this_level_slugs = [c['slug'] for c in Category.objects.filter(**kwargs).values('id','slug') if c['id'] != self.instance.id] if self.cleaned_data['slug'] in this_level_slugs: - raise forms.ValidationError("A category slug must be unique among categories at its level.") + raise forms.ValidationError("A category slug must be unique among" + "categories at its level.") # Validate Category Parent # Make sure the category doesn't set itself or any of its children as its parent." if self.cleaned_data.get('parent', None) is None or self.instance.id is None: return self.cleaned_data elif self.cleaned_data['parent'].id == self.instance.id: - raise forms.ValidationError("You can't set the parent of the category to itself.") + raise forms.ValidationError("You can't set the parent of the " + "category to itself.") elif self.cleaned_data['parent'].id in [i[0] for i in self.instance.get_descendants().values_list('id')]: - raise forms.ValidationError("You can't set the parent of the category to a descendant.") + raise forms.ValidationError("You can't set the parent of the " + "category to a descendant.") return self.cleaned_data class CategoryAdmin(TreeEditor, admin.ModelAdmin): form = CategoryAdminForm - list_display = ('name','order','alternate_title',) - search_fields = (('name',)) + list_display = ('name', 'alternate_title', ) + search_fields = ('name', 'alternate_title', ) prepopulated_fields = {'slug': ('name',)} fieldsets = ( (None, { 'fields': ('parent', 'name', 'thumbnail') }), ('Meta Data', { - 'fields': ('alternate_title', 'alternate_url', 'description', 'meta_keywords', 'meta_extra'), + 'fields': ('alternate_title', 'alternate_url', 'description', + 'meta_keywords', 'meta_extra'), 'classes': ('collapse',), }), ('Advanced', { @@ -107,7 +109,7 @@ for model, modeladmin in admin.site._registry.items(): fields = [cat.split('.')[2] for cat in model_registry if model_registry[cat] == model] # check each field to see if already defined for cat in fields: - for k,v in fieldsets: + for k, v in fieldsets: if cat in v['fields']: fields.remove(cat) # if there are any fields left, add them under the categories fieldset From 309accf3e0ca8affa346ff0d1bec193fb6f99780 Mon Sep 17 00:00:00 2001 From: Corey Oordt Date: Fri, 19 Aug 2011 10:05:25 -0400 Subject: [PATCH 5/8] Updated the get_version function to be PEP 386 compliant and version bump to 0.7.2b1 --- categories/__init__.py | 12 ++++++------ 1 file changed, 6 insertions(+), 6 deletions(-) diff --git a/categories/__init__.py b/categories/__init__.py index 1a560ac..c5c9278 100644 --- a/categories/__init__.py +++ b/categories/__init__.py @@ -1,18 +1,18 @@ __version_info__ = { 'major': 0, 'minor': 7, - 'micro': 1, - 'releaselevel': 'final', + 'micro': 2, + 'releaselevel': 'beta', 'serial': 1 } -def get_version(): +def get_version(short=False): + assert __version_info__['releaselevel'] in ('alpha', 'beta', 'final') vers = ["%(major)i.%(minor)i" % __version_info__, ] - if __version_info__['micro']: vers.append(".%(micro)i" % __version_info__) - if __version_info__['releaselevel'] != 'final': - vers.append('%(releaselevel)s%(serial)i' % __version_info__) + if __version_info__['releaselevel'] != 'final' and not short: + vers.append('%s%i' % (__version_info__['releaselevel'][0], __version_info__['serial'])) return ''.join(vers) __version__ = get_version() From 7cb74829ea296a23386eff50496c4d5223c79906 Mon Sep 17 00:00:00 2001 From: Corey Oordt Date: Fri, 19 Aug 2011 10:13:18 -0400 Subject: [PATCH 6/8] Added a check in migrate_app to see if the app is a string or not. --- categories/migration.py | 4 +++- 1 file changed, 3 insertions(+), 1 deletion(-) diff --git a/categories/migration.py b/categories/migration.py index 9d39ee2..dc69e70 100644 --- a/categories/migration.py +++ b/categories/migration.py @@ -11,8 +11,10 @@ def migrate_app(app, *args, **kwargs): Migrate all models of this app registered """ # pull the information from the registry + if not isinstance(app, basestring): + return fields = [fld for fld in field_registry.keys() if fld.startswith(app)] - + # call the south commands to add the fields/tables for fld in fields: app_name, model_name, field_name = fld.split('.') From 8d0b5f942b1e02ff7d808960c84718981557c379 Mon Sep 17 00:00:00 2001 From: Corey Oordt Date: Fri, 19 Aug 2011 10:19:14 -0400 Subject: [PATCH 7/8] Version bump to 0.7.2 --- categories/__init__.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/categories/__init__.py b/categories/__init__.py index c5c9278..9f7ac4f 100644 --- a/categories/__init__.py +++ b/categories/__init__.py @@ -2,7 +2,7 @@ __version_info__ = { 'major': 0, 'minor': 7, 'micro': 2, - 'releaselevel': 'beta', + 'releaselevel': 'final', 'serial': 1 } From 6f3a925ed50a4de8a7c643a840ad853b2f74fedd Mon Sep 17 00:00:00 2001 From: Corey Oordt Date: Fri, 19 Aug 2011 10:56:58 -0400 Subject: [PATCH 8/8] Pruning the example project --- MANIFEST.in | 2 ++ 1 file changed, 2 insertions(+) diff --git a/MANIFEST.in b/MANIFEST.in index e5d9c7f..512d495 100644 --- a/MANIFEST.in +++ b/MANIFEST.in @@ -11,3 +11,5 @@ recursive-include editor *.html *.gif *.png *.css *.js recursive-include doc_src *.rst *.txt *.png *.css *.html *.js include doc_src/Makefile include doc_src/make.bat + +prune example