Skip to content
Closed
Show file tree
Hide file tree
Changes from 1 commit
Commits
File filter

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
Next Next commit
Add ability to restrict children.
Expanding on the already present can_have_children so that it can also be a list in addition to booleans. (Empty list == False, Non-empty == True) The list is list of values similar to those values accepted in a model ForeignKey. If it is a list it is changed into a list of the content type ids of the listed models. i.e. ["self", "app.Model", "Model", Model] With "self" referring to the model on which can_have_children is defined. "app.Model" is an app_label-model dot string. "Model" is a model in the current app. And the last is a Model class or Model instance.
  • Loading branch information
vinnyrose committed Oct 10, 2014
commit 808bc123ca1a60f3347b443a4cf0a7bf4ccd6daf
42 changes: 30 additions & 12 deletions polymorphic_tree/admin/parentadmin.py
Original file line number Diff line number Diff line change
Expand Up @@ -129,19 +129,26 @@ def can_preview_object(self, node):
return hasattr(node, 'get_absolute_url')


def can_have_children(self, node):
def can_have_children(self, node, child=None, is_real=False):
"""
Define whether a node can have children.
"""
# Allow can_have_children to be either to be a property on the base class that always works.
if not node.can_have_children:
return False

# or a static variable declared on the class (avoids need for downcasted models).
NodeClass = node.get_real_instance_class()
return bool(NodeClass.can_have_children)


# or an instance variable
Node = node if is_real else node.get_real_instance()
if Node.can_have_children:
if child is None:
return True # just interested in boolean
# if we have a child we should check if it is allowed
try:
iter(Node.can_have_children)
except TypeError:
return True # not a list, boolean true
return child.polymorphic_ctype_id in Node.can_have_children
return False

# ---- Custom views ----

Expand Down Expand Up @@ -189,12 +196,23 @@ def api_node_moved_view(self, request):
except self.model.DoesNotExist as e:
return HttpResponseNotFound(json.dumps({'action': 'reload', 'error': str(e[0])}), content_type='application/json')

if not self.can_have_children(target) and position == 'inside':
return HttpResponse(json.dumps({
'action': 'reject',
'moved_id': moved_id,
'error': _(u'Cannot place \u2018{0}\u2019 below \u2018{1}\u2019; a {2} does not allow children!').format(moved, target, target._meta.verbose_name)
}), content_type='application/json', status=409) # Conflict
if position == 'inside':
error = None
real_target = target.get_real_instance()
if not self.can_have_children(real_target, is_real=True):
error = _(u'Cannot place \u2018{0}\u2019 below \u2018{1}\u2019;'
u' a {2} does not allow children!').format(moved, target,
target._meta.verbose_name)
if not self.can_have_children(real_target, moved, is_real=True):
error = _(u'Cannot place \u2018{0}\u2019 below \u2018{1}\u2019;'
u' a {2} does not allow {3} as a child!').format(moved,
target, target._meta.verbose_name, moved._meta.verbose_name)
if error is not None:
return HttpResponse(json.dumps({
'action': 'reject',
'moved_id': moved_id,
'error': error
}), content_type='application/json', status=409) # Conflict
if moved.parent_id != previous_parent_id:
return HttpResponse(json.dumps({
'action': 'reload',
Expand Down
52 changes: 48 additions & 4 deletions polymorphic_tree/models.py
Original file line number Diff line number Diff line change
@@ -1,8 +1,11 @@
"""
Model that inherits from both Polymorphic and MPTT.
"""
from __future__ import unicode_literals
from future.utils import with_metaclass
from future.utils.six import integer_types
from future.builtins import str
from django.contrib.contenttypes.models import ContentType
from django.core.exceptions import ValidationError
from django.utils.translation import ugettext_lazy as _
from mptt.models import MPTTModel, MPTTModelBase, TreeForeignKey
Expand Down Expand Up @@ -34,7 +37,9 @@ class PolymorphicTreeForeignKey(TreeForeignKey):
A foreignkey that limits the node types the parent can be.
"""
default_error_messages = {
'no_children_allowed': _("The selected node cannot have child nodes."),
'no_children_allowed': _('The selected node cannot have child nodes.'),
'child_not_allowed': _('Cannot place this page below '
'\u2018{0}\u2019; a {1} does not allow {2} as a child!')
}

def clean(self, value, model_instance):
Expand All @@ -50,13 +55,21 @@ def _validate_parent(self, parent, model_instance):
# TODO: Improve this code, it's a bit of a hack now because the base model is not known in the NodeTypePool.
base_model = _get_base_polymorphic_model(model_instance.__class__)

# Get parent, TODO: needs to downcast here to read can_have_children.
parent = base_model.objects.get(pk=parent)
parent = base_model.objects.get(pk=parent).get_real_instance()
elif not isinstance(parent, PolymorphicMPTTModel):
raise ValueError("Unknown parent value")

if parent.can_have_children:
return
try:
iter(parent.can_have_children)
except TypeError:
return # boolean True
if model_instance.polymorphic_ctype_id in parent.can_have_children:
return # child is allowed
raise ValidationError(
self.error_messages['child_not_allowed'].format(parent,
parent._meta.verbose_name,
model_instance._meta.verbose_name))

raise ValidationError(self.error_messages['no_children_allowed'])

Expand All @@ -73,6 +86,37 @@ class PolymorphicMPTTModel(with_metaclass(PolymorphicMPTTModelBase, MPTTModel, P
# Django fields
_default_manager = PolymorphicMPTTModelManager()

def __init__(self, *args, **kwargs):
super(PolymorphicMPTTModel, self).__init__(*args, **kwargs)
try:
iterator = iter(self.can_have_children)
except TypeError:
pass # can_have_children is not an iterable
else:
new_children = []
for child in iterator:
if isinstance(unicode(child), str):
child = unicode(child).lower()
# write self to refer to self
if child == 'self':
ct_id = self.polymorphic_ctype_id
else:
# either the name of a model in this app
# or the full app.model dot string
# just like a foreign key
try:
app_label, model = child.rsplit('.', 1)
except ValueError:
app_label = self._meta.app_label
model = child
ct_id = ContentType.objects.get(app_label=app_label,
model=model).id
else:
# pass in a model class
ct_id = ContentType.objects.get_for_model(child).id
new_children.append(ct_id)
self.can_have_children = new_children

class Meta:
abstract = True
ordering = ('lft',)
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -44,9 +44,8 @@
/*
* This code is currently inline because it is generated using various template variables and translation messages.
*/

var data = [{% adminlist_recursetree cl %}{% with is_leaf=node.is_leaf_node %}
{id: {{ node.pk }}, classes: 'nodetype-{{ node|real_model_name|lower }}', can_have_children: {{ node.can_have_children|yesno:'true,false' }}, label: '<div class="col-primary{% if is_leaf %} leaf{% endif %}"><div class="col first-column"><a href="{{ change_url }}"{% if cl.is_popup %} onclick="opener.dismissRelatedLookupPopup(window, {{ node.pk }}); return false;"{% endif %}>{{ first_column|escapejs }}</a></div></div>{% if other_columns %}<div class="col-metadata">{% for name, repr in other_columns %}<div class="col col-{{ name }}">{{ repr|escapejs }}</div>{% endfor %}</div>{% endif %}'{% if not is_leaf %},
{id: {{ node.pk }}, ct: {{ node.polymorphic_ctype_id }}, classes: 'nodetype-{{ ct.model|lower }}', can_have_children: {{ node.get_real_instance.can_have_children|yesnolist:"true,false" }}, label: '<div class="col-primary{% if is_leaf %} leaf{% endif %}"><div class="col first-column"><a href="{{ change_url }}"{% if cl.is_popup %} onclick="opener.dismissRelatedLookupPopup(window, {{ node.pk }}); return false;"{% endif %}>{{ first_column|escapejs }}</a></div></div>{% if other_columns %}<div class="col-metadata">{% for name, repr in other_columns %}<div class="col col-{{ name }}">{{ repr|escapejs }}</div>{% endfor %}</div>{% endif %}'{% if not is_leaf %},
children: [ {{ children }}
]{% endif %}},{% endwith %}{% endadminlist_recursetree %}
null // for MSIE
Expand All @@ -64,9 +63,12 @@
$div.append(contents);
}

function onCanMoveTo(moved_node, target_node, position)
{
return ( target_node.can_have_children || position != 'inside' );
function onCanMoveTo(moved_node, target_node, position) {
var can_have_children = target_node.can_have_children;
if (Object.prototype.toString.call(can_have_children) === '[object Array]'){
can_have_children = jQuery.inArray(moved_node.ct, can_have_children) > -1;
}
return ( can_have_children || position != 'inside' );
}

var tree = jQuery("#js-result-list").tree({
Expand Down
9 changes: 9 additions & 0 deletions polymorphic_tree/templatetags/polymorphic_tree_admin_tags.py
Original file line number Diff line number Diff line change
Expand Up @@ -2,13 +2,22 @@
from django.contrib.admin.views.main import ChangeList
from django.contrib.contenttypes.models import ContentType
from django.template import Library, Node, TemplateSyntaxError, Variable
from django.template.defaultfilters import yesno
from django.utils.safestring import mark_safe
from mptt.templatetags.mptt_tags import cache_tree_children
from polymorphic_tree.templatetags.stylable_admin_list import stylable_column_repr


register = Library()

@register.filter
def yesnolist(value, arg=None):
try:
iter(value)
except TypeError:
return yesno(value, arg)
return value


@register.filter
def real_model_name(node):
Expand Down