------------------------------- ------------------ Django -------------------- | Browser: GET /udo/contact/2 | === wsgi/fcgi ===> | 1. Asks OS for DJANGO_SETTINGS_MODULE | ------------------------------- | 2. Build Request (from wsgi/fcgi callback) | | 3. Get settings.ROOT_URLCONF module | | 4. Resolve URL/view from request.path | # url(r'^udo/contact/(?P<id>\w+)', view, name='url-identifier') | 5. Apply request middlewares | # settings.MIDDLEWARE_CLASSES | 6. Run VIEW | # def view(request, id): --------------------------------------------------------- ------------------------------------------------------------------------------------- # Clasic views (<= django 1.2) from django import http def view(request, id): if request.method == 'GET': return http.HttpResponse("some content") elif request.method == 'POST': return http.HttpResponseBadRequest("no way") # Class views (>= django 1.3) [urls.py] url(r'^udo/contact/(?P<id>\w+)', ClassView.as_view(), name='url-identifier') [views.py] from django.views.generic import View class ClassView(View): def get(request, id): return http.HttpResponse("some content") def post(request, id): return http.HttpResponseBadRequest("no way") --------------------------------------------------------- -------------------------------------------------------------------------------------- | 7. Run response middlewares | | | | | ---------------------------------------------- || || || VV -------------------------- | Browser: Read Response | --------------------------
If you understand the previous section, I focus now on step 6
[urls.py] url(r'^udo/contact/(?P<id>\w+)', Drf.as_view(), name='url-identifier') [views.py] from djangorestframework.views import View # I'm exposing some default attrs on DRF View (in pseudo code) class Drf(View): # parsers = (Json, form, multipart, xml) # renderers = (Json, jsonp, html, xhtml, plain, xml) # authentication = (Userlogged, BasicAuth) # permissions = (FullAnonAccess, ) def post(request, id): validated = self.CONTENT raw = self.DATA user_regarding_of_permissions = self.user # ... 6. Run view * Run Drf.as_view() CLASS function (django internal, because need a callable and we cant have an instance of Drf in urls) return a callable
** [Spanish: Aquí surge una pregunta, quien la adivine gana un caramelo (le invito a un café) :P | Hint: View]**
6.1 Run that callable that in fact run Drf.dispatch bound method || || VV ------ This is DRF behaviour ------------------------------------------------------------------------------------------- 6.2 Read permissions attr (list of permissions handlers with ``check_permission`` methods) 6.2.1 Get user 6.2.1.1 Read authentication attr (list of authenticators with ``authenticate`` methods) 6.2.1.2 Run through this authenticators IN ORDER to attempt to authenticate the request 6.2.1.3 Authenticators could return an User or None UserLogged => Read from request.session BasicAuth => Read from request.headers 6.2.2 Run through this permissions IN ORDER to check regarding of ``user`` obtained before 6.2.3 This permissions could pass (continue request) or raise some 403 response (by default) FullAnonAccess are as you can imagine a dummy. always pass djangorestframework.permissions.IsAuthenticated on the other hand check if user.is_authenticated() [OMG, rly? :P] 6.3 Set ``method`` from request 6.4 Set ``content_type`` from request (so important to parsers) 6.5 Run ``method`` method from view (GET => def get(request...) NOTE: If it can't obtain that method from view, raise a METHOD_NOT_ALLOWED response exception || || VV ------------------- Ok, now we are in our code... but with some magic -------------------------------------------- 6.5.1 Django raw request are in self.request 6.5.2 If flow pass permissions layer, and regarding of that permissions, we got User or AnonymousUser in self.user 6.5.3 self.DATA is the payload RAW (similar to request.POST) [Detail explained bellow] 6.5.4 self.FILES is the request files (similar to request.FILES). Only multipart requests for example [Detail explained bellow] 6.5.5 self.CONTENT is validated self.DATA (though django.forms) [Detail explained bellow] 6.5.6 self.PARAMS is validated self.request.GET (though django.forms) [Detail explained bellow]
I need to explain some concepts in order to understand the big picture
Full doc: https://docs.djangoproject.com/en/dev/topics/forms/
Explained as a black box: payload -> form -> payload_cleaned or raise Invalid
Easy example:
[forms.py] from django import forms class Contact(forms.Form): name = forms.CharField(required=True, max_length=50) email = forms.EmailField() language = forms.CharField(required=True) age = forms.IntegerField() def clean_language(self): language = self.cleaned_data['language'] if language.lower() != u'python': raise forms.ValidationError("Are you crazy? Python rocks!") return language [views.py] mock_payload = { 'name': 'copitux', 'email': '[email protected]', 'language': 'python', 'age': 22, } # Valid form = forms.Contact(mock_payload) assert form.is_valid() # Required fields del mock_payload['name'] form = forms.Contact(mock_payload) assert not form.is_valid() assert form.errors = [{'name': 'This field is required'}] # clean validation mock_payload.update({'language': 'javascript', 'name': 'copitux'}) form = forms.Contact(mock_payload) assert not form.is_valid() assert form.errors = [{'language': 'Are you crazy? Python rocks!'}]
- Each
Field
clean_<field>
methodsclean
Why if jQuery.ajax send payload as string we retrieve in our view.{post/put...} a python object? DRF.parsers!
Let examine a jQuery request
$.ajax({ url: 'udo/contacts/2', type: 'POST', data: '{"name": "copitux"}', // or $.toJson({name: "copitux"}) dataType: 'json', contentType: 'application/json' });
With that code it build the request with some headers different to browser. To this context there are two important headers
#Ajax_request.send => Django request flow / Build Request assert request['HTTP_ACCEPT'] in 'application/json, text/javascript, */*; q=0.01' assert request['CONTENT_TYPE'] in 'application/json' # It maybe come as HTTP_CONTENT_TYPE # It maybe differs, but I only want the concept ## Extra info! (for free :P) Also jQuery send this header ('HTTP_X_REQUESTED_WITH': 'XMLHttpRequest') and with that ``request.is_ajax()`` works
Only CONTENT_TYPE is important for parsers [Spanish: Caramelol!! - Para que puede ser importante HTTP_ACCEPT? | Hint: See attrs of Drf class View]
Did you remember this line? parsers = (Json, xml, form ...)
If fact it's a tuple of drf.parsers: parsers = (djangorestframework.parsers.JSONParser, ...)
Each parser has a content_type
associated, so DRF'll try to parse with each parser until one of them has the content_type
sended. In our example, it's obvious: JSONParser
will parse the payload
But, when?
NOTE: self.DATA, self.FILES and so on are cached properties
When we call self.DATA
the first time in that request so
Build request => Run view (wrapped by DRF permissions behaviour) => access self.DATA in view => DRF see the content_type => DRF identify the parser (JSONparser in the example) => and run it [DRF.JSONparser internal - psecudo code] def parse(raw_payload): return json.load(raw_payload)
In fact, when we call self.DATA; self.FILES will be cached too but it's beyond the scope of this tutorial
Yes, sorry. Another time I need to explain one new concept before glue all
We could set a resource
attribute which runs the validation and prepare response if all go fine or not
Also we could set a <method>_form
form attribute to handle that validation.
DRF has a lot of magic here, it's beyond the scope of this tutorial explain that so I explain the most used method with REST (IMHO)
class Drf(View): # Avoid another attrs yet described post_form = forms.NewContact get_form = forms.Contact # For anybody interested in another ways class Contact(FormResource): post_form = forms.NewContact get_form = forms.Contact class Drf(View): resource = Contact
The validation process begin when we access to self.CONTENT
the first time in the request
class Drf(View): # ... post_form = forms.Contact def post(self, request): payload_validated = self.CONTENT This occurs internally [psecudo code]: form = forms.Contact(self.DATA) if form.is_valid(): return form.cleaned_data else: raise ErrorResponse(status_code=BADREQUEST_400, content=friendly(form.errors))
If we define a protocol between Backend and Frontend to only send JSON dicts
payload = { 'key1': 'value', 'key2': 'value2', # ... }
We could handle the validation cleanly and encapsulate our API rules in forms (per resource, per API. Whatever we want)
Advantages: All
- Clean code
- Smaller views
- Handle validation errors cleanly
- Decouple validation from another tasks => Change algorithms more easily
- Django forms are 100% flexible in an easy way (We can subclass forms, fields, inyect some specific validation to one specific field ...)
- Django rest framework Resources are also 70% flexible. (Por example; we can create a new Resource to handle JSON lists
['value1', 'value2']
or JSON list of dicts[{'key1': 'value1'}, ...]
Disadvantages: Only one but we shouldn't exploit it. If we define a workaround of REST stantard... we could have troubles (and hack DRF validation flow). Corollary: Follow rest standards (if all frameworks in the world; backend and frontend follow that... why we do not?