Skip to content

Commit ec885e3

Browse files
authored
Merge pull request #57 from Saritasa/master
Fix parent field name different from 'parent'
2 parents 61debd4 + 06e26a0 commit ec885e3

File tree

9 files changed

+317
-25
lines changed

9 files changed

+317
-25
lines changed

.gitignore

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -13,3 +13,4 @@ dist/
1313
docs/_build/
1414
*.sqlite3
1515
.tox
16+
.python-version

.travis.yml

Lines changed: 6 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -4,8 +4,9 @@ python:
44
- '2.7'
55
- '3.3'
66
- '3.4'
7+
- '3.5'
8+
- '3.6'
79
env:
8-
- PACKAGES="django>=1.6,<1.7 django-mptt<0.8"
910
- PACKAGES="django>=1.7,<1.8 django-mptt<0.8"
1011
- PACKAGES="django>=1.8,<1.9"
1112
- PACKAGES="django>=1.9,<1.10"
@@ -16,6 +17,10 @@ matrix:
1617
env: PACKAGES="django>=1.9,<1.10"
1718
- python: '3.3'
1819
env: PACKAGES="django>=1.10,<1.11"
20+
- python: '3.5'
21+
env: PACKAGES="django>=1.7,<1.8 django-mptt<0.8"
22+
- python: '3.6'
23+
env: PACKAGES="django>=1.7,<1.8 django-mptt<0.8"
1924
before_install:
2025
- pip install codecov
2126
install:

README.rst

Lines changed: 48 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -172,22 +172,65 @@ The ``child_models`` attribute defines which admin interface is loaded for the *
172172
The list view is still rendered by the parent admin.
173173

174174

175+
Validation
176+
^^^^^^^^^^
177+
178+
When drag-n-drop nodes in tree in admin, ``clean``/``full_clean`` method is not called. To implement validation, you
179+
should override ``can_be_moved`` model method. Method have to return ``True`` if nothing happens and moving is allowing.
180+
Otherwise, ``can_be_moved`` have to raise ``ValidationError`` or ``InvalidMove`` from ``mptt.exceptions``
181+
182+
.. code:: python
183+
184+
class Participant(PolymorphicMPTTModel):
185+
conference = models.ForeignKey(Conference)
186+
invited_by = PolymorphicTreeForeignKey(
187+
'self',
188+
null=True,
189+
blank=True,
190+
related_name='invited',
191+
verbose_name=_('Invited')
192+
)
193+
194+
def clean(self):
195+
if self.participant:
196+
if self.conference != self.manager.conference:
197+
raise ValidationError({
198+
'invited_by': _(
199+
'Participants have to be from the same '
200+
'conference'
201+
)
202+
})
203+
204+
def can_be_moved(self, target):
205+
self.invited_by = target
206+
self.clean()
207+
return True
208+
175209
Tests
176210
-----
177211

178212
To run the included test suite, execute::
179213

180214
./runtests.py
181215

182-
To test support for multiple Python and Django versions, run tox from the repository root::
216+
To test support for multiple Python and Django versions, you need to follow steps below:
183217

184-
pip install tox
185-
tox
218+
* install project requirements in virtual environment
219+
* install python 2.7, 3.3, 3.4, 3.5, 3.6 python versions through pyenv (See pyenv (Linux) or Homebrew (Mac OS X).)
220+
* create .python-version file and add full list of installed versions with which project have to be tested, example::
186221

187-
The Python versions need to be installed at your system. See pyenv (Linux) or Homebrew (Mac OS X).
222+
2.6.9
223+
2.7.13
224+
3.3.6
225+
3.4.5
226+
3.5.2
227+
3.6.0
228+
* run tox from the repository root::
188229

189-
Python 2.6, 2.7, and 3.3 are the currently supported versions.
230+
pip install tox
231+
tox
190232

233+
Python 2.7, 3.3, 3.4, 3.5 and 3.6 and django 1.7, 1.8, 1.9 and 1.10 are the currently supported versions.
191234

192235
Todo
193236
----

polymorphic_tree/admin/parentadmin.py

Lines changed: 17 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,12 +1,14 @@
11
import json, django
22

3+
from django.core.exceptions import ValidationError
34
from future.builtins import str, int
45
from distutils.version import StrictVersion
56
from django.conf import settings
67
from django.core.urlresolvers import reverse
78
from django.db import transaction
89
from django.http import HttpResponseNotFound, HttpResponse, HttpResponseBadRequest, HttpResponseRedirect
910
from django.utils.translation import ugettext_lazy as _
11+
from mptt.exceptions import InvalidMove
1012
from polymorphic.admin import PolymorphicParentModelAdmin, PolymorphicModelChoiceForm
1113
from polymorphic_tree.models import PolymorphicMPTTModel
1214
from mptt.admin import MPTTModelAdmin
@@ -211,7 +213,7 @@ def api_node_moved_view(self, request):
211213
'moved_id': moved_id,
212214
'error': _(u'Cannot place \u2018{0}\u2019 below \u2018{1}\u2019; a {2} does not allow children!').format(moved, target, target._meta.verbose_name)
213215
}), content_type='application/json', status=409) # Conflict
214-
if moved.parent_id != previous_parent_id:
216+
if getattr(moved, '{}_id'.format(moved._mptt_meta.parent_attr)) != previous_parent_id:
215217
return HttpResponse(json.dumps({
216218
'action': 'reload',
217219
'error': 'Client seems to be out-of-sync, please reload!'
@@ -222,6 +224,20 @@ def api_node_moved_view(self, request):
222224
'before': 'left',
223225
'after': 'right',
224226
}[position]
227+
try:
228+
moved.can_be_moved(target)
229+
except ValidationError as e:
230+
return HttpResponse(json.dumps({
231+
'action': 'reject',
232+
'moved_id': moved_id,
233+
'error': '\n'.join(e.messages)
234+
}), content_type='application/json', status=400)
235+
except InvalidMove as e:
236+
return HttpResponse(json.dumps({
237+
'action': 'reject',
238+
'moved_id': moved_id,
239+
'error': str(e)
240+
}), content_type='application/json', status=400)
225241
moved.move_to(target, mptt_position)
226242

227243
# Some packages depend on calling .save() or post_save signal after updating a model.

polymorphic_tree/models.py

Lines changed: 14 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -111,6 +111,20 @@ def get_ancestors_of_type(self, model, ascending=False, include_self=False):
111111
"""
112112
return self.get_ancestors(ascending=ascending, include_self=include_self).instance_of(model)
113113

114+
def can_be_moved(self, target):
115+
"""Can move be finished
116+
117+
Method have to be redefined in inherited model to define cases when
118+
node can be moved. If method is not redefined moving always allows
119+
120+
To deny move, this method have to be raised ``ValidationError`` or
121+
``InvalidMove`` from ``mptt.exceptions``
122+
123+
Args:
124+
target (PolymorphicMPTTModel): future parent of node
125+
"""
126+
pass
127+
114128

115129
if django.VERSION < (1,7):
116130
# South integration

polymorphic_tree/tests/admin.py

Lines changed: 25 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,25 @@
1+
from polymorphic_tree.admin import (PolymorphicMPTTChildModelAdmin,
2+
PolymorphicMPTTParentModelAdmin)
3+
from polymorphic_tree.tests.models import ModelWithCustomParentName
4+
5+
6+
class BaseChildAdmin(PolymorphicMPTTChildModelAdmin):
7+
"""Test child admin"""
8+
GENERAL_FIELDSET = (None, {
9+
'fields': ('chief', 'field5'),
10+
})
11+
12+
base_model = ModelWithCustomParentName
13+
base_fieldsets = (
14+
GENERAL_FIELDSET,
15+
)
16+
17+
18+
class TreeNodeParentAdmin(PolymorphicMPTTParentModelAdmin):
19+
"""Test parent admin"""
20+
base_model = ModelWithCustomParentName
21+
child_models = (
22+
(ModelWithCustomParentName, BaseChildAdmin),
23+
)
24+
25+
list_display = ('field5', 'actions_column',)

polymorphic_tree/tests/models.py

Lines changed: 80 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,6 @@
1+
from django.core.exceptions import ValidationError
12
from django.db import models
3+
from mptt.exceptions import InvalidMove
24

35
from polymorphic.showfields import ShowFieldContent
46
from polymorphic_tree.models import PolymorphicMPTTModel, PolymorphicTreeForeignKey
@@ -46,3 +48,81 @@ class ModelX(Base):
4648

4749
class ModelY(Base):
4850
field_y = models.CharField(max_length=10)
51+
52+
53+
class ModelWithCustomParentName(PolymorphicMPTTModel):
54+
"""Model with custom parent name
55+
56+
A model where ``PolymorphicTreeForeignKey`` attribute has not ``parent``
57+
name, but ``chief``
58+
59+
Attributes:
60+
chief (ModelWithCustomParentName): parent
61+
field5 (str): test field
62+
"""
63+
chief = PolymorphicTreeForeignKey('self',
64+
blank=True,
65+
null=True,
66+
related_name='subordinate',
67+
verbose_name='Chief')
68+
field5 = models.CharField(max_length=10)
69+
70+
class MPTTMeta:
71+
parent_attr = 'chief'
72+
73+
def __str__(self):
74+
return self.field5
75+
76+
77+
class ModelWithValidation(PolymorphicMPTTModel):
78+
"""Model with custom validation
79+
80+
A model with redefined ``clean`` and ``can_be_moved`` methods
81+
82+
``clean`` method always raises ``ValidationError``
83+
``can_be_moved`` always calls ``clean``
84+
85+
Attributes:
86+
parent (ModelWithValidation): parent
87+
field6 (str): test field
88+
"""
89+
90+
parent = PolymorphicTreeForeignKey('self',
91+
blank=True,
92+
null=True,
93+
related_name='children')
94+
95+
field6 = models.CharField(max_length=10)
96+
97+
def clean(self):
98+
"""Raise validation error"""
99+
raise ValidationError({
100+
'parent': 'There is something with parent field'
101+
})
102+
103+
def can_be_moved(self, target):
104+
"""Execute ``clean``"""
105+
self.clean()
106+
107+
108+
class ModelWithInvalidMove(PolymorphicMPTTModel):
109+
"""Model with custom validation
110+
111+
A model with redefined only ``can_be_moved`` method which always raises
112+
``InvalidMove``
113+
114+
Attributes:
115+
parent (ModelWithValidation): parent
116+
field7 (str): test field
117+
"""
118+
119+
parent = PolymorphicTreeForeignKey('self',
120+
blank=True,
121+
null=True,
122+
related_name='children')
123+
124+
field7 = models.CharField(max_length=10)
125+
126+
def can_be_moved(self, target):
127+
"""Raise ``InvalidMove``"""
128+
raise InvalidMove('Invalid move')

0 commit comments

Comments
 (0)