原文链接:https://www.wordfugue.com/using-django-channels-email-sending-queue/
by phildini on April 8, 2016
Channels is a project by led Andrew Godwin to bring native asynchronous processing to Django. Most of the tutorials for integrating Channels into a Django project focus on Channels’ ability to let Django “speak WebSockets”, but Channels has enormous potential as an async task runner. Channels could replace Celery or RQ for most projects, and do so in a way that feels more native.
Channels 是一个由Andrew Godwin领导,给Django带来原生异步处理的项目。很多把Channels集成到Django项目的教程注重于Channels让Django说“Websockets”的能力,但是Channels有着巨大的做为异步任务管理器的潜力。Channels可以替换大多数项目中的Celery或者RQ,而且这样做是一种更为原生的做法。
http://www.machinalis.com/blog/oauth2-authentication/
OAuth2 authentication with Django REST Framework and custom third-party OAuth2 backends A complete example of how to implement OAuth2 authentication on a Django REST Framework with custom third-party OAuth2 backends
Posted by Agustín Bartó 11 months, 2 weeks ago Comments Introduction Using OAuth2 authentication on a Django REST Framework based Django site it’s actually fairly easy to implement thanks to django-oauth-tookit. Things can be trick if you also want to allow the users of the API using third-party OAuth2 authentication backends like Facebook, Twitter, or even your own OAuth2 backends.
Authenticating with social sites on regular Django sites is actually quite easy thanks to applications like django-allauth, but sadly it is not well suited for RESTful APIs authentication.
Not to long ago, Félix Descôteaux wrote an excellent blogpost entitled “A Rest API using Django and authentication with OAuth2 AND third parties!” that showed us how to use third-party OAuth2 backends on Django REST Framework site using using django-oauth-toolkit.
This blogpost provides a complete example of an implementation of what Félix Descôteaux suggests, expanding on the matter by explaining how to use our own OAuth2 provider.
The code The code for this blogpost is available on GitHub. A Vagrant configuration file is included if you want to test the service yourself.
Fake Social Site Fake Social Site is a simple Django project with OAuth2 authentication (using django-oauth-toolkit) that will play the role of the third-party authentication provider. We could have used Facebook too, but we wanted to give you the chance to play with a couple of custom use cases that we had to tackle at Machinalis.
Besides OAuth2 authentication, the site provides two endpoints that expose the user’s profile.
user_details/: It returns the profile information for the currently authenticated user.
user_details_by_username/(?P
$ curl –header “Content-Type: application/x-www-form-urlencoded” –header “Accept: application/json; indent=4” –request POST –data “username=admin&password=admin&client_id=ZQcMr611iZMcUskTGoRcyZuhqCjZYy08lyOsWM5d&grant_type=password” http://localhost:8005/o/token/; echo {“access_token”: “zf8x8YiP3nUPjnV8WWArve4c3tZIMN”, “token_type”: “Bearer”, “expires_in”: 36000, “refresh_token”: “fQF4BSp8nyFs72xobC2UzpeHHYmHYC”, “scope”: “read write”}
$ curl –head –header “Accept: application/json; indent=4” –request GET http://localhost:8005/user_details/; echo HTTP/1.0 401 UNAUTHORIZED Date: Fri, 03 Jul 2015 19:26:07 GMT Server: WSGIServer/0.1 Python/2.7.10 Vary: Accept X-Frame-Options: SAMEORIGIN Content-Type: application/json; indent=4 WWW-Authenticate: Bearer realm=”api” Allow: OPTIONS, GET
$ curl –header “Authorization: Bearer zf8x8YiP3nUPjnV8WWArve4c3tZIMN” –header “Accept: application/json; indent=4” –request GET http://localhost:8005/user_details/; echo { “id”: 1, “username”: “admin”, “first_name”: “Agustin”, “last_name”: “Barto”, “email”: “abarto@rest_oauth_social_test.com” }
$ curl –header “Authorization: Bearer zf8x8YiP3nUPjnV8WWArve4c3tZIMN” –header “Accept: application/json; indent=4” –request GET http://localhost:8005/user_details_by_username/admin/; echo { “id”: 1, “username”: “admin”, “first_name”: “Agustin”, “last_name”: “Barto”, “email”: “abarto@rest_oauth_social_test.com” }
$ curl –head –header “Authorization: Bearer zf8x8YiP3nUPjnV8WWArve4c3tZIMN” –header “Accept: application/json; indent=4” –request GET http://localhost:8005/user_details_by_username/foobar/; echo HTTP/1.0 404 NOT FOUND Date: Fri, 03 Jul 2015 19:25:23 GMT Server: WSGIServer/0.1 Python/2.7.10 Vary: Accept X-Frame-Options: SAMEORIGIN Content-Type: application/json; indent=4 Allow: OPTIONS, GET We created two OAuth2 applications within the site: One for itself, and another that represents the other Django project that’ll serve as our example API (My API):
[ { “model”: “oauth2_provider.application”, “pk”: 1, “fields”: { “skip_authorization”: true, “redirect_uris”: “”, “name”: “my-api-app”, “authorization_grant_type”: “password”, “client_type”: “public”, “client_id”: “ZQcMr611iZMcUskTGoRcyZuhqCjZYy08lyOsWM5d”, “client_secret”: “Ve0AVjI4G7JwPj0spAz4jvY0nNxGGfK9q6IJXqARRS3oobDY0sYxqepH0i1euXDLfcbWe8Dx27atNMyJvg3vRLssUBJd4otkoNgxD6jwje5l3ipJnwGpNy3QFq0EhB1g”, “user”: [ “admin” ] } }, { “model”: “oauth2_provider.application”, “pk”: 2, “fields”: { “skip_authorization”: true, “redirect_uris”: “”, “name”: “fake-social-site-app”, “authorization_grant_type”: “password”, “client_type”: “public”, “client_id”: “aNARymvEsn21XPdpR9wJ8tPcUyto7rCu1ywo6H1T”, “client_secret”: “6Q7nFgQ8UExu2XhaNbI56DQAWTQvnVp7D9l3S9Ps1kpju3fH6NSRPfXktF92gZWmG1q7974NkuJSTm7nahTKdmaKcsaevA2U0tRjE4oXD66bqQrxDjQR8B7AK5JOM9Ko”, “user”: [ “admin” ] } } ] My API The second part of the project is a Django site that exposes a simple API using Django REST Framework and uses django-oauth-toolkit for authentication.
We want to allow users of Fake Social Site access to My API, as well as My API’s own user. As mentioned in the introduction we follow the recipe described in Félix Descôteaux’s blogpost (as well as python-social-auth’s documentation on the matter). The only change we made was to allow supplying custom parameters to the authentication backend when registering the user for the first time.
We expose a Django view that takes an OAuth2 access_token from Fake Social Site and exchanges it for one of My API, creating a new user and its social user profile in the process:
my_api/users/views.py:
_ERROR_TOKEN_EXCHANGE_RESPONSE = JsonResponse( { “error”: “unsuccessful_token_exchange”, “error_description”: “Unable to complete token exchange with social backend.” }, status=401 )
@psa(‘social:complete’) def register_by_access_token(request, backend): token = request.GET.get(‘access_token’) username = request.GET.get(‘username’, None)
# We pass the parameters to the backend so it can make the appropriate requests to the third party site.
user = request.backend.do_auth(token, username=username)
if user:
try:
login(request, user)
except Exception:
return _ERROR_TOKEN_EXCHANGE_RESPONSE
else:
return get_access_token(user)
else:
return _ERROR_TOKEN_EXCHANGE_RESPONSE
my_api/users/tools.py:
def get_token_json(access_token): return JsonResponse({ ‘access_token’: access_token.token, ‘expires_in’: oauth2_settings.ACCESS_TOKEN_EXPIRE_SECONDS, ‘token_type’: ‘Bearer’, ‘refresh_token’: access_token.refresh_token.token, ‘scope’: access_token.scope });
def get_access_token(user): application = Application.objects.get(name=”my-api”)
try:
old_access_token = AccessToken.objects.get(user=user, application=application)
old_refresh_token = RefreshToken.objects.get(user=user, access_token=old_access_token)
except:
pass
else:
old_access_token.delete()
old_refresh_token.delete()
token = generate_token()
refresh_token = generate_token()
expires = now() + timedelta(seconds=oauth2_settings.ACCESS_TOKEN_EXPIRE_SECONDS)
scope = "read write"
access_token = AccessToken.objects.\
create(user=user,
application=application,
expires=expires,
token=token,
scope=scope)
RefreshToken.objects.\
create(user=user,
application=application,
token=refresh_token,
access_token=access_token)
return get_token_json(access_token) In the example of the blogpost, the author uses Facebook as the third party. In order to support Fake Social Site, we wrote an authentication backend based on python-social-backend’s BaseOAuth2:
class FakeSocialSiteOAuth2(BaseOAuth2): name = ‘fake_social_site’ SCOPE_SEPARATOR = ‘,’ EXTRA_DATA = [ (‘id’, ‘id’) ]
def access_token_url(self):
return settings.FAKE_SOCIAL_SITE_AUTH_AUTHORIZATION_URL
def authorization_url(self):
return settings.FAKE_SOCIAL_SITE_AUTH_ACCESS_TOKEN_URL
def get_user_details(self, response):
return {
'username': response.get('username'),
'email': response.get('email') or '',
'first_name': response.get('first_name'),
'last_name': response.get('last_name'),
}
def user_data(self, access_token, *args, **kwargs):
try:
return self.get_json(
settings.FAKE_SOCIAL_SITE_AUTH_USER_DETAILS_URL,
headers={'Authorization': 'Bearer {}'.format(access_token)}
)
except ValueError:
return None There’s not much to it as we leveraged most of BaseOAuth2’s functionality. We also wanted to allow for the use case when the third party site requires a parameters to look for the user’s profile info, so we created another authentication provider based on BaseOAuth2:
class FakeSocialSiteWithParamsOAuth2(BaseOAuth2): name = ‘fake_social_site_with_params’ SCOPE_SEPARATOR = ‘,’ EXTRA_DATA = [ (‘id’, ‘id’) ]
def access_token_url(self):
return settings.FAKE_SOCIAL_SITE_WITH_PARAM_AUTH_AUTHORIZATION_URL
def authorization_url(self):
return settings.FAKE_SOCIAL_SITE_WITH_PARAM_AUTH_ACCESS_TOKEN_URL
def get_user_details(self, response):
return {
'username': response.get('username'),
'email': response.get('email') or '',
'first_name': response.get('first_name'),
'last_name': response.get('last_name'),
}
def user_data(self, access_token, username=None, *args, **kwargs):
try:
return self.get_json(
settings.FAKE_SOCIAL_SITE_WITH_PARAM_AUTH_USER_DETAILS_URL.format(username=username),
headers={'Authorization': 'Bearer {}'.format(access_token)}
)
except ValueError:
return None All we had to do was add the named paratemeter (username in this case) to the user_data method, and use its value to make the request to the third party site. When the do_auth method is invoked in register_by_access_token we supply the parameter taken from the request, and it is passed to user_data when it is eventually invoked by python-social-auth’s authentication pipeline.
原文链接:http://www.machinalis.com/blog/introduction-to-django-channels/
Quick introduction to Django Channels A brief description of Django Channels and its application on a real-life Django site
Posted by Agustín Bartó 4 months, 3 weeks ago Comments Channels is an exciting upcoming feature of Django that will allow Django sites to support use cases that usually required the use of external tools and libraries (even non-Python ones) and even has the potential to change the way we work with the framework entirely.
According to its documentation, Django Channels is…
…a project to make Django able to handle more than just plain HTTP requests, including WebSockets and HTTP2, as well as the ability to run code after a response has been sent for things like thumbnailing or background calculation. If you’ve worked with Django before, you already know how important this project is. Supporting the features mentioned with Django’s current implementation requires libraries like Celery (to handle complex tasks outside the real of a request), or Node.js, django-websocket-redis, or gevent-socketio to support WebSockets. With the exception of Celery (which is a defacto standard) all the other implementations are non-standard way of working around the limitations of Django with problems of their own. We’ve covered implementation of some of these features in older blogposts with varying degrees of success.
Having a standard implementation provides ease of maintenance, better security and faster turn-around times as most developers will be familiar with the concepts involved.
On this blogpost we’ll provide a quick introduction to the concepts involved in developing a Channels enabled Django site as well as presenting a working example covering the use case of pushing notifications to the client using WebSockets.
The example that we’re presenting is a modification of an application that we built for a blogpost on real-time notifications with gevent-socketio. The goal is to give you a chance to see how much simpler it is to implement the same site using Django Channels. The code for the example is available on GitHub.
A default Django site follows the request-response model: A request comes in, it is routed to the proper view, the view generates a response, the response is sent to the client, and everything is done in a single process.
This works just fine for most applications, but it has its limitations. If the request is complex, it might hold the worker process for a long time, making subsequent requests wait for their turn. That’s why we use Celery to do things like generating thumbnails: the image upload comes in, we enqueue the thumbnail generation task and respond to the client right away, while the Celery worker takes care of the image on its own process.
The same happens if we want real-time two-way communication with clients. In a request-response scenario, we would need to keep a process dedicated to each client so we can receive and send information until the connection is no longer needed.
Django channels provides a different model: An event-oriented model. In this model, instead of requests and response we have just events. An event with a request is received, then it is given to the proper event handler which generates a new event with the response that is going to be sent to the client.
But the event model can be applied to other scenarios and not just mimicking the request-response model. For instance, suppose that a hardware sensor is triggered due to external condition, this generates an event which is given to the event handler, which in turn generates another event to notify whomever is interested in the occurrence of the original event.
But how does this process work? We need to discuss channels first before we get our hands on a working example.
What is a channel? According to Django channel’s documentation, a channel is…
…an ordered, first-in first-out queue with message expiry and at-most-once delivery to only one listener at a time. Several producers writes messages into a channel (which is identified by a name), and when a consumer that was subscribed to that channel becomes available, it picks the first message that came into the queue. Simple as that.
Channels changes the way Django works, making it act as a worker. Each worker listens on all channels with consumers assigned (or routed). When a message comes in, the appropriate consumer is invoked. In order for this to work, we need three layers:
Interface servers: It connects the site with the clients, through a WSGI adapter as well as a separate WebSocket server. Channel back-end: It transports the messages between the interface and the workers. It is a datastore (memory for single-server situations, a database or Redis) and Python code to tie it all in. The workers: They listen on all the channels and and runs the consumers (functions) when a message is ready. The interface servers transform connections (HTTP, WebSockets, etc.) into messages on channels, and workers are in charge of handling those messages. The trick here is that message need not come from the interface servers. Messages can be created anywhere, in views, forms, signals, you name it.
Time to get our hands dirty.
Our first consumer We’ll start with an fresh Django 1.8 (works with 1.9, too) installation. First, we need to install the “channels” projects which is available through PyPi. If you want to install the latest development version of channels, follow the instructions on the documentation.
Next we need to put “channels” into our INSTALLED_APPS setting:
INSTALLED_APPS = ( … ‘channels_test’, # Our test app ‘channels’, ) That’s it. Channels default configuration uses an in-memory back-end which works just fine for sites working on a single server.
We’re going to write a simple consumer that listens to messages on the default “http.message” channel and writes a new message onto the reply channel. Let’s create a module called “consumers.py” within our test Django app called “channels_test”:
from json import dumps from django.http import HttpResponse from django.utils.timezone import now
def http_consumer(message): response = HttpResponse( “It is now {} and you’ve requested {} with {} as request parameters.”.format( now(), message.content[‘path’], dumps(message.content[‘get’]) ) )
message.reply_channel.send(response.channel_encode()) A message comes through the standard “request.http” channel and our consumer writes a new message with a response through the response channel. Something worth mentioning is that there are two types of channel: the regular ones used to get messages to consumers, and response channels. Only the interface server is listening on these response channels, and it knows which channel is connected to which connection (client) so it know who to send the responses to.
Before we can proceed, we need a way to tell Django to send messages on the “request.http” channel to our brand new consumer. Create a module called “routing.py” next to your settings module:
channel_routing = { “http.request”: “channels_test.consumers.http_consumer” } Now we run the server (with the development server o a WSGI server, doesn’t matter at this point), and make a request to our site:
$ curl http://localhost:8000/some/path?foo=bar It is now 2016-02-01 11:49:25.166799+00:00 and you’ve requested /some/path with {“foo”: [“bar”]} as request parameters. We got the response we were expecting. Channels took care of most of the problems for us. Now let’s do something a little more interesting.
Real-time notifications (yet again) We’ve covered the topic of real-time notifications on several occasions before, and this gives us a great opportunity to cover one of Channels uses cases and compare two solutions side by side.
We’re going to adapt an existing project to track real life events geographically on specific areas of interesting notifying users of the site of the occurrence of such events in (close to) real-time. In that project we used gevent-socketio, SocketIO, and RabbitMQ (and Node.js second attempt). We’re going to do the same now using Channels, regular WebSockets and Redis
As we said, we’re going to use WebSockets to push notifications to the client. Channels has full support of WebSockets. All we need to do is hook-up a few channels to consumers on our “tracker” app:
channel_routing = { “websocket.connect”: “tracker.consumers.websocket_connect”, “websocket.keepalive”: “tracker.consumers.websocket_keepalive”, “websocket.disconnect”: “tracker.consumers.websocket_disconnect” } We’re not interested in the “websocket.message” channel as we’re not going to be receiving messages from the client. Our goal is to send notifications to all connected clients whenever an events occur in an area of interest. This is quite easy to do using a Group. Let’s take a look at out consumers:
import logging
from channels import Group from channels.sessions import channel_session from channels.auth import channel_session_user_from_http
logger = logging.getLogger(name)
@channel_session_user_from_http def websocket_connect(message): logger.info(‘websocket_connect. message = %s’, message) # transfer_user(message.http_session, message.channel_session) Group(“notifications”).add(message.reply_channel)
@channel_session def websocket_keepalive(message): logger.info(‘websocket_keepalive. message = %s’, message) Group(“notifications”).add(message.reply_channel)
@channel_session def websocket_disconnect(message): logger.info(‘websocket_disconnect. message = %s’, message) Group(“notifications”).discard(message.reply_channel) Whenever a client connects, a message is sent through the “websocket.connect” channel. All we do is then add the reply channel (which comes with the original message), and add it to the “notifications” Groups. Groups allow us to send the same message to all the channels in the group at the same time. All we need to do is keep the set of group channels updated. So whenever a client connects, we add the reply channel to the group and when it disconnects, we remove it. Simple as that. Groups also drop channels after a while, so we use the “websocket.keepalive” channel to add the reply channel to the “notifications” group whenever a keepalive message is received. If the channel was already in the group it won’t be added twice.
Notice that we haven’t sent anything to the Group yet. We need to notify the users whenever an Incident within an AreaOfInterest is reported or updated. We can do that easily using the post_save signals:
import logging
from json import dumps
from django.db.models.signals import post_save, post_delete from django.dispatch import receiver
from channels import Group
from .models import Incident, AreaOfInterest
logger = logging.getLogger(name)
def send_notification(notification): logger.info(‘send_notification. notification = %s’, notification) Group(“notifications”).send({‘text’: dumps(notification)})
@receiver(post_save, sender=Incident) def incident_post_save(sender, **kwargs): send_notification({ ‘type’: ‘post_save’, ‘created’: kwargs[‘created’], ‘feature’: kwargs[‘instance’].geojson_feature })
if not kwargs['instance'].closed:
areas_of_interest = [
area_of_interest.geojson_feature for area_of_interest in AreaOfInterest.objects.filter(
polygon__contains=kwargs['instance'].location,
severity__in=kwargs['instance'].alert_severities,
)
]
if areas_of_interest:
send_notification(dict(
type='alert',
feature=kwargs['instance'].geojson_feature,
areas_of_interest=[
{
'id': area_of_interest['id'],
'name': area_of_interest['properties']['name'],
'severity': area_of_interest['properties']['severity'],
'url': area_of_interest['properties']['url'],
}
for area_of_interest in areas_of_interest
]
))
@receiver(post_save, sender=AreaOfInterest) def area_of_interest_post_save(sender, **kwargs): send_notification({ ‘type’: ‘post_save’, ‘created’: kwargs[‘created’], ‘feature’: kwargs[‘instance’].geojson_feature })
@receiver(post_delete, sender=Incident) @receiver(post_delete, sender=AreaOfInterest) def post_delete(sender, **kwargs): send_notification({ ‘type’: ‘post_delete’, ‘feature’: kwargs[‘instance’].geojson_feature }) All the notification magic occurs in send_notification which, as you can see, it’s just a freakin’ line of code! (go ahead, check the older implementations). The rest of the code is the same as before.
So far, we’ve only used the in-memory channels backend, In order for our notification system to work in a multi-server environment, we need to use the database backend (slow, shouldn’t be used for anything other than development) or the Redis back-end. Let’s use the latter. We have to add the following snippet to our settings.py module:
CHANNEL_LAYERS = { “default”: { “BACKEND”: “asgi_redis.RedisChannelLayer”, “CONFIG”: { “hosts”: [(“localhost”, 6379)], }, “ROUTING”: “tracker_project.routing.channel_routing”, }, } In order for this backend to work we also need to install the asgi_redis package (in-memory and database layers are included in the default channels package). The last thing we need to change on the Django side is creating an asgi.py module that servers a similar role as wsgi.py does for WSGI servers.
import os from channels.asgi import get_channel_layer
os.environ.setdefault(“DJANGO_SETTINGS_MODULE”, “tracker_project.settings”)
channel_layer = get_channel_layer() This module will be used later to run the interface server.
We also needed to change the client code to use WebSockets instead of of Socket.io. Since the code is pretty much the same (construct socket, hook-up the proper events), we won’t go into details here. You can see the implementation here.
All that is left is running the server. For development purposes, we can use the runserver management command. It has been modified to run the Daphne ASGI server and a worker.
(tracker_project_venv)$ ./manage.py runserver Worker thread running, channels enabled Performing system checks…
System check identified no issues (0 silenced). February 01, 2016 - 13:19:43 Django version 1.8.8, using settings ‘tracker_project.settings’ Starting development server at http://127.0.0.1:8000/ Quit the server with CONTROL-C. On a live environment, we would need to run Daphne (or any ASGI compatible server with support for WebSockets) and as many workers as we see fit. Daphne runs with:
(tracker_project_venv)$ daphne tracker_project.asgi:channel_layer We’re telling Daphne to look for the channel layer on our asgi.py module. Each worker runs with:
(venv)$ python ./manage.py runworker With everything up and running, you’ll be able to report incidents and receive notifications if the home view is open.
Note on authentication Something we didn’t mention is that our WebSockets have authentication. The initial step of a WebSocket connection is a regular HTTP request, so we can take advantage of the existing Django session management through the usage of Channel’s @channel_session_user_from_http decorator. If the user is not authenticated (it as no valid session) the consumer won’t run and the reply channel won’t be hooked up to the “notifications” group.
We need to make sure that we pass the session key when constructing the WebSocket:
var socket = new WebSocket(‘ws://localhost:8000?session_key=’ + sessionKey); It is rudimentary, but it gets the job done. There are far better options for WebSocket authentication (I’m partial to token based systems like JWT), but covering them might take a blogpost of its own.
Conclusions Django channels is going to change the way we work dramatically. It will make our lives easier, and it will allow us to tackle a wider range of problems with a Django site, but one of the neatest features of the project is that IT IS ENTIRELY OPTIONAL. In our example, we only routed the channels that we were interested in. We didn’t have to change our existing request-response based code. We get the best of both worlds.
Channels is not ready for a production environment, but once it is integrated into the next versions of Django, it won’t take long for it to mature into a stable framework. Another cool thing is that is going to be back-ported to Django 1.8 as an external app, so you’ll be able to integrate it into your existing sites.
Vagrant A Vagrant configuration file is included if you want to test the solutions.
Feedback As usual, I welcome comments, suggestions and pull requests.
http://www.machinalis.com/blog/nested-resources-with-django/
Nested resources with Django REST Framework A complete example of how to implement nested resources with Django REST Framework
Posted by Agustín Bartó 11 months, 2 weeks ago Comments Introduction Django REST Framework (DRF) is a great way to provide RESTful interfaces for Django sites. Although it is fairly complete and does pretty much everything for you, it doesn’t cover all the possible use cases. One of those cases is nested resources. Whether nested resources are good or bad is a matter for a long discussion, but sadly sometimes an API design is forced on us and we have no choice but to try to match the specification as close as possible.
DRF’s documentation suggests using the @list-route and @detail-route decorators to solve this problem, but with this solution you lose most of the automatic functionality that makes DRF such a great tool.
This blogpost shows you how to implement an alternative solution for nested resources in Django REST framework using drf-nested-routers.
The code The code for this blogpost is available on GitHub. A Vagrant configuration file is included if you want to test the service yourself.
drf-nested-routers drf-nested-routers is a package that allows you to nest DRF’s routers within each other, effectively providing nested resources. Although it works like a charm, the example provided in the documentation assumes that developer already knows all the ins and outs of DRF and that’s not always the case (it wasn’t for me at least). I wrote this blogpost to provide a more thorough example.
The API The model for the API describes blogposts and comments on those blogposts:
class UUIDIdMixin(models.Model): class Meta: abstract = True
id = models.UUIDField(primary_key=True, default=uuid.uuid4, editable=False)
class AuthorMixin(models.Model): class Meta: abstract = True
author = models.ForeignKey(
settings.AUTH_USER_MODEL, editable=False, verbose_name=_('author'),
related_name='%(app_label)s_%(class)s_author'
)
class Blogpost(UUIDIdMixin, TimeStampedModel, TitleSlugDescriptionModel, AuthorMixin): content = models.TextField((‘content’), blank=True, null=True) allow_comments = models.BooleanField((‘allow comments’), default=True)
class Comment(UUIDIdMixin, TimeStampedModel, AuthorMixin):
blogpost = models.ForeignKey(
Blogpost, editable=False, verbose_name=(‘blogpost’), related_name=’comments’
)
content = models.TextField((‘content’), max_length=255, blank=False, null=False)
We want to expose the API for comments related to a specific blogpost on a path like /blogposts/
We expose all the actions (list, retrieve, create, update, partial update and destroy) in nested paths like /blogposts/
We want to expose two sets of actions under different paths and DRF provides an abstraction for such use case: a ViewSet. One of the great things about this framework is that it provides a lot of functionality in easy to use mixins. If we combine these two aspects, our solution is actually quite simple:
class BlogpostViewSet(ModelViewSet): serializer_class = BlogpostSerializer queryset = Blogpost.objects.all() permission_classes = (IsAuthenticatedOrReadOnly, IsAuthorOrReadOnly)
def perform_create(self, serializer):
serializer.save(author=self.request.user)
class CommentViewSet( RetrieveModelMixin, UpdateModelMixin, DestroyModelMixin, ListModelMixin, GenericViewSet ): queryset = Comment.objects.all() serializer_class = CommentSerializer permission_classes = (IsAuthenticatedOrReadOnly, CommentDeleteOrUpdatePermission)
class NestedCommentViewSet(CreateModelMixin, ListModelMixin, GenericViewSet): queryset = Comment.objects.all() serializer_class = CommentSerializer permission_classes = (IsAuthenticatedOrReadOnly, CommentsAllowed)
def get_blogpost(self, request, blogpost_pk=None):
""" Look for the referenced blogpost """
# Check if the referenced blogpost exists
blogpost = get_object_or_404(Blogpost.objects.all(), pk=blogpost_pk)
# Check permissions
self.check_object_permissions(self.request, blogpost)
return blogpost
def create(self, request, *args, **kwargs):
self.get_blogpost(request, blogpost_pk=kwargs['blogpost_pk'])
return super().create(request, *args, **kwargs)
def perform_create(self, serializer):
serializer.save(
author=self.request.user,
blogpost_id=self.kwargs['blogpost_pk']
)
def get_queryset(self):
return Comment.objects.filter(blogpost=self.kwargs['blogpost_pk'])
def list(self, request, *args, **kwargs):
self.get_blogpost(request, blogpost_pk=kwargs['blogpost_pk'])
return super().list(request, *args, **kwargs) The first ViewSet is just a regular ModelViewSet to provide a typical API for the Blogpost model. The other two implement the design that I mentioned before, one ViewSet for the nested comments (NestedCommentViewSet) that only provides list and create actions (through the CreateModelMixin and ListModelMixin mixins) and a root level ViewSet for the rest of the actions.
You’ll also need the appropriate serializers:
class BlogpostSerializer(HyperlinkedModelSerializer): class Meta: model = Blogpost fields = (‘url’, ‘title’, ‘slug’, ‘description’, ‘content’, ‘allow_comments’, ‘author’, ‘created’, ‘modified’) read_only_fields = (‘url’, ‘slug’, ‘author’, ‘created’, ‘modified’)
class CommentSerializer(HyperlinkedModelSerializer): class Meta: model = Comment fields = (‘url’, ‘content’, ‘author’, ‘created’, ‘modified’, ‘blogpost’) read_only_fields = (‘url’, ‘author’, ‘created’, ‘modified’, ‘blogpost’) All that is left is wiring these viewsets to URLs, and that’s when drf-nested-routers comes into play:
router = DefaultRouter() router.register(r’users’, UserViewSet) router.register(r’blogposts’, BlogpostViewSet) router.register(r’comments’, CommentViewSet)
blogposts_router = NestedSimpleRouter(router, r’blogposts’, lookup=’blogpost’) blogposts_router.register(r’comments’, NestedCommentViewSet)
urlpatterns = [ url(r’^admin/’, include(admin.site.urls)), url(r’^api/’, include(router.urls)), url(r’^api/’, include(blogposts_router.urls)), url(r’^o/’, include(‘oauth2_provider.urls’, namespace=’oauth2_provider’)), ] And that’s it. DRF will take care of the rest (no pun intended). The other approach would have required a bit of extra work like implementing a custom HyperlinkedRelatedField implementation.
Usage We used OAuth2 for authentication and authorization, and created an application to allow access to the API. The application was defined as “Public” with grant type “Resource owner password-base”, so all we need to do to access the API is request an access token:
$ curl –silent –header “Content-Type: application/x-www-form-urlencoded” –data “username=admin&password=admin&grant_type=password&client_id=7ytbv0sG9FusDdDYRcZPUIGoNrx9TBZJnye5CVvj” –request POST http://localhost:8000/o/token/|python -mjson.tool; echo { “access_token”: “Q8Wbo12h5jwgwR208WDNrhNpK20Ta0”, “expires_in”: 36000, “refresh_token”: “inEHtHuerVRXSH5QjbvokgrqJYxngL”, “scope”: “read write”, “token_type”: “Bearer” } Afterwards, you can request a list of blogposts:
$ curl –header “Authorization: Bearer Q8Wbo12h5jwgwR208WDNrhNpK20Ta0” –header “Accept: application/json; indent=4” –request GET http://localhost:8000/api/blogposts/; echo [ { “url”: “http://127.0.0.1:8000/api/blogposts/588660f1-4848-4a32-8eb5-9688fd4409dd/”, “title”: “A longer blogpost”, “slug”: “a-longer-blogpost”, “description”: “Lorem ipsum dolor sit amet…”, “content”: “Lorem ipsum dolor sit amet, consectetur adipiscing elit. Vivamus maximus, lorem eget accumsan maximus, ante mauris lacinia massa, sit amet pellentesque nisl leo eu libero. Fusce hendrerit risus eu vehicula cursus. Duis tincidunt enim eget felis tempus, ut consequat purus elementum.”, “allow_comments”: true, “author”: “http://127.0.0.1:8000/api/users/2/”, “created”: “2015-07-10T00:15:38.135000Z”, “modified”: “2015-07-10T00:16:34.192000Z” }, { “url”: “http://127.0.0.1:8000/api/blogposts/b44d4918-219e-4496-9318-b68ab13e2b25/”, “title”: “A short blogpost”, “slug”: “a-short-blogpost”, “description”: “The description of the blogpost is short”, “content”: “This is just a short blogpost.”, “allow_comments”: true, “author”: “http://127.0.0.1:8000/api/users/2/”, “created”: “2015-07-10T00:14:06.500000Z”, “modified”: “2015-07-10T00:14:06.501000Z” } ] You can request a list of comments for a specific blogpost:
$ curl –header “Authorization: Bearer Q8Wbo12h5jwgwR208WDNrhNpK20Ta0” –header “Accept: application/json; indent=4” –request GET http://localhost:8000/api/blogposts/588660f1-4848-4a32-8eb5-9688fd4409dd/comments/; echo [ { “url”: “http://127.0.0.1:8000/api/comments/17288f69-bbd7-4758-adfd-a96d0fa5ca01/”, “content”: “I hate the Internet”, “author”: “http://127.0.0.1:8000/api/users/2/”, “created”: “2015-07-10T00:24:47.766000Z”, “modified”: “2015-07-10T00:24:47.766000Z”, “blogpost”: “http://127.0.0.1:8000/api/blogposts/588660f1-4848-4a32-8eb5-9688fd4409dd/” } ] You can create create a new comment POSTing to the nested URL for a specific blogpost:
$ curl –verbose –header “Authorization: Bearer Q8Wbo12h5jwgwR208WDNrhNpK20Ta0” –header “Accept: application/json; indent=4” –header “Content-Type: application/json” –request POST –data ‘{“content”: “No RESTful for the wicked”}’ http://localhost:8000/api/blogposts/588660f1-4848-4a32-8eb5-9688fd4409dd/comments/; echo
POST /api/blogposts/588660f1-4848-4a32-8eb5-9688fd4409dd/comments/ HTTP/1.1 User-Agent: curl/7.40.0 Host: localhost:8000 Authorization: Bearer Q8Wbo12h5jwgwR208WDNrhNpK20Ta0 Accept: application/json; indent=4 Content-Type: application/json Content-Length: 40
$ curl –header “Authorization: Bearer Q8Wbo12h5jwgwR208WDNrhNpK20Ta0” –header “Accept: application/json; indent=4” –request GET http://localhost:8000/api/blogposts/588660f1-4848-4a32-8eb5-9688fd4409dd/comments/; echo [ { “url”: “http://127.0.0.1:8000/api/comments/17288f69-bbd7-4758-adfd-a96d0fa5ca01/”, “content”: “I hate the Internet”, “author”: “http://127.0.0.1:8000/api/users/2/”, “created”: “2015-07-10T00:24:47.766000Z”, “modified”: “2015-07-10T00:24:47.766000Z”, “blogpost”: “http://127.0.0.1:8000/api/blogposts/588660f1-4848-4a32-8eb5-9688fd4409dd/” }, { “url”: “http://127.0.0.1:8000/api/comments/81e5afc6-56fc-47d4-9665-db56229d0fba/”, “content”: “No RESTful for the wicked”, “author”: “http://127.0.0.1:8000/api/users/1/”, “created”: “2015-07-29T18:15:30.294242Z”, “modified”: “2015-07-29T18:15:30.294635Z”, “blogpost”: “http://127.0.0.1:8000/api/blogposts/588660f1-4848-4a32-8eb5-9688fd4409dd/” } ] You can also hit the /comments endpoint directly to list, update or delete a comment:
$ curl –header “Authorization: Bearer Q8Wbo12h5jwgwR208WDNrhNpK20Ta0” –header “Accept: application/json; indent=4” –request GET http://localhost:8000/api/comments/81e5afc6-56fc-47d4-9665-db56229d0fba/; echo { “url”: “http://127.0.0.1:8000/api/comments/81e5afc6-56fc-47d4-9665-db56229d0fba/”, “content”: “No RESTful for the wicked”, “author”: “http://127.0.0.1:8000/api/users/1/”, “created”: “2015-07-29T18:15:30.294242Z”, “modified”: “2015-07-29T18:15:30.294635Z”, “blogpost”: “http://127.0.0.1:8000/api/blogposts/588660f1-4848-4a32-8eb5-9688fd4409dd/” } Conclusions Django REST Framework is powerful, flexible and easy to use. Although it doesn’t support nested resources, they can be implemented without much effort as long as you’re willing to accommodate certain design restrictions.
原文链接:http://www.machinalis.com/blog/image-fields-with-django-rest-framework/
A complete example that illustrates how to implement image uploads and models with image fields with Django REST Framework
Not long ago we had to implement a Django site that exposes an API using Django REST Framework. Through this API we gave access to the users to models that had image fields on them. Although the problem is pretty simple, all we could find were disjointed examples that showed how to implement a solution. We wrote this blogpost to provide a complete example that shows how to handle image uploads and image fields using a Django REST Framework API.
The code for this blogpost is available on GitHub. A Vagrant configuration file is included if you want to test the service yourself.
Out example has only one class that represents the typical “User Profile” use case on a Django site:
def upload_to(instance, filename):
return 'user_profile_image/{}/{}'.format(instance.user_id, filename)
class UserProfile(models.Model):
GENDER_UNKNOWN = 'U'
GENDER_MALE = 'M'
GENDER_FEMALE = 'F'
GENDER_CHOICES = (
(GENDER_UNKNOWN, _('unknown')),
(GENDER_MALE, _('male')),
(GENDER_FEMALE, _('female')),
)
user = models.OneToOneField(settings.AUTH_USER_MODEL, primary_key=True)
date_of_birth = models.DateField(_('date of birth'), blank=True, null=True)
phone_number = PhoneNumberField(_('phone number'), blank=True)
gender = models.CharField(_('gender'), max_length=1, choices=GENDER_CHOICES, default=GENDER_UNKNOWN)
image = models.ImageField(_('image'), blank=True, null=True, upload_to=upload_to)
With the exception of the phone_number field (which uses django-phonenumber-field), the rest of the fields are regular Django fields, including the image which is the subject of this project and represents an image for the associated user.
As with all Django REST Framework APIs, we need to define serializers, views (or viewsets) and hook the views in the site’s URLs. Let’s start with the serializers:
class UserProfileSerializer(HyperlinkedModelSerializer):
class Meta:
model = UserProfile
fields = ('url', 'date_of_birth', 'phone_number', 'gender', 'image')
readonly_fields = ('url', 'image')
It couldn’t be simpler. UserProfileSerializer it’s just a HyperlinkedModelSerializer that handles the UserProfile model. Given that it is not possible to handle uploads using the default JSON parser, we marked the image field as read-only.
The views are a little more interesting:
class UserProfileViewSet(RetrieveModelMixin, UpdateModelMixin, GenericViewSet):
queryset = UserProfile.objects.all()
serializer_class = UserProfileSerializer
permission_classes = (IsAdminOrIsSelf,)
def __init__(self, *args, **kwargs):
super().__init__(*args, **kwargs)
@detail_route(methods=['POST'], permission_classes=[IsAdminOrIsSelf])
@parser_classes((FormParser, MultiPartParser,))
def image(self, request, *args, **kwargs):
if 'upload' in request.data:
user_profile = self.get_object()
user_profile.image.delete()
upload = request.data['upload']
user_profile.image.save(upload.name, upload)
return Response(status=HTTP_201_CREATED, headers={'Location': user_profile.image.url})
else:
return Response(status=HTTP_400_BAD_REQUEST)
We have a GenericViewSet combined with RetrieveModelMixin and UpdateModelMixin to provide retrieve and update funcionality for our UserProfile model (It doesn’t make sense to provide list or destroy in this context). The interesting part is the image method, which is exposed as a view using @detail_route decorator.
The trick here is that the method is also decorated using @parser_classes where we declare that the requests should be parsed using FormParser or MultiPartParser, and this is what is going to allow us to handle the uploaded files.
When the method is invoked, we check that the request data contains an upload entry, and if it does we delete the image associated with the user profile, replace it with the UploadedFile contents and return a Response with status code 201 (Created). If upload is not in the request data, we return a fail response with status 400 (Bad Request).
The last part is to set up the URLs for our API:
router = DefaultRouter()
router.register(r'user_profiles', UserProfileViewSet)
urlpatterns = [
url(r'^admin/', include(admin.site.urls)),
url(r'^', include(router.urls)),
url(r'^o/', include('oauth2_provider.urls', namespace='oauth2_provider')),
]
We used a Django REST Framework Router which wires everything automatically and thus save us a lot of work. Notice that we’re also using Django OAuth Toolkit to provide authentication for our API.
The following session illustrates the typical usage of our API.
$ curl --header "Content-Type: application/x-www-form-urlencoded" --header "Accept: application/json; indent=4" --request POST --data "username=admin&password=admin&client_id=zmfZyf7EAGJJ6imph3qtwGtoH8eqt1VdVmRZh7NC&grant_type=password" http://localhost:8000/o/token/; echo
{"access_token": "PkwvCYq0cRYfvpJeXvc4czFKvohwea", "expires_in": 36000, "token_type": "Bearer", "scope": "write read", "refresh_token": "jl3Y5Mo7fLaHvJDWCQv5I9g4zbLHkT"}
$ curl --header "Authorization: Bearer PkwvCYq0cRYfvpJeXvc4czFKvohwea" --header "Accept: application/json; indent=4" --request GET http://localhost:8000/user_profiles/1/; echo
{
"url": "http://localhost:8000/user_profiles/1/",
"date_of_birth": "2015-07-07",
"phone_number": "+41524204242",
"gender": "M",
"image": "http://localhost:8000/media/user_profile_image/1/admin.png"
}
$ curl --verbose --header "Authorization: Bearer PkwvCYq0cRYfvpJeXvc4czFKvohwea" --header "Accept: application/json; indent=4" --request POST --form upload=@admin2.jpg http://localhost:8000/user_profiles/1/image/; echo
* Trying 127.0.0.1...
* Connected to localhost (127.0.0.1) port 8000 (#0)
* Initializing NSS with certpath: sql:/etc/pki/nssdb
> POST /user_profiles/1/image/ HTTP/1.1
> User-Agent: curl/7.40.0
> Host: localhost:8000
> Authorization: Bearer PkwvCYq0cRYfvpJeXvc4czFKvohwea
> Accept: application/json; indent=4
> Content-Length: 3737
> Expect: 100-continue
> Content-Type: multipart/form-data; boundary=------------------------f915e8f2eaef4479
>
* Done waiting for 100-continue
* HTTP 1.0, assume close after body
< HTTP/1.0 201 CREATED
< Date: Tue, 07 Jul 2015 01:34:01 GMT
< Server: WSGIServer/0.2 CPython/3.4.2
< Vary: Accept
< Location: http://localhost:8000/media/user_profile_image/1/admin2.jpg
< X-Frame-Options: SAMEORIGIN
< Allow: POST, OPTIONS
<
* Closing connection 0
$ curl --header "Authorization: Bearer PkwvCYq0cRYfvpJeXvc4czFKvohwea" --header "Accept: application/json; indent=4" --request GET http://localhost:8000/user_profiles/1/; echo
{
"url": "http://localhost:8000/user_profiles/1/",
"date_of_birth": "2015-07-07",
"phone_number": "+41524204242",
"gender": "M",
"image": "http://localhost:8000/media/user_profile_image/1/admin2.jpg"
}
As we mentioned before, this problem was actually pretty simple, and it only required know how to wire everything properly in a way that makes sense in the context of Django REST Framework API.