mirror of
https://github.com/jazzband/django-categories.git
synced 2026-05-17 20:11:08 +00:00
Merge branch '0.7.2'
This commit is contained in:
commit
bc7fa900a2
6 changed files with 228 additions and 80 deletions
|
|
@ -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
|
||||
|
|
|
|||
|
|
@ -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()
|
||||
|
|
|
|||
|
|
@ -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
|
||||
|
|
|
|||
|
|
@ -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
|
||||
)
|
||||
|
|
|
|||
|
|
@ -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('.')
|
||||
|
|
|
|||
|
|
@ -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
|
||||
|
|
|
|||
Loading…
Reference in a new issue