Skip to content

Commit 4d29d87

Browse files
author
Mirek Simek
committed
version 0.7.1: support for creating foreign key linked objects
1 parent a978ac9 commit 4d29d87

File tree

17 files changed

+205
-22
lines changed

17 files changed

+205
-22
lines changed

angular/package-dist.json

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,6 @@
11
{
22
"name": "django-angular-dynamic-forms",
3-
"version": "0.6.12",
3+
"version": "0.7.1",
44
"license": "MIT",
55
"description": "Django Rest Framework meets Angular 5 material.io dynamic forms - automatic create and edit dialogs",
66
"typings": "./django-angular-dynamic-forms.d.ts",

angular/package.json

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,6 @@
11
{
22
"name": "django-angular-dynamic-forms",
3-
"version": "0.6.12",
3+
"version": "0.7.1",
44
"license": "MIT",
55
"description": "Django Rest Framework meets Angular 5 material.io dynamic forms - rapid development of create and edit dialogs",
66
"typings": "./django-angular-dynamic-forms.d.ts",

angular_dynamic_forms/__init__.py

Lines changed: 4 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,12 +1,15 @@
11
from .autocomplete import AutoCompleteMixin, autocomplete
22
from .rest import AngularFormMixin
33
from .foreign_key import foreign_field_autocomplete, ForeignFieldAutoCompleteMixin, M2MEnabledMetadata
4+
from .linked_form import linked_form, linked_forms
45

56
__all__ = [
67
AutoCompleteMixin,
78
AngularFormMixin,
89
autocomplete,
910
foreign_field_autocomplete,
1011
ForeignFieldAutoCompleteMixin,
11-
M2MEnabledMetadata
12+
M2MEnabledMetadata,
13+
linked_form,
14+
linked_forms
1215
]
Lines changed: 60 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,60 @@
1+
from rest_framework.decorators import detail_route
2+
from rest_framework.relations import PrimaryKeyRelatedField
3+
4+
5+
def linked_form(viewset, form_id=None, link=None, method='create'):
6+
"""
7+
When having foreign key or m2m relationships between models A and B (B has foreign key to A named parent),
8+
we want to have a form that sits on A's viewset but creates/edits B and sets it relationship to A
9+
automatically.
10+
11+
In order to do so, define linked_forms on A's viewset containing a call to linked_form as follows:
12+
13+
@linked_forms()
14+
class AViewSet(AngularFormMixin, ...):
15+
linked_forms = {
16+
'new-b': linked_form(BViewSet, link='parent')
17+
}
18+
19+
Then, there will be a form definition on <aviewset>/pk/forms/new-b, with POST/PATCH operations pointing
20+
to an automatically created endpoint <aviewset>/pk/linked-endpoint/new-b and detail-route named "new_b"
21+
22+
:param viewset: the foreign viewset
23+
:param form_id: id of the form on the foreign viewset. If unset, use the default form
24+
:param link: either a field name on the foreign viewset or a callable that will get (foreign_instance, this_instance)
25+
:return: an internal definition of a linked form
26+
"""
27+
return {
28+
'viewset' : viewset,
29+
'form_id' : form_id,
30+
'link' : link,
31+
'method' : method
32+
}
33+
34+
35+
def linked_forms():
36+
def build_form(clz, form_name, form_def):
37+
def form_method(self, request, pk, *args, **kwargs):
38+
viewset = form_def['viewset']()
39+
viewset.request = request
40+
viewset.format_kwarg = self.format_kwarg
41+
link = form_def['link']
42+
if isinstance(link, str):
43+
serializer = viewset.get_serializer()
44+
fld = serializer.fields[link]
45+
if isinstance(fld, PrimaryKeyRelatedField):
46+
request.data[link] = self.get_object().pk
47+
else:
48+
request.data[link] = self.get_object()
49+
return getattr(viewset, form_def['method'])(request, *args, **kwargs)
50+
51+
setattr(clz, form_name.replace('-', '_'),
52+
detail_route(methods=['get', 'post', 'patch'], url_path=form_name)(form_method))
53+
54+
def wrapper(clz):
55+
forms = getattr(clz, 'linked_forms', {})
56+
if forms:
57+
for form_name, form_def in forms.items():
58+
build_form(clz, form_name, form_def)
59+
return clz
60+
return wrapper

angular_dynamic_forms/rest.py

Lines changed: 34 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -1,9 +1,10 @@
11
# noinspection PyUnresolvedReferences
22
import inspect
3+
import re
34

45
from django.core.exceptions import FieldDoesNotExist
56
from django.db.models import TextField
6-
from django.http import HttpResponseNotFound
7+
from django.http import HttpResponseNotFound, Http404
78
from django.utils.translation import gettext
89
from rest_framework import renderers
910
from rest_framework.decorators import detail_route, list_route
@@ -60,6 +61,12 @@ class AngularFormMixin(object):
6061
form_titles = {}
6162
form_defaults_map = {}
6263

64+
"""
65+
A map of linked forms, i.e. forms defined on other viewsets linked by foreign key or m2m. See @linked_forms
66+
decorator and linked_form(...) call for details.
67+
"""
68+
linked_forms = {}
69+
6370
@staticmethod
6471
def fieldset(title, controls):
6572
"""
@@ -104,22 +111,22 @@ def group(*controls):
104111
# noinspection PyUnusedLocal
105112
@detail_route(renderer_classes=[renderers.JSONRenderer], url_path='form')
106113
def form(self, request, *args, **kwargs):
107-
return self._get_form_metadata(has_instance=True)
114+
return Response(self._get_form_metadata(has_instance=True))
108115

109116
# noinspection PyUnusedLocal
110117
@list_route(renderer_classes=[renderers.JSONRenderer], url_path='form')
111118
def form_list(self, request, *args, **kwargs):
112-
return self._get_form_metadata(has_instance=False)
119+
return Response(self._get_form_metadata(has_instance=False))
113120

114121
# noinspection PyUnusedLocal
115122
@detail_route(renderer_classes=[renderers.JSONRenderer], url_path='form/(?P<form_name>.+)')
116123
def form_with_name(self, request, *args, form_name=None, **kwargs):
117-
return self._get_form_metadata(has_instance=True, form_name =form_name or '')
124+
return Response(self._get_form_metadata(has_instance=True, form_name =form_name or ''))
118125

119126
# noinspection PyUnusedLocal
120127
@list_route(renderer_classes=[renderers.JSONRenderer], url_path='form/(?P<form_name>.+)')
121128
def form_list_with_name(self, request, *args, form_name=None, **kwargs):
122-
return self._get_form_metadata(has_instance=False, form_name =form_name or '')
129+
return Response(self._get_form_metadata(has_instance=False, form_name =form_name or ''))
123130

124131
#
125132
# the rest of the methods on this class are private ones
@@ -272,12 +279,16 @@ def _get_actions(self, has_instance, serializer):
272279
def _get_form_metadata(self, has_instance, form_name=''):
273280

274281
if form_name:
282+
283+
if form_name in self.linked_forms:
284+
return self._linked_form_metadata(form_name)
285+
275286
if not self.form_layouts:
276-
return HttpResponseNotFound('Form layouts not configured. '
287+
raise Http404('Form layouts not configured. '
277288
'Please add form_layouts attribute on the viewset class')
278289

279290
if form_name not in self.form_layouts:
280-
return HttpResponseNotFound('Form with name %s not found' % form_name)
291+
raise Http404('Form with name %s not found' % form_name)
281292

282293
ret = {}
283294

@@ -301,8 +312,21 @@ def _get_form_metadata(self, has_instance, form_name=''):
301312
ret['hasInitialData'] = has_instance
302313

303314
# print(json.dumps(ret, indent=4))
315+
return ret
304316

305-
return Response(ret)
317+
def _linked_form_metadata(self, form_name):
318+
form_def = self.linked_forms[form_name]
319+
viewset = form_def['viewset']()
320+
viewset.request = self.request
321+
viewset.format_kwarg = self.format_kwarg
322+
ret = viewset._get_form_metadata(False, form_name=form_def['form_id'])
323+
324+
path = self.request.path
325+
# must be called from /form/ ...
326+
path = re.sub(r'/form(/[^/]+)?/?$', '', path)
327+
328+
ret['djangoUrl'] = '%s/%s/' % (path, form_name)
329+
return ret
306330

307331
# @LoggerDecorator.log()
308332
def _decorate_layout(self, layout, fields_info):
@@ -340,3 +364,5 @@ def _decorate_layout_item(self, item):
340364
def camel(snake_str):
341365
first, *others = snake_str.split('_')
342366
return ''.join([first.lower(), *map(str.title, others)])
367+
368+

demo/angular/package.json

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,6 @@
11
{
22
"name": "django-angular-dynamic-forms-demo",
3-
"version": "0.6.12",
3+
"version": "0.7.1",
44
"license": "MIT",
55
"scripts": {
66
"ng": "ng",

demo/angular/src/app/app-routing.module.ts

Lines changed: 6 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -13,6 +13,7 @@ import {CreateViaDialogMultipleFormsComponent} from './create-via-dialog-multipl
1313
import {CreateInPageMultipleFormsComponent} from './create-in-page-multiple-forms/create-in-page-multiple-forms.component';
1414
import {AllControlsComponent} from './all-controls/all-controls.component';
1515
import {ForeignComponent} from './foreign/foreign.component';
16+
import {CreateForeignComponent} from './create-foreign/create-foreign.component';
1617

1718
const routes: Routes = [
1819
{
@@ -65,6 +66,11 @@ const routes: Routes = [
6566
pathMatch: 'full',
6667
component: ForeignComponent
6768
},
69+
{
70+
path: 'create-foreign',
71+
pathMatch: 'full',
72+
component: CreateForeignComponent
73+
},
6874
{
6975
path: 'edit-in-page',
7076
pathMatch: 'prefix',

demo/angular/src/app/app.component.ts

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -57,6 +57,7 @@ import {environment} from '../environments/environment';
5757
</mat-expansion-panel-header>
5858
<a routerLink="/foreign">Foreign Key</a>
5959
<a routerLink="/all-controls">m2m in All available controls and layout demo</a>
60+
<a routerLink="/create-foreign">Create a new instance of referenced object</a>
6061
</mat-expansion-panel>
6162
<mat-expansion-panel>
6263
<mat-expansion-panel-header>

demo/angular/src/app/app.module.ts

Lines changed: 4 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -43,6 +43,7 @@ import {FormsModule, ReactiveFormsModule} from '@angular/forms';
4343
import {ForeignSelectorFactoryService} from './all-controls/foreign-selector-factory.service';
4444
import {ForeignSelectorComponent} from './all-controls/foreign-selector.component';
4545
import {TagSelectorComponent} from './all-controls/tag-selector.component';
46+
import { CreateForeignComponent } from './create-foreign/create-foreign.component';
4647

4748

4849
@Injectable()
@@ -76,7 +77,8 @@ export class SimpleForeignFieldFormatter implements ForeignFieldFormatter {
7677
ForeignComponent,
7778
SampleForeignSelectorComponent,
7879
ForeignSelectorComponent,
79-
TagSelectorComponent
80+
TagSelectorComponent,
81+
CreateForeignComponent
8082
],
8183
imports: [
8284
BrowserAnimationsModule,
@@ -101,7 +103,7 @@ export class SimpleForeignFieldFormatter implements ForeignFieldFormatter {
101103
MatSortModule,
102104
MatSliderModule,
103105
MatSlideToggleModule,
104-
FormsModule
106+
FormsModule,
105107
],
106108
providers: [
107109
{

demo/angular/src/app/edit-via-dialog/edit-via-dialog.component.ts

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -161,7 +161,7 @@ urlpatterns = [
161161
.catch(err => this.errors.showCommunicationError(err))
162162
.subscribe(resp => {
163163
this.data = new MatTableDataSource(resp);
164-
})
164+
});
165165
}
166166

167167
edit(id: string) {

0 commit comments

Comments
 (0)