Movatterモバイル変換


[0]ホーム

URL:


Skip to content

Navigation Menu

Sign in
Appearance settings

Search code, repositories, users, issues, pull requests...

Provide feedback

We read every piece of feedback, and take your input very seriously.

Saved searches

Use saved searches to filter your results more quickly

Sign up
Appearance settings

Commitc1348cf

Browse files
committed
Add SimplePathRouter
1 parent90eaf51 commitc1348cf

File tree

3 files changed

+265
-47
lines changed

3 files changed

+265
-47
lines changed

‎docs/api-guide/routers.md‎

Lines changed: 8 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -173,6 +173,12 @@ The router will match lookup values containing any characters except slashes and
173173
lookup_field = 'my_model_id'
174174
lookup_value_regex = '[0-9a-f]{32}'
175175

176+
##SimplePathRouter
177+
178+
This router is similar to`SimpleRouter` as above, but instead of_regexs_ it uses[path converters][path-convertes-topic-reference] to build urls.
179+
180+
**Note**: this router is available only with Django 2.x or above, since this feature was introduced in 2.0. See[release note][simplified-routing-release-note]
181+
176182
##DefaultRouter
177183

178184
This router is similar to`SimpleRouter` as above, but additionally includes a default API root view, that returns a response containing hyperlinks to all the list views. It also generates routes for optional`.json` style format suffixes.
@@ -340,3 +346,5 @@ The [`DRF-extensions` package][drf-extensions] provides [routers][drf-extensions
340346
[drf-extensions-customizable-endpoint-names]:https://chibisov.github.io/drf-extensions/docs/#controller-endpoint-name
341347
[url-namespace-docs]:https://docs.djangoproject.com/en/1.11/topics/http/urls/#url-namespaces
342348
[include-api-reference]:https://docs.djangoproject.com/en/2.0/ref/urls/#include
349+
[simplified-routing-release-note]:https://docs.djangoproject.com/en/2.0/releases/2.0/#simplified-url-routing-syntax
350+
[path-convertes-topic-reference]:https://docs.djangoproject.com/en/2.0/topics/http/urls/#path-converters

‎rest_framework/routers.py‎

Lines changed: 167 additions & 44 deletions
Original file line numberDiff line numberDiff line change
@@ -21,6 +21,7 @@
2121
fromdjango.urlsimportNoReverseMatch
2222

2323
fromrest_frameworkimportviews
24+
fromrest_framework.compatimportpath
2425
fromrest_framework.responseimportResponse
2526
fromrest_framework.reverseimportreverse
2627
fromrest_framework.schemasimportSchemaGenerator
@@ -79,50 +80,10 @@ def urls(self):
7980
returnself._urls
8081

8182

82-
classSimpleRouter(BaseRouter):
83-
84-
routes= [
85-
# List route.
86-
Route(
87-
url=r'^{prefix}{trailing_slash}$',
88-
mapping={
89-
'get':'list',
90-
'post':'create'
91-
},
92-
name='{basename}-list',
93-
detail=False,
94-
initkwargs={'suffix':'List'}
95-
),
96-
# Dynamically generated list routes. Generated using
97-
# @action(detail=False) decorator on methods of the viewset.
98-
DynamicRoute(
99-
url=r'^{prefix}/{url_path}{trailing_slash}$',
100-
name='{basename}-{url_name}',
101-
detail=False,
102-
initkwargs={}
103-
),
104-
# Detail route.
105-
Route(
106-
url=r'^{prefix}/{lookup}{trailing_slash}$',
107-
mapping={
108-
'get':'retrieve',
109-
'put':'update',
110-
'patch':'partial_update',
111-
'delete':'destroy'
112-
},
113-
name='{basename}-detail',
114-
detail=True,
115-
initkwargs={'suffix':'Instance'}
116-
),
117-
# Dynamically generated detail routes. Generated using
118-
# @action(detail=True) decorator on methods of the viewset.
119-
DynamicRoute(
120-
url=r'^{prefix}/{lookup}/{url_path}{trailing_slash}$',
121-
name='{basename}-{url_name}',
122-
detail=True,
123-
initkwargs={}
124-
),
125-
]
83+
classAbstractSimpleRouter(BaseRouter):
84+
"""
85+
Base class for SimpleRouter and SimplePathRouter.
86+
"""
12687

12788
def__init__(self,trailing_slash=True):
12889
self.trailing_slash='/'iftrailing_slashelse''
@@ -203,6 +164,52 @@ def get_method_map(self, viewset, method_map):
203164
bound_methods[method]=action
204165
returnbound_methods
205166

167+
168+
classSimpleRouter(AbstractSimpleRouter):
169+
170+
routes= [
171+
# List route.
172+
Route(
173+
url=r'^{prefix}{trailing_slash}$',
174+
mapping={
175+
'get':'list',
176+
'post':'create'
177+
},
178+
name='{basename}-list',
179+
detail=False,
180+
initkwargs={'suffix':'List'}
181+
),
182+
# Dynamically generated list routes. Generated using
183+
# @action(detail=False) decorator on methods of the viewset.
184+
DynamicRoute(
185+
url=r'^{prefix}/{url_path}{trailing_slash}$',
186+
name='{basename}-{url_name}',
187+
detail=False,
188+
initkwargs={}
189+
),
190+
# Detail route.
191+
Route(
192+
url=r'^{prefix}/{lookup}{trailing_slash}$',
193+
mapping={
194+
'get':'retrieve',
195+
'put':'update',
196+
'patch':'partial_update',
197+
'delete':'destroy'
198+
},
199+
name='{basename}-detail',
200+
detail=True,
201+
initkwargs={'suffix':'Instance'}
202+
),
203+
# Dynamically generated detail routes. Generated using
204+
# @action(detail=True) decorator on methods of the viewset.
205+
DynamicRoute(
206+
url=r'^{prefix}/{lookup}/{url_path}{trailing_slash}$',
207+
name='{basename}-{url_name}',
208+
detail=True,
209+
initkwargs={}
210+
),
211+
]
212+
206213
defget_lookup_regex(self,viewset,lookup_prefix=''):
207214
"""
208215
Given a viewset, return the portion of URL regex that is used
@@ -270,6 +277,122 @@ def get_urls(self):
270277
returnret
271278

272279

280+
classSimplePathRouter(AbstractSimpleRouter):
281+
"""
282+
Router which uses Django 2.x path to build urls
283+
"""
284+
285+
routes= [
286+
# List route.
287+
Route(
288+
url='{prefix}{trailing_slash}',
289+
mapping={
290+
'get':'list',
291+
'post':'create'
292+
},
293+
name='{basename}-list',
294+
detail=False,
295+
initkwargs={'suffix':'List'}
296+
),
297+
# Dynamically generated list routes. Generated using
298+
# @action(detail=False) decorator on methods of the viewset.
299+
DynamicRoute(
300+
url='{prefix}/{url_path}{trailing_slash}',
301+
name='{basename}-{url_name}',
302+
detail=False,
303+
initkwargs={}
304+
),
305+
# Detail route.
306+
Route(
307+
url='{prefix}/{lookup}{trailing_slash}',
308+
mapping={
309+
'get':'retrieve',
310+
'put':'update',
311+
'patch':'partial_update',
312+
'delete':'destroy'
313+
},
314+
name='{basename}-detail',
315+
detail=True,
316+
initkwargs={'suffix':'Instance'}
317+
),
318+
# Dynamically generated detail routes. Generated using
319+
# @action(detail=True) decorator on methods of the viewset.
320+
DynamicRoute(
321+
url='{prefix}/{lookup}/{url_path}{trailing_slash}',
322+
name='{basename}-{url_name}',
323+
detail=True,
324+
initkwargs={}
325+
),
326+
]
327+
328+
defget_lookup_path(self,viewset,lookup_prefix=''):
329+
"""
330+
Given a viewset, return the portion of URL path that is used
331+
to match against a single instance.
332+
333+
Note that lookup_prefix is not used directly inside REST rest_framework
334+
itself, but is required in order to nicely support nested router
335+
implementations, such as drf-nested-routers.
336+
337+
https://github.com/alanjds/drf-nested-routers
338+
"""
339+
base_converter='<{lookup_converter}:{lookup_prefix}{lookup_url_kwarg}>'
340+
# Use `pk` as default field, unset set. Default regex should not
341+
# consume `.json` style suffixes and should break at '/' boundaries.
342+
lookup_field=getattr(viewset,'lookup_field','pk')
343+
lookup_url_kwarg=getattr(viewset,'lookup_url_kwarg',None)orlookup_field
344+
lookup_converter=getattr(viewset,'lookup_converter','path')
345+
returnbase_converter.format(
346+
lookup_prefix=lookup_prefix,
347+
lookup_url_kwarg=lookup_url_kwarg,
348+
lookup_converter=lookup_converter
349+
)
350+
351+
defget_urls(self):
352+
"""
353+
Use the registered viewsets to generate a list of URL patterns.
354+
"""
355+
assertpathisnotNone,'SimplePathRouter requires Django 2.x path'
356+
ret= []
357+
358+
forprefix,viewset,basenameinself.registry:
359+
lookup=self.get_lookup_path(viewset)
360+
routes=self.get_routes(viewset)
361+
362+
forrouteinroutes:
363+
364+
# Only actions which actually exist on the viewset will be bound
365+
mapping=self.get_method_map(viewset,route.mapping)
366+
ifnotmapping:
367+
continue
368+
369+
# Build the url pattern
370+
url_path=route.url.format(
371+
prefix=prefix,
372+
lookup=lookup,
373+
trailing_slash=self.trailing_slash
374+
)
375+
376+
# If there is no prefix, the first part of the url is probably
377+
# controlled by project's urls.py and the router is in an app,
378+
# so a slash in the beginning will (A) cause Django to give
379+
# warnings and (B) generate URLS that will require using '//'.
380+
ifnotprefixandurl_path[0]=='/':
381+
url_path=url_path[1:]
382+
383+
initkwargs=route.initkwargs.copy()
384+
initkwargs.update({
385+
'basename':basename,
386+
'detail':route.detail,
387+
})
388+
389+
view=viewset.as_view(mapping,**initkwargs)
390+
name=route.name.format(basename=basename)
391+
ret.append(path(url_path,view,name=name))
392+
393+
returnret
394+
395+
273396
classAPIRootView(views.APIView):
274397
"""
275398
The default basic root view for DefaultRouter

‎tests/test_routers.py‎

Lines changed: 90 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,6 @@
11
fromcollectionsimportnamedtuple
22

3+
importdjango
34
importpytest
45
fromdjango.conf.urlsimportinclude,url
56
fromdjango.core.exceptionsimportImproperlyConfigured
@@ -8,11 +9,15 @@
89
fromdjango.urlsimportresolve,reverse
910

1011
fromrest_frameworkimportpermissions,serializers,viewsets
11-
fromrest_framework.compatimportget_regex_pattern
12+
fromrest_framework.compatimportget_regex_pattern,path
1213
fromrest_framework.decoratorsimportaction
1314
fromrest_framework.responseimportResponse
14-
fromrest_framework.routersimportDefaultRouter,SimpleRouter
15-
fromrest_framework.testimportAPIRequestFactory,URLPatternsTestCase
15+
fromrest_framework.routersimport (
16+
DefaultRouter,SimplePathRouter,SimpleRouter
17+
)
18+
fromrest_framework.testimport (
19+
APIClient,APIRequestFactory,URLPatternsTestCase
20+
)
1621
fromrest_framework.utilsimportjson
1722

1823
factory=APIRequestFactory()
@@ -77,9 +82,25 @@ def regex_url_path_detail(self, request, *args, **kwargs):
7782
returnResponse({'pk':pk,'kwarg':kwarg})
7883

7984

85+
classUrlPathViewSet(viewsets.ViewSet):
86+
@action(detail=False,url_path='list/<int:kwarg>')
87+
defurl_path_list(self,request,*args,**kwargs):
88+
kwarg=self.kwargs.get('kwarg','')
89+
returnResponse({'kwarg':kwarg})
90+
91+
@action(detail=True,url_path='detail/<int:kwarg>')
92+
defurl_path_detail(self,request,*args,**kwargs):
93+
pk=self.kwargs.get('pk','')
94+
kwarg=self.kwargs.get('kwarg','')
95+
returnResponse({'pk':pk,'kwarg':kwarg})
96+
97+
8098
notes_router=SimpleRouter()
8199
notes_router.register(r'notes',NoteViewSet)
82100

101+
notes_path_router=SimplePathRouter()
102+
notes_path_router.register('notes',NoteViewSet)
103+
83104
kwarged_notes_router=SimpleRouter()
84105
kwarged_notes_router.register(r'notes',KWargedNoteViewSet)
85106

@@ -92,6 +113,9 @@ def regex_url_path_detail(self, request, *args, **kwargs):
92113
regex_url_path_router=SimpleRouter()
93114
regex_url_path_router.register(r'',RegexUrlPathViewSet,basename='regex')
94115

116+
url_path_router=SimplePathRouter()
117+
url_path_router.register('',UrlPathViewSet,basename='path')
118+
95119

96120
classBasicViewSet(viewsets.ViewSet):
97121
deflist(self,request,*args,**kwargs):
@@ -463,6 +487,69 @@ def test_regex_url_path_detail(self):
463487
assertjson.loads(response.content.decode())== {'pk':pk,'kwarg':kwarg}
464488

465489

490+
@pytest.mark.skipif(django.VERSION< (2,0),reason='Django version < 2.0')
491+
classTestUrlPath(URLPatternsTestCase,TestCase):
492+
client_class=APIClient
493+
urlpatterns= [
494+
path('path/',include(url_path_router.urls)),
495+
path('example/',include(notes_path_router.urls))
496+
]ifpathelse []
497+
498+
defsetUp(self):
499+
RouterTestModel.objects.create(uuid='123',text='foo bar')
500+
RouterTestModel.objects.create(uuid='a b',text='baz qux')
501+
502+
deftest_create(self):
503+
new_note= {
504+
'uuid':'foo',
505+
'text':'example'
506+
}
507+
response=self.client.post('/example/notes/',data=new_note)
508+
assertresponse.status_code==201
509+
assertresponse['location']=='http://testserver/example/notes/foo/'
510+
assertresponse.data== {"url":"http://testserver/example/notes/foo/","uuid":"foo","text":"example"}
511+
assertRouterTestModel.objects.filter(uuid='foo').first()isnotNone
512+
513+
deftest_retrieve(self):
514+
response=self.client.get('/example/notes/123/')
515+
assertresponse.status_code==200
516+
assertresponse.data== {"url":"http://testserver/example/notes/123/","uuid":"123","text":"foo bar"}
517+
518+
deftest_list(self):
519+
response=self.client.get('/example/notes/')
520+
assertresponse.status_code==200
521+
assertresponse.data== [
522+
{"url":"http://testserver/example/notes/123/","uuid":"123","text":"foo bar"},
523+
{"url":"http://testserver/example/notes/a%20b/","uuid":"a b","text":"baz qux"},
524+
]
525+
526+
deftest_update(self):
527+
updated_note= {
528+
'text':'foo bar example'
529+
}
530+
response=self.client.patch('/example/notes/123/',data=updated_note)
531+
assertresponse.status_code==200
532+
assertresponse.data== {"url":"http://testserver/example/notes/123/","uuid":"123","text":"foo bar example"}
533+
534+
deftest_delete(self):
535+
response=self.client.delete('/example/notes/123/')
536+
assertresponse.status_code==204
537+
assertRouterTestModel.objects.filter(uuid='123').first()isNone
538+
539+
deftest_list_extra_action(self):
540+
kwarg=1234
541+
response=self.client.get('/path/list/{}/'.format(kwarg))
542+
assertresponse.status_code==200
543+
assertjson.loads(response.content.decode())== {'kwarg':kwarg}
544+
545+
deftest_detail_extra_action(self):
546+
pk='1'
547+
kwarg=1234
548+
response=self.client.get('/path/{}/detail/{}/'.format(pk,kwarg))
549+
assertresponse.status_code==200
550+
assertjson.loads(response.content.decode())== {'pk':pk,'kwarg':kwarg}
551+
552+
466553
classTestViewInitkwargs(URLPatternsTestCase,TestCase):
467554
urlpatterns= [
468555
url(r'^example/',include(notes_router.urls)),

0 commit comments

Comments
 (0)

[8]ページ先頭

©2009-2025 Movatter.jp