Merge branch '0.7.2'

This commit is contained in:
Corey Oordt 2011-08-19 10:57:09 -04:00
commit bc7fa900a2
6 changed files with 228 additions and 80 deletions

View file

@ -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

View file

@ -1,18 +1,18 @@
__version_info__ = {
'major': 0,
'minor': 7,
'micro': 1,
'micro': 2,
'releaselevel': 'final',
'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()

View file

@ -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

View file

@ -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
)

View file

@ -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
@ -12,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('.')

View file

@ -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