Skip to content

Commit

Permalink
Refactored the editor to become Django 1.1.1 compatible and some PEP8…
Browse files Browse the repository at this point in the history
… formatting.
  • Loading branch information
Corey Oordt committed Aug 19, 2011
1 parent 5a74525 commit e7fad27
Showing 1 changed file with 203 additions and 60 deletions.
263 changes: 203 additions & 60 deletions editor/tree_editor.py
Original file line number Diff line number Diff line change
@@ -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 settings
import django

class TreeChangeList(ChangeList):
def get_ordering(self):
if isinstance(self.model_admin, TreeEditor):
return '', ''
return super(ChangeList, self).get_ordering()
import settings

def _build_tree_structure(cls):
class TreeEditorQuerySet(QuerySet):
"""
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": []},
...
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.
"""
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)
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)

return all_nodes
class TreeChangeList(ChangeList):
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()),
Expand All @@ -70,6 +103,129 @@ def get_changelist(self, request, **kwargs):
"""
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
Expand All @@ -78,30 +234,17 @@ def changelist_view(self, request, extra_context=None, *args, **kwargs):
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

0 comments on commit e7fad27

Please sign in to comment.