}` –
78 | * transform function or an array of such functions. The transform function takes the http
79 | * response body and headers and returns its transformed (typically deserialized) version.
80 | * - **`cache`** – `{boolean|Cache}` – If true, a default $http cache will be used to cache the
81 | * GET request, otherwise if a cache instance built with
82 | * {@link http://docs.angularjs.org/api/ng.$cacheFactory $cacheFactory}, this cache will be used for
83 | * caching.
84 | * - **`timeout`** – `{number}` – timeout in milliseconds.
85 | * - **`withCredentials`** - `{boolean}` - whether to to set the `withCredentials` flag on the
86 | * XHR object. See {@link https://developer.mozilla.org/en/http_access_control#section_5
87 | * requests with credentials} for more information.
88 | * - **`responseType`** - `{string}` - see
89 | * {@link https://developer.mozilla.org/en-US/docs/DOM/XMLHttpRequest#responseType requestType}.
90 | *
91 | * @returns {Object} A resource "class" object with methods for the default set of resource actions
92 | * optionally extended with custom `actions`. The default set contains these actions:
93 | *
94 | * { 'get': {method:'GET'},
95 | * 'save': {method:'POST', method_if_field_has_value:['id', 'PUT']},
96 | * 'update': {method:'PUT'},
97 | * 'query': {method:'GET', isArray:true},
98 | * 'remove': {method:'DELETE'},
99 | * 'delete': {method:'DELETE'} };
100 | *
101 | * Calling these methods invoke an {@link http://docs.angularjs.org/api/ng.$http $http} with the specified http
102 | * method, destination and parameters. When the data is returned from the server then the object is an
103 | * instance of the resource class. The actions `save`, `remove` and `delete` are available on it
104 | * as methods with the `$` prefix. This allows you to easily perform CRUD operations (create,
105 | * read, update, delete) on server-side data like this:
106 | *
107 | var User = djResource('/user/:userId', {userId:'@id'});
108 | var user = User.get({userId:123}, function() {
109 | user.abc = true;
110 | user.$save();
111 | });
112 |
113 | *
114 | * Invoking a djResource object method immediately returns an empty reference (object or array depending
115 | * on `isArray`). Once the data is returned from the server the existing reference is populated with the actual data.
116 | *
117 | * The action methods on the class object or instance object can be invoked with the following
118 | * parameters:
119 | *
120 | * - HTTP GET "class" actions: `DjangoRESTResource.action([parameters], [success], [error])`
121 | * - non-GET "class" actions: `DjangoRESTResource.action([parameters], postData, [success], [error])`
122 | * - non-GET instance actions: `instance.$action([parameters], [success], [error])`
123 | *
124 | *
125 | * The DjangoRESTResource instances and collection have these additional properties:
126 | *
127 | * - `$then`: the `then` method of a {@link http://docs.angularjs.org/api/ng.$q promise} derived from the underlying
128 | * {@link http://docs.angularjs.org/api/ng.$http $http} call.
129 | *
130 | * The success callback for the `$then` method will be resolved if the underlying `$http` requests
131 | * succeeds.
132 | *
133 | * The success callback is called with a single object which is the
134 | * {@link http://docs.angularjs.org/api/ng.$http http response}
135 | * object extended with a new property `resource`. This `resource` property is a reference to the
136 | * result of the resource action — resource object or array of resources.
137 | *
138 | * The error callback is called with the {@link http://docs.angularjs.org/api/ng.$http http response} object when
139 | * an http error occurs.
140 | *
141 | * - `$resolved`: true if the promise has been resolved (either with success or rejection);
142 | * Knowing if the DjangoRESTResource has been resolved is useful in data-binding.
143 | */
144 | angular.module('djangoRESTResources', ['ng']).
145 | factory('djResource', ['$http', '$parse', function($http, $parse) {
146 | var DEFAULT_ACTIONS = {
147 | 'get': {method:'GET'},
148 | 'save': {method:'POST', method_if_field_has_value: ['id','PUT']},
149 | 'update': {method:'PUT'},
150 | 'query': {method:'GET', isArray:true},
151 | 'remove': {method:'DELETE'},
152 | 'delete': {method:'DELETE'}
153 | };
154 | var noop = angular.noop,
155 | forEach = angular.forEach,
156 | extend = angular.extend,
157 | copy = angular.copy,
158 | isFunction = angular.isFunction,
159 | getter = function(obj, path) {
160 | return $parse(path)(obj);
161 | };
162 |
163 | /**
164 | * We need our custom method because encodeURIComponent is too aggressive and doesn't follow
165 | * http://www.ietf.org/rfc/rfc3986.txt with regards to the character set (pchar) allowed in path
166 | * segments:
167 | * segment = *pchar
168 | * pchar = unreserved / pct-encoded / sub-delims / ":" / "@"
169 | * pct-encoded = "%" HEXDIG HEXDIG
170 | * unreserved = ALPHA / DIGIT / "-" / "." / "_" / "~"
171 | * sub-delims = "!" / "$" / "&" / "'" / "(" / ")"
172 | * / "*" / "+" / "," / ";" / "="
173 | */
174 | function encodeUriSegment(val) {
175 | return encodeUriQuery(val, true).
176 | replace(/%26/gi, '&').
177 | replace(/%3D/gi, '=').
178 | replace(/%2B/gi, '+');
179 | }
180 |
181 |
182 | /**
183 | * This method is intended for encoding *key* or *value* parts of query component. We need a custom
184 | * method because encodeURIComponent is too aggressive and encodes stuff that doesn't have to be
185 | * encoded per http://tools.ietf.org/html/rfc3986:
186 | * query = *( pchar / "/" / "?" )
187 | * pchar = unreserved / pct-encoded / sub-delims / ":" / "@"
188 | * unreserved = ALPHA / DIGIT / "-" / "." / "_" / "~"
189 | * pct-encoded = "%" HEXDIG HEXDIG
190 | * sub-delims = "!" / "$" / "&" / "'" / "(" / ")"
191 | * / "*" / "+" / "," / ";" / "="
192 | */
193 | function encodeUriQuery(val, pctEncodeSpaces) {
194 | return encodeURIComponent(val).
195 | replace(/%40/gi, '@').
196 | replace(/%3A/gi, ':').
197 | replace(/%24/g, '$').
198 | replace(/%2C/gi, ',').
199 | replace(/%20/g, (pctEncodeSpaces ? '%20' : '+'));
200 | }
201 |
202 | function Route(template, defaults) {
203 | this.template = template = template + '#';
204 | this.defaults = defaults || {};
205 | this.urlParams = {};
206 | }
207 |
208 | Route.prototype = {
209 | setUrlParams: function(config, params, actionUrl) {
210 | var self = this,
211 | url = actionUrl || self.template,
212 | val,
213 | encodedVal;
214 |
215 | var urlParams = self.urlParams = {};
216 | forEach(url.split(/\W/), function(param){
217 | if (!(new RegExp("^\\d+$").test(param)) && param && (new RegExp("(^|[^\\\\]):" + param + "(\\W|$)").test(url))) {
218 | urlParams[param] = true;
219 | }
220 | });
221 | url = url.replace(/\\:/g, ':');
222 |
223 | params = params || {};
224 | forEach(self.urlParams, function(_, urlParam){
225 | val = params.hasOwnProperty(urlParam) ? params[urlParam] : self.defaults[urlParam];
226 | if (angular.isDefined(val) && val !== null) {
227 | encodedVal = encodeUriSegment(val);
228 | url = url.replace(new RegExp(":" + urlParam + "(\\W|$)", "g"), encodedVal + "$1");
229 | } else {
230 | url = url.replace(new RegExp("(\/?):" + urlParam + "(\\W|$)", "g"), function(match,
231 | leadingSlashes, tail) {
232 | if (tail.charAt(0) == '/') {
233 | return tail;
234 | } else {
235 | return leadingSlashes + tail;
236 | }
237 | });
238 | }
239 | });
240 |
241 | // set the url
242 | config.url = url.replace(/#$/, '');
243 |
244 | // set params - delegate param encoding to $http
245 | forEach(params, function(value, key){
246 | if (!self.urlParams[key]) {
247 | config.params = config.params || {};
248 | config.params[key] = value;
249 | }
250 | });
251 | }
252 | };
253 |
254 |
255 | function DjangoRESTResourceFactory(url, paramDefaults, actions) {
256 | var route = new Route(url);
257 |
258 | actions = extend({}, DEFAULT_ACTIONS, actions);
259 |
260 | function extractParams(data, actionParams){
261 | var ids = {};
262 | actionParams = extend({}, paramDefaults, actionParams);
263 | forEach(actionParams, function(value, key){
264 | if (isFunction(value)) { value = value(); }
265 | ids[key] = value.charAt && value.charAt(0) == '@' ? getter(data, value.substr(1)) : value;
266 | });
267 | return ids;
268 | }
269 |
270 | function DjangoRESTResource(value){
271 | copy(value || {}, this);
272 | }
273 |
274 | forEach(actions, function(action, name) {
275 | action.method = angular.uppercase(action.method);
276 | var hasBody = action.method == 'POST' || action.method == 'PUT' || action.method == 'PATCH';
277 | DjangoRESTResource[name] = function(a1, a2, a3, a4) {
278 | var params = {};
279 | var data;
280 | var success = noop;
281 | var error = null;
282 | var promise;
283 |
284 | switch(arguments.length) {
285 | case 4:
286 | error = a4;
287 | success = a3;
288 | //fallthrough
289 | case 3:
290 | case 2:
291 | if (isFunction(a2)) {
292 | if (isFunction(a1)) {
293 | success = a1;
294 | error = a2;
295 | break;
296 | }
297 |
298 | success = a2;
299 | error = a3;
300 | //fallthrough
301 | } else {
302 | params = a1;
303 | data = a2;
304 | success = a3;
305 | break;
306 | }
307 | case 1:
308 | if (isFunction(a1)) success = a1;
309 | else if (hasBody) data = a1;
310 | else params = a1;
311 | break;
312 | case 0: break;
313 | default:
314 | throw "Expected between 0-4 arguments [params, data, success, error], got " +
315 | arguments.length + " arguments.";
316 | }
317 |
318 | var value = this instanceof DjangoRESTResource ? this : (action.isArray ? [] : new DjangoRESTResource(data));
319 | var httpConfig = {},
320 | promise;
321 |
322 | forEach(action, function(value, key) {
323 | if (key == 'method' && action.hasOwnProperty('method_if_field_has_value')) {
324 | // Check if the action's HTTP method is dependent on a field holding a value ('id' for example)
325 | var field = action.method_if_field_has_value[0];
326 | var fieldDependentMethod = action.method_if_field_has_value[1];
327 | httpConfig.method =
328 | (data.hasOwnProperty(field) && data[field] !== null) ? fieldDependentMethod : action.method;
329 | } else if (key != 'params' && key != 'isArray' ) {
330 | httpConfig[key] = copy(value);
331 | }
332 | });
333 | httpConfig.data = data;
334 | route.setUrlParams(httpConfig, extend({}, extractParams(data, action.params || {}), params), action.url);
335 |
336 | function markResolved() { value.$resolved = true; }
337 |
338 | promise = $http(httpConfig);
339 | value.$resolved = false;
340 |
341 | promise.then(markResolved, markResolved);
342 | value.$then = promise.then(function(response) {
343 | // Success wrapper
344 |
345 | var data = response.data;
346 | var then = value.$then, resolved = value.$resolved;
347 |
348 | var deferSuccess = false;
349 |
350 | if (data) {
351 | if (action.isArray) {
352 | value.length = 0;
353 |
354 | // If it's an object with count and results, it's a pagination container, not an array:
355 | if (data.hasOwnProperty("count") && data.hasOwnProperty("results")) {
356 | // Don't call success callback until the last page has been accepted:
357 | deferSuccess = true;
358 |
359 | var paginator = function recursivePaginator(data) {
360 | // If there is a next page, go ahead and request it before parsing our results. Less wasted time.
361 | if (data.next !== null) {
362 | var next_config = copy(httpConfig);
363 | next_config.params = {};
364 | next_config.url = data.next;
365 | $http(next_config).success(function(next_data) { recursivePaginator(next_data); }).error(error);
366 | }
367 | // Ok, now load this page's results:
368 | forEach(data.results, function(item) {
369 | value.push(new DjangoRESTResource(item));
370 | });
371 | if (data.next == null) {
372 | // We've reached the last page, call the original success callback with the concatenated pages of data.
373 | (success||noop)(value, response.headers);
374 | }
375 | };
376 | paginator(data);
377 | } else {
378 | //Not paginated, push into array as normal.
379 | forEach(data, function(item) {
380 | value.push(new DjangoRESTResource(item));
381 | });
382 | }
383 | } else {
384 | // Not an isArray action
385 | copy(data, value);
386 |
387 | // Copy operation destroys value's original properties, so restore some of the old ones:
388 | value.$then = then;
389 | value.$resolved = resolved;
390 | value.$promise = promise;
391 | }
392 | }
393 |
394 | if (!deferSuccess) {
395 | (success||noop)(value, response.headers);
396 | }
397 |
398 | response.resource = value;
399 | return response;
400 | }, error).then;
401 |
402 | return value;
403 | };
404 |
405 |
406 | DjangoRESTResource.prototype['$' + name] = function(a1, a2, a3) {
407 | var params = extractParams(this),
408 | success = noop,
409 | error;
410 |
411 | switch(arguments.length) {
412 | case 3: params = a1; success = a2; error = a3; break;
413 | case 2:
414 | case 1:
415 | if (isFunction(a1)) {
416 | success = a1;
417 | error = a2;
418 | } else {
419 | params = a1;
420 | success = a2 || noop;
421 | }
422 | case 0: break;
423 | default:
424 | throw "Expected between 1-3 arguments [params, success, error], got " +
425 | arguments.length + " arguments.";
426 | }
427 | var data = hasBody ? this : undefined;
428 | return DjangoRESTResource[name].call(this, params, data, success, error);
429 | };
430 | });
431 |
432 | DjangoRESTResource.bind = function(additionalParamDefaults){
433 | return DjangoRESTResourceFactory(url, extend({}, paramDefaults, additionalParamDefaults), actions);
434 | };
435 |
436 | return DjangoRESTResource;
437 | }
438 |
439 | return DjangoRESTResourceFactory;
440 | }]);
441 |
--------------------------------------------------------------------------------
/bower.json:
--------------------------------------------------------------------------------
1 | {
2 | "name": "angular-django-rest-resource",
3 | "main": "angular-django-rest-resource.js",
4 | "version": "1.0.3",
5 | "homepage": "https://github.com/blacklocus/angular-django-rest-resource",
6 | "authors": [
7 | "BlackLocus",
8 | "Justin Turner Arthur",
9 | "Contributors"
10 | ],
11 | "description": "An AngularJS module that provides a resource-generation service similar to ngResource, but optimized for the Django REST Framework.",
12 | "keywords": [
13 | "AngularJS",
14 | "Django",
15 | "REST",
16 | "Framework",
17 | "Django",
18 | "Angular",
19 | "paginate",
20 | "pagination"
21 | ],
22 | "license": "MIT",
23 | "ignore": [
24 | "**/.*",
25 | "**/.md",
26 | "node_modules",
27 | "bower_components",
28 | "test",
29 | "tests"
30 | ]
31 | }
32 |
--------------------------------------------------------------------------------
/test/django_project/__init__.py:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/blacklocus/angular-django-rest-resource/22c0631e8af4a31cc013471a9e0f219d1c6f6b66/test/django_project/__init__.py
--------------------------------------------------------------------------------
/test/django_project/manage.py:
--------------------------------------------------------------------------------
1 | #!/usr/bin/env python
2 | import os
3 | import sys
4 |
5 | if __name__ == "__main__":
6 | os.environ.setdefault("DJANGO_SETTINGS_MODULE", "settings")
7 |
8 | from django.core.management import execute_from_command_line
9 |
10 | execute_from_command_line(sys.argv)
11 |
--------------------------------------------------------------------------------
/test/django_project/settings.py:
--------------------------------------------------------------------------------
1 | """
2 | Django settings for Angular_Django_REST_Resource_Test project.
3 |
4 | For more information on this file, see
5 | https://docs.djangoproject.com/en/1.6/topics/settings/
6 |
7 | For the full list of settings and their values, see
8 | https://docs.djangoproject.com/en/1.6/ref/settings/
9 | """
10 |
11 | # Build paths inside the project like this: os.path.join(BASE_DIR, ...)
12 | import os
13 | BASE_DIR = os.path.dirname(os.path.dirname(__file__))
14 |
15 |
16 | # Quick-start development settings - unsuitable for production
17 | # See https://docs.djangoproject.com/en/1.6/howto/deployment/checklist/
18 |
19 | # SECURITY WARNING: keep the secret key used in production secret!
20 | SECRET_KEY = '=eimp4enwyw-z5opxcmt$bvpycx0bu@mkb=-^0r+&iw+-)6xts'
21 |
22 | # SECURITY WARNING: don't run with debug turned on in production!
23 | DEBUG = True
24 |
25 | TEMPLATE_DEBUG = True
26 |
27 | ALLOWED_HOSTS = []
28 |
29 |
30 | # Application definition
31 |
32 | INSTALLED_APPS = (
33 | 'django.contrib.admin',
34 | 'django.contrib.auth',
35 | 'django.contrib.contenttypes',
36 | 'django.contrib.sessions',
37 | 'django.contrib.messages',
38 | 'django.contrib.staticfiles',
39 | 'rest_framework',
40 | 'test_rest',
41 | )
42 |
43 | MIDDLEWARE_CLASSES = (
44 | 'django.contrib.sessions.middleware.SessionMiddleware',
45 | 'django.middleware.common.CommonMiddleware',
46 | 'django.middleware.csrf.CsrfViewMiddleware',
47 | 'django.contrib.auth.middleware.AuthenticationMiddleware',
48 | 'django.contrib.messages.middleware.MessageMiddleware',
49 | 'django.middleware.clickjacking.XFrameOptionsMiddleware',
50 | )
51 |
52 | ROOT_URLCONF = 'urls'
53 |
54 | WSGI_APPLICATION = 'wsgi.application'
55 |
56 |
57 | # Database
58 | # https://docs.djangoproject.com/en/1.6/ref/settings/#databases
59 |
60 | DATABASES = {
61 | 'default': {
62 | 'ENGINE': 'django.db.backends.sqlite3',
63 | 'NAME': os.path.join(BASE_DIR, 'db.sqlite3'),
64 | }
65 | }
66 |
67 | # Internationalization
68 | # https://docs.djangoproject.com/en/1.6/topics/i18n/
69 |
70 | LANGUAGE_CODE = 'en-us'
71 |
72 | TIME_ZONE = 'UTC'
73 |
74 | USE_I18N = True
75 |
76 | USE_L10N = True
77 |
78 | USE_TZ = True
79 |
80 |
81 | # Static files (CSS, JavaScript, Images)
82 | # https://docs.djangoproject.com/en/1.6/howto/static-files/
83 |
84 | STATIC_URL = '/static/'
85 |
86 | REST_FRAMEWORK = {
87 | 'PAGINATE_BY': 5
88 | }
--------------------------------------------------------------------------------
/test/django_project/test_rest/__init__.py:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/blacklocus/angular-django-rest-resource/22c0631e8af4a31cc013471a9e0f219d1c6f6b66/test/django_project/test_rest/__init__.py
--------------------------------------------------------------------------------
/test/django_project/test_rest/admin.py:
--------------------------------------------------------------------------------
1 | from django.contrib import admin
2 |
3 | from test_rest.models import Plant, Animal
4 |
5 | # Register your models here.
6 | admin.site.register(Plant)
7 | admin.site.register(Animal)
--------------------------------------------------------------------------------
/test/django_project/test_rest/fixtures/initial_data.json:
--------------------------------------------------------------------------------
1 | [
2 | {
3 | "pk": 1,
4 | "model": "test_rest.plant",
5 | "fields": {
6 | "common_name": "Venus Flytrap",
7 | "scientific_name": "Dionaea muscipula",
8 | "is_evergreen": false,
9 | "is_succulent": false
10 | }
11 | },
12 | {
13 | "pk": 2,
14 | "model": "test_rest.plant",
15 | "fields": {
16 | "common_name": "Sensitive Plant",
17 | "scientific_name": "Mimosa pudica",
18 | "is_evergreen": false,
19 | "is_succulent": false
20 | }
21 | }
22 | ]
--------------------------------------------------------------------------------
/test/django_project/test_rest/models.py:
--------------------------------------------------------------------------------
1 | from django.db import models
2 |
3 |
4 | class Plant(models.Model):
5 | common_name = models.CharField(max_length=512)
6 | scientific_name = models.CharField(max_length=1024)
7 | is_succulent = models.NullBooleanField()
8 | is_evergreen = models.NullBooleanField()
9 |
10 | def __unicode__(self):
11 | return self.common_name
12 |
13 |
14 | class Animal(models.Model):
15 | common_name = models.CharField(max_length=512)
16 | scientific_name = models.CharField(max_length=1024)
17 | is_mammal = models.NullBooleanField()
18 | is_reptile = models.NullBooleanField()
19 | is_arachnid = models.NullBooleanField()
20 |
21 | def __unicode__(self):
22 | return self.common_name
--------------------------------------------------------------------------------
/test/django_project/test_rest/serializers.py:
--------------------------------------------------------------------------------
1 | from rest_framework import serializers
2 | from test_rest.models import Animal
3 | from test_rest.models import Plant
4 |
5 |
6 | class AnimalSerializer(serializers.ModelSerializer):
7 | class Meta:
8 | model = Animal
9 |
10 |
11 | class PlantSerializer(serializers.ModelSerializer):
12 | class Meta:
13 | model = Plant
14 |
--------------------------------------------------------------------------------
/test/django_project/test_rest/static/index.html:
--------------------------------------------------------------------------------
1 |
2 |
3 | Angular Django REST Resource Text
4 |
11 |
12 |
13 |
24 |
25 |
26 |
27 | Animals retrieved from the server so far: ({{ animals.length }}):
28 |
29 | - {{animal.common_name}}
30 |
31 | Plants retrieved from the server so far: ({{ plants.length }}):
32 |
33 | - {{plant.common_name}}
34 |
35 |
36 |
44 |
45 |
53 |
54 |
55 |
--------------------------------------------------------------------------------
/test/django_project/test_rest/tests.py:
--------------------------------------------------------------------------------
1 | from django.test import TestCase
2 |
3 | # Create your tests here.
4 |
--------------------------------------------------------------------------------
/test/django_project/test_rest/views.py:
--------------------------------------------------------------------------------
1 | from rest_framework import viewsets
2 | from test_rest.models import Animal, Plant
3 | from test_rest.serializers import AnimalSerializer, PlantSerializer
4 |
5 |
6 | class AnimalViewSet(viewsets.ModelViewSet):
7 | queryset = Animal.objects.all()
8 | serializer_class = AnimalSerializer
9 |
10 |
11 | class PlantViewSet(viewsets.ModelViewSet):
12 | queryset = Plant.objects.all()
13 | serializer_class = PlantSerializer
--------------------------------------------------------------------------------
/test/django_project/urls.py:
--------------------------------------------------------------------------------
1 | from django.conf import settings
2 | from django.conf.urls import patterns, include, url
3 | from django.views.generic.base import RedirectView
4 | from rest_framework import routers
5 | from test_rest import views
6 | from os import path
7 |
8 | from django.contrib import admin
9 | admin.autodiscover()
10 |
11 | rest_router = routers.DefaultRouter()
12 | rest_router.register(r'animals', views.AnimalViewSet)
13 | rest_router.register(r'plants', views.PlantViewSet)
14 |
15 | urlpatterns = patterns('',
16 | url(r'^$', RedirectView.as_view(url="/static/index.html")),
17 | url(r'^(?Pangular-django-rest-resource\.js)$', 'django.views.static.serve', {
18 | 'document_root': path.join(settings.BASE_DIR, ".."),
19 | }),
20 | url(r'^admin/', include(admin.site.urls)),
21 | url(r'^', include(rest_router.urls)),
22 | )
--------------------------------------------------------------------------------
/test/django_project/wsgi.py:
--------------------------------------------------------------------------------
1 | """
2 | WSGI config for Angular_Django_REST_Resource_Test project.
3 |
4 | It exposes the WSGI callable as a module-level variable named ``application``.
5 |
6 | For more information on this file, see
7 | https://docs.djangoproject.com/en/1.6/howto/deployment/wsgi/
8 | """
9 |
10 | import os
11 | os.environ.setdefault("DJANGO_SETTINGS_MODULE", "settings")
12 |
13 | from django.core.wsgi import get_wsgi_application
14 | application = get_wsgi_application()
15 |
--------------------------------------------------------------------------------
/test/requirements.txt:
--------------------------------------------------------------------------------
1 | Django==1.6.5
2 | django-filter==0.7
3 | djangorestframework==2.3.13
4 | wsgiref==0.1.2
5 |
--------------------------------------------------------------------------------