Skip to content

Commit d65e7b3

Browse files
committed
Merge branch 'restrict_children_2' of https://github.com/vinnyrose/django-polymorphic-tree into vinnyrose-restrict_children_2
2 parents ec885e3 + 6d9fbf5 commit d65e7b3

File tree

3 files changed

+95
-19
lines changed

3 files changed

+95
-19
lines changed

polymorphic_tree/admin/parentadmin.py

Lines changed: 26 additions & 12 deletions
Original file line numberDiff line numberDiff line change
@@ -143,18 +143,21 @@ def can_preview_object(self, node):
143143
return hasattr(node, 'get_absolute_url')
144144

145145

146-
def can_have_children(self, node):
146+
def can_have_children(self, node, child=None):
147147
"""
148148
Define whether a node can have children.
149149
"""
150-
# Allow can_have_children to be either to be a property on the base class that always works.
151-
if not node.can_have_children:
150+
can_have_children = node.can_have_children
151+
if not can_have_children:
152152
return False
153153

154-
# or a static variable declared on the class (avoids need for downcasted models).
155-
NodeClass = node.get_real_instance_class()
156-
return bool(NodeClass.can_have_children)
154+
child_types = node.get_child_types()
157155

156+
# if child is None then we are just interested in boolean
157+
# if we have a child we should check if it is allowed
158+
return (can_have_children and (
159+
child is None or len(child_types) == 0 or
160+
child.polymorphic_ctype_id in child_types))
158161

159162

160163
# ---- Custom views ----
@@ -207,12 +210,23 @@ def api_node_moved_view(self, request):
207210
'error': _('You do not have permission to move this node.')
208211
}), content_type='application/json', status=409)
209212

210-
if not self.can_have_children(target) and position == 'inside':
211-
return HttpResponse(json.dumps({
212-
'action': 'reject',
213-
'moved_id': moved_id,
214-
'error': _(u'Cannot place \u2018{0}\u2019 below \u2018{1}\u2019; a {2} does not allow children!').format(moved, target, target._meta.verbose_name)
215-
}), content_type='application/json', status=409) # Conflict
213+
if position == 'inside':
214+
error = None
215+
if not self.can_have_children(target):
216+
error = _(u'Cannot place \u2018{0}\u2019 below \u2018{1}\u2019;'
217+
u' a {2} does not allow children!').format(moved, target,
218+
target._meta.verbose_name)
219+
elif not self.can_have_children(target, moved):
220+
error = _(u'Cannot place \u2018{0}\u2019 below \u2018{1}\u2019;'
221+
u' a {2} does not allow {3} as a child!').format(moved,
222+
target, target._meta.verbose_name, moved._meta.verbose_name)
223+
if error is not None:
224+
return HttpResponse(json.dumps({
225+
'action': 'reject',
226+
'moved_id': moved_id,
227+
'error': error
228+
}), content_type='application/json', status=409) # Conflict
229+
216230
if getattr(moved, '{}_id'.format(moved._mptt_meta.parent_attr)) != previous_parent_id:
217231
return HttpResponse(json.dumps({
218232
'action': 'reload',

polymorphic_tree/models.py

Lines changed: 61 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -2,10 +2,11 @@
22
Model that inherits from both Polymorphic and MPTT.
33
"""
44
import django
5+
from six import integer_types, string_types
56
from future.utils import with_metaclass
7+
from django.contrib.contenttypes.models import ContentType
68
from django.core.exceptions import ValidationError
79
from django.utils.translation import ugettext_lazy as _
8-
from django.utils.six import integer_types
910
from mptt.models import MPTTModel, MPTTModelBase, TreeForeignKey, raise_if_unsaved
1011
from polymorphic.base import PolymorphicModelBase
1112
from polymorphic_tree.managers import PolymorphicMPTTModelManager
@@ -55,15 +56,24 @@ def _validate_parent(self, parent, model_instance):
5556
elif isinstance(parent, integer_types):
5657
# TODO: Improve this code, it's a bit of a hack now because the base model is not known in the NodeTypePool.
5758
base_model = _get_base_polymorphic_model(model_instance.__class__)
58-
59-
# Get parent, TODO: needs to downcast here to read can_have_children.
6059
parent = base_model.objects.get(pk=parent)
6160
elif not isinstance(parent, PolymorphicMPTTModel):
6261
raise ValueError("Unknown parent value")
6362

6463
if parent.can_have_children:
6564
return
6665

66+
can_have_children = parent.can_have_children
67+
if can_have_children:
68+
child_types = parent.get_child_types()
69+
if (len(child_types) == 0 or
70+
model_instance.polymorphic_ctype_id in child_types):
71+
return # child is allowed
72+
raise ValidationError(
73+
self.error_messages['child_not_allowed'].format(parent,
74+
parent._meta.verbose_name,
75+
model_instance._meta.verbose_name))
76+
6777
raise ValidationError(self.error_messages['no_children_allowed'])
6878

6979

@@ -75,13 +85,61 @@ class PolymorphicMPTTModel(with_metaclass(PolymorphicMPTTModelBase, MPTTModel, P
7585

7686
#: Whether the node type allows to have children.
7787
can_have_children = True
88+
#: Allowed child types for this page.
89+
child_types = []
90+
# Cache child types using a class variable to ensure that get_child_types
91+
# is run once per page class, per django initiation.
92+
__child_types = {}
7893

7994
# Django fields
8095
if django.VERSION >= (1, 10):
8196
objects = PolymorphicMPTTModelManager()
8297
else:
8398
_default_manager = PolymorphicMPTTModelManager()
8499

100+
@property
101+
def page_key(self):
102+
"""
103+
A unique key for this page to ensure get_child_types is run once per
104+
page.
105+
"""
106+
return repr(self)
107+
108+
def get_child_types(self):
109+
"""
110+
Get the allowed child types and convert them into content type ids.
111+
This allows for the lookup of allowed children in the admin tree.
112+
"""
113+
key = self.page_key
114+
child_types = self._PolymorphicMPTTModel__child_types
115+
if self.can_have_children and child_types.setdefault(key, None) is None:
116+
new_children = []
117+
iterator = iter(self.child_types)
118+
for child in iterator:
119+
if isinstance(child, string_types):
120+
child = str(child).lower()
121+
# write self to refer to self
122+
if child == 'self':
123+
ct_id = self.polymorphic_ctype_id
124+
else:
125+
# either the name of a model in this app
126+
# or the full app.model dot string
127+
# just like a foreign key
128+
try:
129+
app_label, model = child.rsplit('.', 1)
130+
except ValueError:
131+
app_label = self._meta.app_label
132+
model = child
133+
ct_id = ContentType.objects.get(app_label=app_label,
134+
model=model).id
135+
else:
136+
# pass in a model class
137+
ct_id = ContentType.objects.get_for_model(child).id
138+
new_children.append(ct_id)
139+
child_types[key] = new_children
140+
return child_types[key]
141+
142+
85143
class Meta:
86144
abstract = True
87145
ordering = ('tree_id', 'lft',)

polymorphic_tree/templates/admin/polymorphic_tree/jstree_list_results.html

Lines changed: 8 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -47,7 +47,7 @@
4747
*/
4848

4949
var data = [{% adminlist_recursetree cl %}{% with is_leaf=node.is_leaf_node %}
50-
{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 %},
50+
{id: {{ node.pk }}, ct: {{ node.polymorphic_ctype_id }}, classes: 'nodetype-{{ ct.model|lower }}', can_have_children: {{ node.can_have_children|yesno:"true,false" }}, child_types: {% firstof node.get_child_types "[]" %}, 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 %},
5151
children: [ {{ children }}
5252
]{% endif %}},{% endwith %}{% endadminlist_recursetree %}
5353
null // for MSIE
@@ -65,9 +65,13 @@
6565
$div.append(contents);
6666
}
6767

68-
function onCanMoveTo(moved_node, target_node, position)
69-
{
70-
return ( target_node.can_have_children || position != 'inside' );
68+
function onCanMoveTo(moved_node, target_node, position) {
69+
var can_have_children = target_node.can_have_children;
70+
var child_types = target_node.child_types;
71+
if (can_have_children && child_types.length > 0){
72+
can_have_children = jQuery.inArray(moved_node.ct, child_types) > -1;
73+
}
74+
return ( can_have_children || position != 'inside' );
7175
}
7276

7377
var tree = jQuery("#js-result-list").tree({

0 commit comments

Comments
 (0)