├── LICENSE ├── Readme.md ├── ch2-library-app-and-api ├── Pipfile ├── Pipfile.lock ├── api │ ├── __init__.py │ ├── admin.py │ ├── apps.py │ ├── migrations │ │ └── __init__.py │ ├── models.py │ ├── serializers.py │ ├── tests.py │ ├── urls.py │ └── views.py ├── books │ ├── __init__.py │ ├── admin.py │ ├── apps.py │ ├── migrations │ │ ├── 0001_initial.py │ │ └── __init__.py │ ├── models.py │ ├── templates │ │ └── books │ │ │ └── book_list.html │ ├── tests.py │ ├── urls.py │ └── views.py ├── config │ ├── __init__.py │ ├── asgi.py │ ├── settings.py │ ├── urls.py │ └── wsgi.py ├── db.sqlite3 └── manage.py ├── ch3-todo-api ├── Pipfile.lock └── backend │ ├── Pipfile │ ├── config │ ├── __init__.py │ ├── asgi.py │ ├── settings.py │ ├── urls.py │ └── wsgi.py │ ├── db.sqlite3 │ ├── manage.py │ └── todos │ ├── __init__.py │ ├── admin.py │ ├── apps.py │ ├── migrations │ ├── 0001_initial.py │ └── __init__.py │ ├── models.py │ ├── serializers.py │ ├── tests.py │ ├── urls.py │ └── views.py ├── ch4-todo-api-frontend └── backend │ ├── Pipfile │ ├── Pipfile.lock │ ├── config │ ├── __init__.py │ ├── asgi.py │ ├── settings.py │ ├── urls.py │ └── wsgi.py │ ├── db.sqlite3 │ ├── manage.py │ └── todos │ ├── __init__.py │ ├── admin.py │ ├── apps.py │ ├── migrations │ ├── 0001_initial.py │ └── __init__.py │ ├── models.py │ ├── serializers.py │ ├── tests.py │ ├── urls.py │ └── views.py ├── ch5-blog-api ├── Pipfile ├── Pipfile.lock ├── config │ ├── __init__.py │ ├── asgi.py │ ├── settings.py │ ├── urls.py │ └── wsgi.py ├── db.sqlite3 ├── manage.py └── posts │ ├── __init__.py │ ├── admin.py │ ├── apps.py │ ├── migrations │ ├── 0001_initial.py │ └── __init__.py │ ├── models.py │ ├── serializers.py │ ├── tests.py │ ├── urls.py │ └── views.py ├── ch6-blog-api-permissions ├── Pipfile ├── Pipfile.lock ├── config │ ├── __init__.py │ ├── asgi.py │ ├── settings.py │ ├── urls.py │ └── wsgi.py ├── db.sqlite3 ├── manage.py └── posts │ ├── __init__.py │ ├── admin.py │ ├── apps.py │ ├── migrations │ ├── 0001_initial.py │ └── __init__.py │ ├── models.py │ ├── permissions.py │ ├── serializers.py │ ├── tests.py │ ├── urls.py │ └── views.py ├── ch7-user-authentication ├── Pipfile ├── Pipfile.lock ├── config │ ├── __init__.py │ ├── asgi.py │ ├── settings.py │ ├── urls.py │ └── wsgi.py ├── db.sqlite3 ├── manage.py └── posts │ ├── __init__.py │ ├── admin.py │ ├── apps.py │ ├── migrations │ ├── 0001_initial.py │ └── __init__.py │ ├── models.py │ ├── permissions.py │ ├── serializers.py │ ├── tests.py │ ├── urls.py │ └── views.py ├── ch8-viewsets-and-routers ├── Pipfile ├── Pipfile.lock ├── config │ ├── __init__.py │ ├── asgi.py │ ├── settings.py │ ├── urls.py │ └── wsgi.py ├── db.sqlite3 ├── manage.py └── posts │ ├── __init__.py │ ├── admin.py │ ├── apps.py │ ├── migrations │ ├── 0001_initial.py │ └── __init__.py │ ├── models.py │ ├── permissions.py │ ├── serializers.py │ ├── tests.py │ ├── urls.py │ └── views.py ├── ch9-schemas-and-documentation ├── Pipfile ├── Pipfile.lock ├── config │ ├── __init__.py │ ├── asgi.py │ ├── settings.py │ ├── urls.py │ └── wsgi.py ├── db.sqlite3 ├── manage.py ├── openapi-schema.yml └── posts │ ├── __init__.py │ ├── admin.py │ ├── apps.py │ ├── migrations │ ├── 0001_initial.py │ └── __init__.py │ ├── models.py │ ├── permissions.py │ ├── serializers.py │ ├── tests.py │ ├── urls.py │ └── views.py └── cover.jpg /LICENSE: -------------------------------------------------------------------------------- 1 | MIT License 2 | 3 | Copyright (c) 2018 William S. Vincent 4 | 5 | Permission is hereby granted, free of charge, to any person obtaining a copy 6 | of this software and associated documentation files (the "Software"), to deal 7 | in the Software without restriction, including without limitation the rights 8 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 9 | copies of the Software, and to permit persons to whom the Software is 10 | furnished to do so, subject to the following conditions: 11 | 12 | The above copyright notice and this permission notice shall be included in all 13 | copies or substantial portions of the Software. 14 | 15 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 16 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 17 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 18 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 19 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 20 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE 21 | SOFTWARE. 22 | -------------------------------------------------------------------------------- /Readme.md: -------------------------------------------------------------------------------- 1 | The official source code for [Django for APIs](https://djangoforapis.com). Available as an [ebook](https://gum.co/EzsI) or [paperback](https://www.amazon.com/dp/1735467227/?tag=wsvincent-20). 2 | 3 | ![Cover](cover.jpg) 4 | 5 | If you have the 3.0 version, please refer to [this repo for the source code](https://github.com/wsvincent/djangoforapis_30). 6 | -------------------------------------------------------------------------------- /ch2-library-app-and-api/Pipfile: -------------------------------------------------------------------------------- 1 | [[source]] 2 | name = "pypi" 3 | url = "https://pypi.org/simple" 4 | verify_ssl = true 5 | 6 | [dev-packages] 7 | 8 | [packages] 9 | django = "~=3.1.0" 10 | djangorestframework = "==3.11.0" 11 | 12 | [requires] 13 | python_version = "3.8" 14 | -------------------------------------------------------------------------------- /ch2-library-app-and-api/Pipfile.lock: -------------------------------------------------------------------------------- 1 | { 2 | "_meta": { 3 | "hash": { 4 | "sha256": "d50f113a63968a27a3bb3975146f499e12e0e53154a5f727aef7fd473504f888" 5 | }, 6 | "pipfile-spec": 6, 7 | "requires": { 8 | "python_version": "3.8" 9 | }, 10 | "sources": [ 11 | { 12 | "name": "pypi", 13 | "url": "https://pypi.org/simple", 14 | "verify_ssl": true 15 | } 16 | ] 17 | }, 18 | "default": { 19 | "asgiref": { 20 | "hashes": [ 21 | "sha256:7e51911ee147dd685c3c8b805c0ad0cb58d360987b56953878f8c06d2d1c6f1a", 22 | "sha256:9fc6fb5d39b8af147ba40765234fa822b39818b12cc80b35ad9b0cef3a476aed" 23 | ], 24 | "markers": "python_version >= '3.5'", 25 | "version": "==3.2.10" 26 | }, 27 | "django": { 28 | "hashes": [ 29 | "sha256:1a63f5bb6ff4d7c42f62a519edc2adbb37f9b78068a5a862beff858b68e3dc8b", 30 | "sha256:2d390268a13c655c97e0e2ede9d117007996db692c1bb93eabebd4fb7ea7012b" 31 | ], 32 | "index": "pypi", 33 | "version": "==3.1" 34 | }, 35 | "djangorestframework": { 36 | "hashes": [ 37 | "sha256:05809fc66e1c997fd9a32ea5730d9f4ba28b109b9da71fccfa5ff241201fd0a4", 38 | "sha256:e782087823c47a26826ee5b6fa0c542968219263fb3976ec3c31edab23a4001f" 39 | ], 40 | "index": "pypi", 41 | "version": "==3.11.0" 42 | }, 43 | "pytz": { 44 | "hashes": [ 45 | "sha256:a494d53b6d39c3c6e44c3bec237336e14305e4f29bbf800b599253057fbb79ed", 46 | "sha256:c35965d010ce31b23eeb663ed3cc8c906275d6be1a34393a1d73a41febf4a048" 47 | ], 48 | "version": "==2020.1" 49 | }, 50 | "sqlparse": { 51 | "hashes": [ 52 | "sha256:022fb9c87b524d1f7862b3037e541f68597a730a8843245c349fc93e1643dc4e", 53 | "sha256:e162203737712307dfe78860cc56c8da8a852ab2ee33750e33aeadf38d12c548" 54 | ], 55 | "markers": "python_version >= '2.7' and python_version not in '3.0, 3.1, 3.2, 3.3'", 56 | "version": "==0.3.1" 57 | } 58 | }, 59 | "develop": {} 60 | } 61 | -------------------------------------------------------------------------------- /ch2-library-app-and-api/api/__init__.py: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/wsvincent/djangoforapis_31/c678537076d809f4fea19f0beb5e20e3be8661e2/ch2-library-app-and-api/api/__init__.py -------------------------------------------------------------------------------- /ch2-library-app-and-api/api/admin.py: -------------------------------------------------------------------------------- 1 | from django.contrib import admin 2 | 3 | # Register your models here. 4 | -------------------------------------------------------------------------------- /ch2-library-app-and-api/api/apps.py: -------------------------------------------------------------------------------- 1 | from django.apps import AppConfig 2 | 3 | 4 | class ApiConfig(AppConfig): 5 | name = 'api' 6 | -------------------------------------------------------------------------------- /ch2-library-app-and-api/api/migrations/__init__.py: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/wsvincent/djangoforapis_31/c678537076d809f4fea19f0beb5e20e3be8661e2/ch2-library-app-and-api/api/migrations/__init__.py -------------------------------------------------------------------------------- /ch2-library-app-and-api/api/models.py: -------------------------------------------------------------------------------- 1 | from django.db import models 2 | 3 | # Create your models here. 4 | -------------------------------------------------------------------------------- /ch2-library-app-and-api/api/serializers.py: -------------------------------------------------------------------------------- 1 | from rest_framework import serializers 2 | from books.models import Book 3 | 4 | 5 | class BookSerializer(serializers.ModelSerializer): 6 | class Meta: 7 | model = Book 8 | fields = ('title', 'subtitle', 'author', 'isbn') 9 | -------------------------------------------------------------------------------- /ch2-library-app-and-api/api/tests.py: -------------------------------------------------------------------------------- 1 | from django.test import TestCase 2 | 3 | # Create your tests here. 4 | -------------------------------------------------------------------------------- /ch2-library-app-and-api/api/urls.py: -------------------------------------------------------------------------------- 1 | from django.urls import path 2 | from .views import BookAPIView 3 | 4 | urlpatterns = [ 5 | path('', BookAPIView.as_view()), 6 | ] 7 | -------------------------------------------------------------------------------- /ch2-library-app-and-api/api/views.py: -------------------------------------------------------------------------------- 1 | from rest_framework import generics 2 | from books.models import Book 3 | from .serializers import BookSerializer 4 | 5 | 6 | class BookAPIView(generics.ListAPIView): 7 | queryset = Book.objects.all() 8 | serializer_class = BookSerializer 9 | -------------------------------------------------------------------------------- /ch2-library-app-and-api/books/__init__.py: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/wsvincent/djangoforapis_31/c678537076d809f4fea19f0beb5e20e3be8661e2/ch2-library-app-and-api/books/__init__.py -------------------------------------------------------------------------------- /ch2-library-app-and-api/books/admin.py: -------------------------------------------------------------------------------- 1 | from django.contrib import admin 2 | from .models import Book 3 | 4 | admin.site.register(Book) 5 | -------------------------------------------------------------------------------- /ch2-library-app-and-api/books/apps.py: -------------------------------------------------------------------------------- 1 | from django.apps import AppConfig 2 | 3 | 4 | class BooksConfig(AppConfig): 5 | name = 'books' 6 | -------------------------------------------------------------------------------- /ch2-library-app-and-api/books/migrations/0001_initial.py: -------------------------------------------------------------------------------- 1 | # Generated by Django 3.1rc1 on 2020-07-28 13:47 2 | 3 | from django.db import migrations, models 4 | 5 | 6 | class Migration(migrations.Migration): 7 | 8 | initial = True 9 | 10 | dependencies = [ 11 | ] 12 | 13 | operations = [ 14 | migrations.CreateModel( 15 | name='Book', 16 | fields=[ 17 | ('id', models.AutoField(auto_created=True, primary_key=True, serialize=False, verbose_name='ID')), 18 | ('title', models.CharField(max_length=250)), 19 | ('subtitle', models.CharField(max_length=250)), 20 | ('author', models.CharField(max_length=100)), 21 | ('isbn', models.CharField(max_length=13)), 22 | ], 23 | ), 24 | ] 25 | -------------------------------------------------------------------------------- /ch2-library-app-and-api/books/migrations/__init__.py: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/wsvincent/djangoforapis_31/c678537076d809f4fea19f0beb5e20e3be8661e2/ch2-library-app-and-api/books/migrations/__init__.py -------------------------------------------------------------------------------- /ch2-library-app-and-api/books/models.py: -------------------------------------------------------------------------------- 1 | from django.db import models 2 | 3 | class Book(models.Model): 4 | title = models.CharField(max_length=250) 5 | subtitle = models.CharField(max_length=250) 6 | author = models.CharField(max_length=100) 7 | isbn = models.CharField(max_length=13) 8 | 9 | def __str__(self): 10 | return self.title 11 | -------------------------------------------------------------------------------- /ch2-library-app-and-api/books/templates/books/book_list.html: -------------------------------------------------------------------------------- 1 |

All books

2 | {% for book in object_list %} 3 | 9 | {% endfor %} 10 | -------------------------------------------------------------------------------- /ch2-library-app-and-api/books/tests.py: -------------------------------------------------------------------------------- 1 | from django.test import TestCase 2 | 3 | # Create your tests here. 4 | -------------------------------------------------------------------------------- /ch2-library-app-and-api/books/urls.py: -------------------------------------------------------------------------------- 1 | from django.urls import path 2 | from .views import BookListView 3 | 4 | urlpatterns = [ 5 | path('', BookListView.as_view(), name='home'), 6 | ] 7 | -------------------------------------------------------------------------------- /ch2-library-app-and-api/books/views.py: -------------------------------------------------------------------------------- 1 | from django.views.generic import ListView 2 | from .models import Book 3 | 4 | 5 | class BookListView(ListView): 6 | model = Book 7 | template_name = 'book_list.html' 8 | -------------------------------------------------------------------------------- /ch2-library-app-and-api/config/__init__.py: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/wsvincent/djangoforapis_31/c678537076d809f4fea19f0beb5e20e3be8661e2/ch2-library-app-and-api/config/__init__.py -------------------------------------------------------------------------------- /ch2-library-app-and-api/config/asgi.py: -------------------------------------------------------------------------------- 1 | """ 2 | ASGI config for config project. 3 | 4 | It exposes the ASGI callable as a module-level variable named ``application``. 5 | 6 | For more information on this file, see 7 | https://docs.djangoproject.com/en/dev/howto/deployment/asgi/ 8 | """ 9 | 10 | import os 11 | 12 | from django.core.asgi import get_asgi_application 13 | 14 | os.environ.setdefault('DJANGO_SETTINGS_MODULE', 'config.settings') 15 | 16 | application = get_asgi_application() 17 | -------------------------------------------------------------------------------- /ch2-library-app-and-api/config/settings.py: -------------------------------------------------------------------------------- 1 | from pathlib import Path 2 | 3 | # Build paths inside the project like this: BASE_DIR / 'subdir'. 4 | BASE_DIR = Path(__file__).resolve(strict=True).parent.parent 5 | 6 | 7 | # Quick-start development settings - unsuitable for production 8 | # See https://docs.djangoproject.com/en/dev/howto/deployment/checklist/ 9 | 10 | # SECURITY WARNING: keep the secret key used in production secret! 11 | SECRET_KEY = 'qnhe=6s_paq+kd_#mvyi!&6xn!&o&0m)#i!18u-#w^8n9%lwj%' 12 | 13 | # SECURITY WARNING: don't run with debug turned on in production! 14 | DEBUG = True 15 | 16 | ALLOWED_HOSTS = [] 17 | 18 | 19 | # Application definition 20 | 21 | INSTALLED_APPS = [ 22 | 'django.contrib.admin', 23 | 'django.contrib.auth', 24 | 'django.contrib.contenttypes', 25 | 'django.contrib.sessions', 26 | 'django.contrib.messages', 27 | 'django.contrib.staticfiles', 28 | 29 | # 3rd party 30 | 'rest_framework', # new 31 | 32 | # Local 33 | 'books', 34 | 'api', # new 35 | ] 36 | 37 | MIDDLEWARE = [ 38 | 'django.middleware.security.SecurityMiddleware', 39 | 'django.contrib.sessions.middleware.SessionMiddleware', 40 | 'django.middleware.common.CommonMiddleware', 41 | 'django.middleware.csrf.CsrfViewMiddleware', 42 | 'django.contrib.auth.middleware.AuthenticationMiddleware', 43 | 'django.contrib.messages.middleware.MessageMiddleware', 44 | 'django.middleware.clickjacking.XFrameOptionsMiddleware', 45 | ] 46 | 47 | ROOT_URLCONF = 'config.urls' 48 | 49 | TEMPLATES = [ 50 | { 51 | 'BACKEND': 'django.template.backends.django.DjangoTemplates', 52 | 'DIRS': [], 53 | 'APP_DIRS': True, 54 | 'OPTIONS': { 55 | 'context_processors': [ 56 | 'django.template.context_processors.debug', 57 | 'django.template.context_processors.request', 58 | 'django.contrib.auth.context_processors.auth', 59 | 'django.contrib.messages.context_processors.messages', 60 | ], 61 | }, 62 | }, 63 | ] 64 | 65 | WSGI_APPLICATION = 'config.wsgi.application' 66 | 67 | 68 | # Database 69 | # https://docs.djangoproject.com/en/dev/ref/settings/#databases 70 | 71 | DATABASES = { 72 | 'default': { 73 | 'ENGINE': 'django.db.backends.sqlite3', 74 | 'NAME': BASE_DIR / 'db.sqlite3', 75 | } 76 | } 77 | 78 | 79 | # Password validation 80 | # https://docs.djangoproject.com/en/dev/ref/settings/#auth-password-validators 81 | 82 | AUTH_PASSWORD_VALIDATORS = [ 83 | { 84 | 'NAME': 'django.contrib.auth.password_validation.UserAttributeSimilarityValidator', 85 | }, 86 | { 87 | 'NAME': 'django.contrib.auth.password_validation.MinimumLengthValidator', 88 | }, 89 | { 90 | 'NAME': 'django.contrib.auth.password_validation.CommonPasswordValidator', 91 | }, 92 | { 93 | 'NAME': 'django.contrib.auth.password_validation.NumericPasswordValidator', 94 | }, 95 | ] 96 | 97 | 98 | # Internationalization 99 | # https://docs.djangoproject.com/en/dev/topics/i18n/ 100 | 101 | LANGUAGE_CODE = 'en-us' 102 | 103 | TIME_ZONE = 'UTC' 104 | 105 | USE_I18N = True 106 | 107 | USE_L10N = True 108 | 109 | USE_TZ = True 110 | 111 | 112 | # Static files (CSS, JavaScript, Images) 113 | # https://docs.djangoproject.com/en/dev/howto/static-files/ 114 | 115 | STATIC_URL = '/static/' 116 | -------------------------------------------------------------------------------- /ch2-library-app-and-api/config/urls.py: -------------------------------------------------------------------------------- 1 | from django.contrib import admin 2 | from django.urls import path, include # new 3 | 4 | urlpatterns = [ 5 | path('admin/', admin.site.urls), 6 | path('api/', include('api.urls')), # new 7 | path('', include('books.urls')), # new 8 | ] 9 | -------------------------------------------------------------------------------- /ch2-library-app-and-api/config/wsgi.py: -------------------------------------------------------------------------------- 1 | """ 2 | WSGI config for config 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/dev/howto/deployment/wsgi/ 8 | """ 9 | 10 | import os 11 | 12 | from django.core.wsgi import get_wsgi_application 13 | 14 | os.environ.setdefault('DJANGO_SETTINGS_MODULE', 'config.settings') 15 | 16 | application = get_wsgi_application() 17 | -------------------------------------------------------------------------------- /ch2-library-app-and-api/db.sqlite3: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/wsvincent/djangoforapis_31/c678537076d809f4fea19f0beb5e20e3be8661e2/ch2-library-app-and-api/db.sqlite3 -------------------------------------------------------------------------------- /ch2-library-app-and-api/manage.py: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env python 2 | """Django's command-line utility for administrative tasks.""" 3 | import os 4 | import sys 5 | 6 | 7 | def main(): 8 | """Run administrative tasks.""" 9 | os.environ.setdefault('DJANGO_SETTINGS_MODULE', 'config.settings') 10 | try: 11 | from django.core.management import execute_from_command_line 12 | except ImportError as exc: 13 | raise ImportError( 14 | "Couldn't import Django. Are you sure it's installed and " 15 | "available on your PYTHONPATH environment variable? Did you " 16 | "forget to activate a virtual environment?" 17 | ) from exc 18 | execute_from_command_line(sys.argv) 19 | 20 | 21 | if __name__ == '__main__': 22 | main() 23 | -------------------------------------------------------------------------------- /ch3-todo-api/Pipfile.lock: -------------------------------------------------------------------------------- 1 | { 2 | "_meta": { 3 | "hash": { 4 | "sha256": "e56ad8b23f3e562a4287d55b3ec7789ff5b63f5d12f82a1b47b358d16b8402d3" 5 | }, 6 | "pipfile-spec": 6, 7 | "requires": { 8 | "python_version": "3.8" 9 | }, 10 | "sources": [ 11 | { 12 | "name": "pypi", 13 | "url": "https://pypi.org/simple", 14 | "verify_ssl": true 15 | } 16 | ] 17 | }, 18 | "default": { 19 | "asgiref": { 20 | "hashes": [ 21 | "sha256:7e51911ee147dd685c3c8b805c0ad0cb58d360987b56953878f8c06d2d1c6f1a", 22 | "sha256:9fc6fb5d39b8af147ba40765234fa822b39818b12cc80b35ad9b0cef3a476aed" 23 | ], 24 | "markers": "python_version >= '3.5'", 25 | "version": "==3.2.10" 26 | }, 27 | "django": { 28 | "hashes": [ 29 | "sha256:1a63f5bb6ff4d7c42f62a519edc2adbb37f9b78068a5a862beff858b68e3dc8b", 30 | "sha256:2d390268a13c655c97e0e2ede9d117007996db692c1bb93eabebd4fb7ea7012b" 31 | ], 32 | "index": "pypi", 33 | "version": "==3.1" 34 | }, 35 | "django-cors-headers": { 36 | "hashes": [ 37 | "sha256:5240062ef0b16668ce8a5f43324c388d65f5439e1a30e22c38684d5ddaff0d15", 38 | "sha256:f5218f2f0bb1210563ff87687afbf10786e080d8494a248e705507ebd92d7153" 39 | ], 40 | "index": "pypi", 41 | "version": "==3.4.0" 42 | }, 43 | "djangorestframework": { 44 | "hashes": [ 45 | "sha256:05809fc66e1c997fd9a32ea5730d9f4ba28b109b9da71fccfa5ff241201fd0a4", 46 | "sha256:e782087823c47a26826ee5b6fa0c542968219263fb3976ec3c31edab23a4001f" 47 | ], 48 | "index": "pypi", 49 | "version": "==3.11.0" 50 | }, 51 | "pytz": { 52 | "hashes": [ 53 | "sha256:a494d53b6d39c3c6e44c3bec237336e14305e4f29bbf800b599253057fbb79ed", 54 | "sha256:c35965d010ce31b23eeb663ed3cc8c906275d6be1a34393a1d73a41febf4a048" 55 | ], 56 | "version": "==2020.1" 57 | }, 58 | "sqlparse": { 59 | "hashes": [ 60 | "sha256:022fb9c87b524d1f7862b3037e541f68597a730a8843245c349fc93e1643dc4e", 61 | "sha256:e162203737712307dfe78860cc56c8da8a852ab2ee33750e33aeadf38d12c548" 62 | ], 63 | "markers": "python_version >= '2.7' and python_version not in '3.0, 3.1, 3.2, 3.3'", 64 | "version": "==0.3.1" 65 | } 66 | }, 67 | "develop": {} 68 | } 69 | -------------------------------------------------------------------------------- /ch3-todo-api/backend/Pipfile: -------------------------------------------------------------------------------- 1 | [[source]] 2 | name = "pypi" 3 | url = "https://pypi.org/simple" 4 | verify_ssl = true 5 | 6 | [dev-packages] 7 | 8 | [packages] 9 | django = "==3.1rc1" 10 | djangorestframework = "==3.11.0" 11 | django-cors-headers = "==3.4.0" 12 | 13 | [requires] 14 | python_version = "3.8" 15 | -------------------------------------------------------------------------------- /ch3-todo-api/backend/config/__init__.py: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/wsvincent/djangoforapis_31/c678537076d809f4fea19f0beb5e20e3be8661e2/ch3-todo-api/backend/config/__init__.py -------------------------------------------------------------------------------- /ch3-todo-api/backend/config/asgi.py: -------------------------------------------------------------------------------- 1 | """ 2 | ASGI config for config project. 3 | 4 | It exposes the ASGI callable as a module-level variable named ``application``. 5 | 6 | For more information on this file, see 7 | https://docs.djangoproject.com/en/dev/howto/deployment/asgi/ 8 | """ 9 | 10 | import os 11 | 12 | from django.core.asgi import get_asgi_application 13 | 14 | os.environ.setdefault('DJANGO_SETTINGS_MODULE', 'config.settings') 15 | 16 | application = get_asgi_application() 17 | -------------------------------------------------------------------------------- /ch3-todo-api/backend/config/settings.py: -------------------------------------------------------------------------------- 1 | from pathlib import Path 2 | 3 | # Build paths inside the project like this: BASE_DIR / 'subdir'. 4 | BASE_DIR = Path(__file__).resolve(strict=True).parent.parent 5 | 6 | 7 | # Quick-start development settings - unsuitable for production 8 | # See https://docs.djangoproject.com/en/dev/howto/deployment/checklist/ 9 | 10 | # SECURITY WARNING: keep the secret key used in production secret! 11 | SECRET_KEY = 'y2kf+s*_9sws0agzq%gx4mw6d53xm&n^j!wf3e)=uf_^6m(@z@' 12 | 13 | # SECURITY WARNING: don't run with debug turned on in production! 14 | DEBUG = True 15 | 16 | ALLOWED_HOSTS = [] 17 | 18 | 19 | # Application definition 20 | 21 | INSTALLED_APPS = [ 22 | 'django.contrib.admin', 23 | 'django.contrib.auth', 24 | 'django.contrib.contenttypes', 25 | 'django.contrib.sessions', 26 | 'django.contrib.messages', 27 | 'django.contrib.staticfiles', 28 | 29 | # 3rd party 30 | 'rest_framework', # new 31 | 'corsheaders', # new 32 | 33 | # Local 34 | 'todos', # new 35 | ] 36 | 37 | MIDDLEWARE = [ 38 | 'django.middleware.security.SecurityMiddleware', 39 | 'django.contrib.sessions.middleware.SessionMiddleware', 40 | 'corsheaders.middleware.CorsMiddleware', # new 41 | 'django.middleware.common.CommonMiddleware', 42 | 'django.middleware.csrf.CsrfViewMiddleware', 43 | 'django.contrib.auth.middleware.AuthenticationMiddleware', 44 | 'django.contrib.messages.middleware.MessageMiddleware', 45 | 'django.middleware.clickjacking.XFrameOptionsMiddleware', 46 | ] 47 | 48 | ROOT_URLCONF = 'config.urls' 49 | 50 | TEMPLATES = [ 51 | { 52 | 'BACKEND': 'django.template.backends.django.DjangoTemplates', 53 | 'DIRS': [], 54 | 'APP_DIRS': True, 55 | 'OPTIONS': { 56 | 'context_processors': [ 57 | 'django.template.context_processors.debug', 58 | 'django.template.context_processors.request', 59 | 'django.contrib.auth.context_processors.auth', 60 | 'django.contrib.messages.context_processors.messages', 61 | ], 62 | }, 63 | }, 64 | ] 65 | 66 | WSGI_APPLICATION = 'config.wsgi.application' 67 | 68 | 69 | # Database 70 | # https://docs.djangoproject.com/en/dev/ref/settings/#databases 71 | 72 | DATABASES = { 73 | 'default': { 74 | 'ENGINE': 'django.db.backends.sqlite3', 75 | 'NAME': BASE_DIR / 'db.sqlite3', 76 | } 77 | } 78 | 79 | 80 | # Password validation 81 | # https://docs.djangoproject.com/en/dev/ref/settings/#auth-password-validators 82 | 83 | AUTH_PASSWORD_VALIDATORS = [ 84 | { 85 | 'NAME': 'django.contrib.auth.password_validation.UserAttributeSimilarityValidator', 86 | }, 87 | { 88 | 'NAME': 'django.contrib.auth.password_validation.MinimumLengthValidator', 89 | }, 90 | { 91 | 'NAME': 'django.contrib.auth.password_validation.CommonPasswordValidator', 92 | }, 93 | { 94 | 'NAME': 'django.contrib.auth.password_validation.NumericPasswordValidator', 95 | }, 96 | ] 97 | 98 | 99 | # Internationalization 100 | # https://docs.djangoproject.com/en/dev/topics/i18n/ 101 | 102 | LANGUAGE_CODE = 'en-us' 103 | 104 | TIME_ZONE = 'UTC' 105 | 106 | USE_I18N = True 107 | 108 | USE_L10N = True 109 | 110 | USE_TZ = True 111 | 112 | 113 | # Static files (CSS, JavaScript, Images) 114 | # https://docs.djangoproject.com/en/dev/howto/static-files/ 115 | 116 | STATIC_URL = '/static/' 117 | 118 | REST_FRAMEWORK = { 119 | 'DEFAULT_PERMISSION_CLASSES': [ 120 | 'rest_framework.permissions.AllowAny', 121 | ] 122 | } 123 | 124 | CORS_ORIGIN_WHITELIST = ( 125 | 'http://localhost:3000', 126 | 'http://localhost:8000', 127 | ) 128 | -------------------------------------------------------------------------------- /ch3-todo-api/backend/config/urls.py: -------------------------------------------------------------------------------- 1 | from django.contrib import admin 2 | from django.urls import include, path # new 3 | 4 | 5 | urlpatterns = [ 6 | path('admin/', admin.site.urls), 7 | path('api/', include('todos.urls')), # new 8 | ] 9 | -------------------------------------------------------------------------------- /ch3-todo-api/backend/config/wsgi.py: -------------------------------------------------------------------------------- 1 | """ 2 | WSGI config for config 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/dev/howto/deployment/wsgi/ 8 | """ 9 | 10 | import os 11 | 12 | from django.core.wsgi import get_wsgi_application 13 | 14 | os.environ.setdefault('DJANGO_SETTINGS_MODULE', 'config.settings') 15 | 16 | application = get_wsgi_application() 17 | -------------------------------------------------------------------------------- /ch3-todo-api/backend/db.sqlite3: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/wsvincent/djangoforapis_31/c678537076d809f4fea19f0beb5e20e3be8661e2/ch3-todo-api/backend/db.sqlite3 -------------------------------------------------------------------------------- /ch3-todo-api/backend/manage.py: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env python 2 | """Django's command-line utility for administrative tasks.""" 3 | import os 4 | import sys 5 | 6 | 7 | def main(): 8 | """Run administrative tasks.""" 9 | os.environ.setdefault('DJANGO_SETTINGS_MODULE', 'config.settings') 10 | try: 11 | from django.core.management import execute_from_command_line 12 | except ImportError as exc: 13 | raise ImportError( 14 | "Couldn't import Django. Are you sure it's installed and " 15 | "available on your PYTHONPATH environment variable? Did you " 16 | "forget to activate a virtual environment?" 17 | ) from exc 18 | execute_from_command_line(sys.argv) 19 | 20 | 21 | if __name__ == '__main__': 22 | main() 23 | -------------------------------------------------------------------------------- /ch3-todo-api/backend/todos/__init__.py: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/wsvincent/djangoforapis_31/c678537076d809f4fea19f0beb5e20e3be8661e2/ch3-todo-api/backend/todos/__init__.py -------------------------------------------------------------------------------- /ch3-todo-api/backend/todos/admin.py: -------------------------------------------------------------------------------- 1 | from django.contrib import admin 2 | from .models import Todo 3 | 4 | admin.site.register(Todo) 5 | -------------------------------------------------------------------------------- /ch3-todo-api/backend/todos/apps.py: -------------------------------------------------------------------------------- 1 | from django.apps import AppConfig 2 | 3 | 4 | class TodosConfig(AppConfig): 5 | name = 'todos' 6 | -------------------------------------------------------------------------------- /ch3-todo-api/backend/todos/migrations/0001_initial.py: -------------------------------------------------------------------------------- 1 | # Generated by Django 3.1rc1 on 2020-07-28 15:01 2 | 3 | from django.db import migrations, models 4 | 5 | 6 | class Migration(migrations.Migration): 7 | 8 | initial = True 9 | 10 | dependencies = [ 11 | ] 12 | 13 | operations = [ 14 | migrations.CreateModel( 15 | name='Todo', 16 | fields=[ 17 | ('id', models.AutoField(auto_created=True, primary_key=True, serialize=False, verbose_name='ID')), 18 | ('title', models.CharField(max_length=200)), 19 | ('body', models.TextField()), 20 | ], 21 | ), 22 | ] 23 | -------------------------------------------------------------------------------- /ch3-todo-api/backend/todos/migrations/__init__.py: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/wsvincent/djangoforapis_31/c678537076d809f4fea19f0beb5e20e3be8661e2/ch3-todo-api/backend/todos/migrations/__init__.py -------------------------------------------------------------------------------- /ch3-todo-api/backend/todos/models.py: -------------------------------------------------------------------------------- 1 | from django.db import models 2 | 3 | 4 | class Todo(models.Model): 5 | title = models.CharField(max_length=200) 6 | body = models.TextField() 7 | 8 | def __str__(self): 9 | return self.title 10 | -------------------------------------------------------------------------------- /ch3-todo-api/backend/todos/serializers.py: -------------------------------------------------------------------------------- 1 | from rest_framework import serializers 2 | from .models import Todo 3 | 4 | 5 | class TodoSerializer(serializers.ModelSerializer): 6 | class Meta: 7 | model = Todo 8 | fields = ('id', 'title', 'body',) 9 | -------------------------------------------------------------------------------- /ch3-todo-api/backend/todos/tests.py: -------------------------------------------------------------------------------- 1 | from django.test import TestCase 2 | from .models import Todo 3 | 4 | 5 | class TodoModelTest(TestCase): 6 | 7 | @classmethod 8 | def setUpTestData(cls): 9 | Todo.objects.create(title='first todo', body='a body here') 10 | 11 | def test_title_content(self): 12 | todo = Todo.objects.get(id=1) 13 | expected_object_name = f'{todo.title}' 14 | self.assertEqual(expected_object_name, 'first todo') 15 | 16 | def test_body_content(self): 17 | todo = Todo.objects.get(id=1) 18 | expected_object_name = f'{todo.body}' 19 | self.assertEqual(expected_object_name, 'a body here') 20 | -------------------------------------------------------------------------------- /ch3-todo-api/backend/todos/urls.py: -------------------------------------------------------------------------------- 1 | from django.urls import path 2 | from .views import ListTodo, DetailTodo 3 | 4 | urlpatterns = [ 5 | path('/', DetailTodo.as_view()), 6 | path('', ListTodo.as_view()), 7 | ] 8 | -------------------------------------------------------------------------------- /ch3-todo-api/backend/todos/views.py: -------------------------------------------------------------------------------- 1 | from rest_framework import generics 2 | from .models import Todo 3 | from .serializers import TodoSerializer 4 | 5 | 6 | class ListTodo(generics.ListAPIView): 7 | queryset = Todo.objects.all() 8 | serializer_class = TodoSerializer 9 | 10 | 11 | class DetailTodo(generics.RetrieveAPIView): 12 | queryset = Todo.objects.all() 13 | serializer_class = TodoSerializer 14 | -------------------------------------------------------------------------------- /ch4-todo-api-frontend/backend/Pipfile: -------------------------------------------------------------------------------- 1 | [[source]] 2 | name = "pypi" 3 | url = "https://pypi.org/simple" 4 | verify_ssl = true 5 | 6 | [dev-packages] 7 | 8 | [packages] 9 | django = "~=3.1.0" 10 | djangorestframework = "==3.11.0" 11 | django-cors-headers = "==3.4.0" 12 | 13 | [requires] 14 | python_version = "3.8" 15 | -------------------------------------------------------------------------------- /ch4-todo-api-frontend/backend/Pipfile.lock: -------------------------------------------------------------------------------- 1 | { 2 | "_meta": { 3 | "hash": { 4 | "sha256": "e56ad8b23f3e562a4287d55b3ec7789ff5b63f5d12f82a1b47b358d16b8402d3" 5 | }, 6 | "pipfile-spec": 6, 7 | "requires": { 8 | "python_version": "3.8" 9 | }, 10 | "sources": [ 11 | { 12 | "name": "pypi", 13 | "url": "https://pypi.org/simple", 14 | "verify_ssl": true 15 | } 16 | ] 17 | }, 18 | "default": { 19 | "asgiref": { 20 | "hashes": [ 21 | "sha256:7e51911ee147dd685c3c8b805c0ad0cb58d360987b56953878f8c06d2d1c6f1a", 22 | "sha256:9fc6fb5d39b8af147ba40765234fa822b39818b12cc80b35ad9b0cef3a476aed" 23 | ], 24 | "markers": "python_version >= '3.5'", 25 | "version": "==3.2.10" 26 | }, 27 | "django": { 28 | "hashes": [ 29 | "sha256:1a63f5bb6ff4d7c42f62a519edc2adbb37f9b78068a5a862beff858b68e3dc8b", 30 | "sha256:2d390268a13c655c97e0e2ede9d117007996db692c1bb93eabebd4fb7ea7012b" 31 | ], 32 | "index": "pypi", 33 | "version": "==3.1" 34 | }, 35 | "django-cors-headers": { 36 | "hashes": [ 37 | "sha256:5240062ef0b16668ce8a5f43324c388d65f5439e1a30e22c38684d5ddaff0d15", 38 | "sha256:f5218f2f0bb1210563ff87687afbf10786e080d8494a248e705507ebd92d7153" 39 | ], 40 | "index": "pypi", 41 | "version": "==3.4.0" 42 | }, 43 | "djangorestframework": { 44 | "hashes": [ 45 | "sha256:05809fc66e1c997fd9a32ea5730d9f4ba28b109b9da71fccfa5ff241201fd0a4", 46 | "sha256:e782087823c47a26826ee5b6fa0c542968219263fb3976ec3c31edab23a4001f" 47 | ], 48 | "index": "pypi", 49 | "version": "==3.11.0" 50 | }, 51 | "pytz": { 52 | "hashes": [ 53 | "sha256:a494d53b6d39c3c6e44c3bec237336e14305e4f29bbf800b599253057fbb79ed", 54 | "sha256:c35965d010ce31b23eeb663ed3cc8c906275d6be1a34393a1d73a41febf4a048" 55 | ], 56 | "version": "==2020.1" 57 | }, 58 | "sqlparse": { 59 | "hashes": [ 60 | "sha256:022fb9c87b524d1f7862b3037e541f68597a730a8843245c349fc93e1643dc4e", 61 | "sha256:e162203737712307dfe78860cc56c8da8a852ab2ee33750e33aeadf38d12c548" 62 | ], 63 | "markers": "python_version >= '2.7' and python_version not in '3.0, 3.1, 3.2, 3.3'", 64 | "version": "==0.3.1" 65 | } 66 | }, 67 | "develop": {} 68 | } 69 | -------------------------------------------------------------------------------- /ch4-todo-api-frontend/backend/config/__init__.py: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/wsvincent/djangoforapis_31/c678537076d809f4fea19f0beb5e20e3be8661e2/ch4-todo-api-frontend/backend/config/__init__.py -------------------------------------------------------------------------------- /ch4-todo-api-frontend/backend/config/asgi.py: -------------------------------------------------------------------------------- 1 | """ 2 | ASGI config for config project. 3 | 4 | It exposes the ASGI callable as a module-level variable named ``application``. 5 | 6 | For more information on this file, see 7 | https://docs.djangoproject.com/en/dev/howto/deployment/asgi/ 8 | """ 9 | 10 | import os 11 | 12 | from django.core.asgi import get_asgi_application 13 | 14 | os.environ.setdefault('DJANGO_SETTINGS_MODULE', 'config.settings') 15 | 16 | application = get_asgi_application() 17 | -------------------------------------------------------------------------------- /ch4-todo-api-frontend/backend/config/settings.py: -------------------------------------------------------------------------------- 1 | from pathlib import Path 2 | 3 | # Build paths inside the project like this: BASE_DIR / 'subdir'. 4 | BASE_DIR = Path(__file__).resolve(strict=True).parent.parent 5 | 6 | 7 | # Quick-start development settings - unsuitable for production 8 | # See https://docs.djangoproject.com/en/dev/howto/deployment/checklist/ 9 | 10 | # SECURITY WARNING: keep the secret key used in production secret! 11 | SECRET_KEY = 'y2kf+s*_9sws0agzq%gx4mw6d53xm&n^j!wf3e)=uf_^6m(@z@' 12 | 13 | # SECURITY WARNING: don't run with debug turned on in production! 14 | DEBUG = True 15 | 16 | ALLOWED_HOSTS = [] 17 | 18 | 19 | # Application definition 20 | 21 | INSTALLED_APPS = [ 22 | 'django.contrib.admin', 23 | 'django.contrib.auth', 24 | 'django.contrib.contenttypes', 25 | 'django.contrib.sessions', 26 | 'django.contrib.messages', 27 | 'django.contrib.staticfiles', 28 | 29 | # 3rd party 30 | 'rest_framework', # new 31 | 'corsheaders', # new 32 | 33 | # Local 34 | 'todos.apps.TodosConfig', # new 35 | ] 36 | 37 | MIDDLEWARE = [ 38 | 'django.middleware.security.SecurityMiddleware', 39 | 'django.contrib.sessions.middleware.SessionMiddleware', 40 | 'corsheaders.middleware.CorsMiddleware', # new 41 | 'django.middleware.common.CommonMiddleware', 42 | 'django.middleware.csrf.CsrfViewMiddleware', 43 | 'django.contrib.auth.middleware.AuthenticationMiddleware', 44 | 'django.contrib.messages.middleware.MessageMiddleware', 45 | 'django.middleware.clickjacking.XFrameOptionsMiddleware', 46 | ] 47 | 48 | ROOT_URLCONF = 'config.urls' 49 | 50 | TEMPLATES = [ 51 | { 52 | 'BACKEND': 'django.template.backends.django.DjangoTemplates', 53 | 'DIRS': [], 54 | 'APP_DIRS': True, 55 | 'OPTIONS': { 56 | 'context_processors': [ 57 | 'django.template.context_processors.debug', 58 | 'django.template.context_processors.request', 59 | 'django.contrib.auth.context_processors.auth', 60 | 'django.contrib.messages.context_processors.messages', 61 | ], 62 | }, 63 | }, 64 | ] 65 | 66 | WSGI_APPLICATION = 'config.wsgi.application' 67 | 68 | 69 | # Database 70 | # https://docs.djangoproject.com/en/dev/ref/settings/#databases 71 | 72 | DATABASES = { 73 | 'default': { 74 | 'ENGINE': 'django.db.backends.sqlite3', 75 | 'NAME': BASE_DIR / 'db.sqlite3', 76 | } 77 | } 78 | 79 | 80 | # Password validation 81 | # https://docs.djangoproject.com/en/dev/ref/settings/#auth-password-validators 82 | 83 | AUTH_PASSWORD_VALIDATORS = [ 84 | { 85 | 'NAME': 'django.contrib.auth.password_validation.UserAttributeSimilarityValidator', 86 | }, 87 | { 88 | 'NAME': 'django.contrib.auth.password_validation.MinimumLengthValidator', 89 | }, 90 | { 91 | 'NAME': 'django.contrib.auth.password_validation.CommonPasswordValidator', 92 | }, 93 | { 94 | 'NAME': 'django.contrib.auth.password_validation.NumericPasswordValidator', 95 | }, 96 | ] 97 | 98 | 99 | # Internationalization 100 | # https://docs.djangoproject.com/en/dev/topics/i18n/ 101 | 102 | LANGUAGE_CODE = 'en-us' 103 | 104 | TIME_ZONE = 'UTC' 105 | 106 | USE_I18N = True 107 | 108 | USE_L10N = True 109 | 110 | USE_TZ = True 111 | 112 | 113 | # Static files (CSS, JavaScript, Images) 114 | # https://docs.djangoproject.com/en/dev/howto/static-files/ 115 | 116 | STATIC_URL = '/static/' 117 | 118 | REST_FRAMEWORK = { 119 | 'DEFAULT_PERMISSION_CLASSES': [ 120 | 'rest_framework.permissions.AllowAny', 121 | ] 122 | } 123 | 124 | CORS_ORIGIN_WHITELIST = ( 125 | 'http://localhost:3000', 126 | 'http://localhost:8000', 127 | ) 128 | -------------------------------------------------------------------------------- /ch4-todo-api-frontend/backend/config/urls.py: -------------------------------------------------------------------------------- 1 | from django.contrib import admin 2 | from django.urls import include, path # new 3 | 4 | 5 | urlpatterns = [ 6 | path('admin/', admin.site.urls), 7 | path('api/', include('todos.urls')), # new 8 | ] 9 | -------------------------------------------------------------------------------- /ch4-todo-api-frontend/backend/config/wsgi.py: -------------------------------------------------------------------------------- 1 | """ 2 | WSGI config for config 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/dev/howto/deployment/wsgi/ 8 | """ 9 | 10 | import os 11 | 12 | from django.core.wsgi import get_wsgi_application 13 | 14 | os.environ.setdefault('DJANGO_SETTINGS_MODULE', 'config.settings') 15 | 16 | application = get_wsgi_application() 17 | -------------------------------------------------------------------------------- /ch4-todo-api-frontend/backend/db.sqlite3: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/wsvincent/djangoforapis_31/c678537076d809f4fea19f0beb5e20e3be8661e2/ch4-todo-api-frontend/backend/db.sqlite3 -------------------------------------------------------------------------------- /ch4-todo-api-frontend/backend/manage.py: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env python 2 | """Django's command-line utility for administrative tasks.""" 3 | import os 4 | import sys 5 | 6 | 7 | def main(): 8 | """Run administrative tasks.""" 9 | os.environ.setdefault('DJANGO_SETTINGS_MODULE', 'config.settings') 10 | try: 11 | from django.core.management import execute_from_command_line 12 | except ImportError as exc: 13 | raise ImportError( 14 | "Couldn't import Django. Are you sure it's installed and " 15 | "available on your PYTHONPATH environment variable? Did you " 16 | "forget to activate a virtual environment?" 17 | ) from exc 18 | execute_from_command_line(sys.argv) 19 | 20 | 21 | if __name__ == '__main__': 22 | main() 23 | -------------------------------------------------------------------------------- /ch4-todo-api-frontend/backend/todos/__init__.py: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/wsvincent/djangoforapis_31/c678537076d809f4fea19f0beb5e20e3be8661e2/ch4-todo-api-frontend/backend/todos/__init__.py -------------------------------------------------------------------------------- /ch4-todo-api-frontend/backend/todos/admin.py: -------------------------------------------------------------------------------- 1 | from django.contrib import admin 2 | from .models import Todo 3 | 4 | admin.site.register(Todo) 5 | -------------------------------------------------------------------------------- /ch4-todo-api-frontend/backend/todos/apps.py: -------------------------------------------------------------------------------- 1 | from django.apps import AppConfig 2 | 3 | 4 | class TodosConfig(AppConfig): 5 | name = 'todos' 6 | -------------------------------------------------------------------------------- /ch4-todo-api-frontend/backend/todos/migrations/0001_initial.py: -------------------------------------------------------------------------------- 1 | # Generated by Django 3.1rc1 on 2020-07-28 15:01 2 | 3 | from django.db import migrations, models 4 | 5 | 6 | class Migration(migrations.Migration): 7 | 8 | initial = True 9 | 10 | dependencies = [ 11 | ] 12 | 13 | operations = [ 14 | migrations.CreateModel( 15 | name='Todo', 16 | fields=[ 17 | ('id', models.AutoField(auto_created=True, primary_key=True, serialize=False, verbose_name='ID')), 18 | ('title', models.CharField(max_length=200)), 19 | ('body', models.TextField()), 20 | ], 21 | ), 22 | ] 23 | -------------------------------------------------------------------------------- /ch4-todo-api-frontend/backend/todos/migrations/__init__.py: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/wsvincent/djangoforapis_31/c678537076d809f4fea19f0beb5e20e3be8661e2/ch4-todo-api-frontend/backend/todos/migrations/__init__.py -------------------------------------------------------------------------------- /ch4-todo-api-frontend/backend/todos/models.py: -------------------------------------------------------------------------------- 1 | from django.db import models 2 | 3 | 4 | class Todo(models.Model): 5 | title = models.CharField(max_length=200) 6 | body = models.TextField() 7 | 8 | def __str__(self): 9 | return self.title 10 | -------------------------------------------------------------------------------- /ch4-todo-api-frontend/backend/todos/serializers.py: -------------------------------------------------------------------------------- 1 | from rest_framework import serializers 2 | from .models import Todo 3 | 4 | 5 | class TodoSerializer(serializers.ModelSerializer): 6 | class Meta: 7 | model = Todo 8 | fields = ('id', 'title', 'body',) 9 | -------------------------------------------------------------------------------- /ch4-todo-api-frontend/backend/todos/tests.py: -------------------------------------------------------------------------------- 1 | from django.test import TestCase 2 | from .models import Todo 3 | 4 | 5 | class TodoModelTest(TestCase): 6 | 7 | @classmethod 8 | def setUpTestData(cls): 9 | Todo.objects.create(title='first todo', body='a body here') 10 | 11 | def test_title_content(self): 12 | todo = Todo.objects.get(id=1) 13 | expected_object_name = f'{todo.title}' 14 | self.assertEqual(expected_object_name, 'first todo') 15 | 16 | def test_body_content(self): 17 | todo = Todo.objects.get(id=1) 18 | expected_object_name = f'{todo.body}' 19 | self.assertEqual(expected_object_name, 'a body here') 20 | -------------------------------------------------------------------------------- /ch4-todo-api-frontend/backend/todos/urls.py: -------------------------------------------------------------------------------- 1 | from django.urls import path 2 | from .views import ListTodo, DetailTodo 3 | 4 | urlpatterns = [ 5 | path('/', DetailTodo.as_view()), 6 | path('', ListTodo.as_view()), 7 | ] 8 | -------------------------------------------------------------------------------- /ch4-todo-api-frontend/backend/todos/views.py: -------------------------------------------------------------------------------- 1 | from rest_framework import generics 2 | from .models import Todo 3 | from .serializers import TodoSerializer 4 | 5 | 6 | class ListTodo(generics.ListAPIView): 7 | queryset = Todo.objects.all() 8 | serializer_class = TodoSerializer 9 | 10 | 11 | class DetailTodo(generics.RetrieveAPIView): 12 | queryset = Todo.objects.all() 13 | serializer_class = TodoSerializer 14 | -------------------------------------------------------------------------------- /ch5-blog-api/Pipfile: -------------------------------------------------------------------------------- 1 | [[source]] 2 | name = "pypi" 3 | url = "https://pypi.org/simple" 4 | verify_ssl = true 5 | 6 | [dev-packages] 7 | 8 | [packages] 9 | django = "~=3.1.0" 10 | djangorestframework = "==3.11.0" 11 | 12 | [requires] 13 | python_version = "3.8" 14 | -------------------------------------------------------------------------------- /ch5-blog-api/Pipfile.lock: -------------------------------------------------------------------------------- 1 | { 2 | "_meta": { 3 | "hash": { 4 | "sha256": "d50f113a63968a27a3bb3975146f499e12e0e53154a5f727aef7fd473504f888" 5 | }, 6 | "pipfile-spec": 6, 7 | "requires": { 8 | "python_version": "3.8" 9 | }, 10 | "sources": [ 11 | { 12 | "name": "pypi", 13 | "url": "https://pypi.org/simple", 14 | "verify_ssl": true 15 | } 16 | ] 17 | }, 18 | "default": { 19 | "asgiref": { 20 | "hashes": [ 21 | "sha256:7e51911ee147dd685c3c8b805c0ad0cb58d360987b56953878f8c06d2d1c6f1a", 22 | "sha256:9fc6fb5d39b8af147ba40765234fa822b39818b12cc80b35ad9b0cef3a476aed" 23 | ], 24 | "markers": "python_version >= '3.5'", 25 | "version": "==3.2.10" 26 | }, 27 | "django": { 28 | "hashes": [ 29 | "sha256:1a63f5bb6ff4d7c42f62a519edc2adbb37f9b78068a5a862beff858b68e3dc8b", 30 | "sha256:2d390268a13c655c97e0e2ede9d117007996db692c1bb93eabebd4fb7ea7012b" 31 | ], 32 | "index": "pypi", 33 | "version": "==3.1" 34 | }, 35 | "djangorestframework": { 36 | "hashes": [ 37 | "sha256:05809fc66e1c997fd9a32ea5730d9f4ba28b109b9da71fccfa5ff241201fd0a4", 38 | "sha256:e782087823c47a26826ee5b6fa0c542968219263fb3976ec3c31edab23a4001f" 39 | ], 40 | "index": "pypi", 41 | "version": "==3.11.0" 42 | }, 43 | "pytz": { 44 | "hashes": [ 45 | "sha256:a494d53b6d39c3c6e44c3bec237336e14305e4f29bbf800b599253057fbb79ed", 46 | "sha256:c35965d010ce31b23eeb663ed3cc8c906275d6be1a34393a1d73a41febf4a048" 47 | ], 48 | "version": "==2020.1" 49 | }, 50 | "sqlparse": { 51 | "hashes": [ 52 | "sha256:022fb9c87b524d1f7862b3037e541f68597a730a8843245c349fc93e1643dc4e", 53 | "sha256:e162203737712307dfe78860cc56c8da8a852ab2ee33750e33aeadf38d12c548" 54 | ], 55 | "markers": "python_version >= '2.7' and python_version not in '3.0, 3.1, 3.2, 3.3'", 56 | "version": "==0.3.1" 57 | } 58 | }, 59 | "develop": {} 60 | } 61 | -------------------------------------------------------------------------------- /ch5-blog-api/config/__init__.py: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/wsvincent/djangoforapis_31/c678537076d809f4fea19f0beb5e20e3be8661e2/ch5-blog-api/config/__init__.py -------------------------------------------------------------------------------- /ch5-blog-api/config/asgi.py: -------------------------------------------------------------------------------- 1 | """ 2 | ASGI config for config project. 3 | 4 | It exposes the ASGI callable as a module-level variable named ``application``. 5 | 6 | For more information on this file, see 7 | https://docs.djangoproject.com/en/dev/howto/deployment/asgi/ 8 | """ 9 | 10 | import os 11 | 12 | from django.core.asgi import get_asgi_application 13 | 14 | os.environ.setdefault('DJANGO_SETTINGS_MODULE', 'config.settings') 15 | 16 | application = get_asgi_application() 17 | -------------------------------------------------------------------------------- /ch5-blog-api/config/settings.py: -------------------------------------------------------------------------------- 1 | """ 2 | Django settings for config project. 3 | 4 | Generated by 'django-admin startproject' using Django 3.1rc1. 5 | 6 | For more information on this file, see 7 | https://docs.djangoproject.com/en/dev/topics/settings/ 8 | 9 | For the full list of settings and their values, see 10 | https://docs.djangoproject.com/en/dev/ref/settings/ 11 | """ 12 | 13 | from pathlib import Path 14 | 15 | # Build paths inside the project like this: BASE_DIR / 'subdir'. 16 | BASE_DIR = Path(__file__).resolve(strict=True).parent.parent 17 | 18 | 19 | # Quick-start development settings - unsuitable for production 20 | # See https://docs.djangoproject.com/en/dev/howto/deployment/checklist/ 21 | 22 | # SECURITY WARNING: keep the secret key used in production secret! 23 | SECRET_KEY = '5kji9d4+u2=pw4k2bh+v4pt7(57)$hsns-g_ma0oskgf*aa16&' 24 | 25 | # SECURITY WARNING: don't run with debug turned on in production! 26 | DEBUG = True 27 | 28 | ALLOWED_HOSTS = [] 29 | 30 | 31 | # Application definition 32 | 33 | INSTALLED_APPS = [ 34 | 'django.contrib.admin', 35 | 'django.contrib.auth', 36 | 'django.contrib.contenttypes', 37 | 'django.contrib.sessions', 38 | 'django.contrib.messages', 39 | 'django.contrib.staticfiles', 40 | 41 | # 3rd-party apps 42 | 'rest_framework', # new 43 | 44 | # Local 45 | 'posts', # new 46 | ] 47 | 48 | MIDDLEWARE = [ 49 | 'django.middleware.security.SecurityMiddleware', 50 | 'django.contrib.sessions.middleware.SessionMiddleware', 51 | 'django.middleware.common.CommonMiddleware', 52 | 'django.middleware.csrf.CsrfViewMiddleware', 53 | 'django.contrib.auth.middleware.AuthenticationMiddleware', 54 | 'django.contrib.messages.middleware.MessageMiddleware', 55 | 'django.middleware.clickjacking.XFrameOptionsMiddleware', 56 | ] 57 | 58 | ROOT_URLCONF = 'config.urls' 59 | 60 | TEMPLATES = [ 61 | { 62 | 'BACKEND': 'django.template.backends.django.DjangoTemplates', 63 | 'DIRS': [], 64 | 'APP_DIRS': True, 65 | 'OPTIONS': { 66 | 'context_processors': [ 67 | 'django.template.context_processors.debug', 68 | 'django.template.context_processors.request', 69 | 'django.contrib.auth.context_processors.auth', 70 | 'django.contrib.messages.context_processors.messages', 71 | ], 72 | }, 73 | }, 74 | ] 75 | 76 | WSGI_APPLICATION = 'config.wsgi.application' 77 | 78 | 79 | # Database 80 | # https://docs.djangoproject.com/en/dev/ref/settings/#databases 81 | 82 | DATABASES = { 83 | 'default': { 84 | 'ENGINE': 'django.db.backends.sqlite3', 85 | 'NAME': BASE_DIR / 'db.sqlite3', 86 | } 87 | } 88 | 89 | 90 | # Password validation 91 | # https://docs.djangoproject.com/en/dev/ref/settings/#auth-password-validators 92 | 93 | AUTH_PASSWORD_VALIDATORS = [ 94 | { 95 | 'NAME': 'django.contrib.auth.password_validation.UserAttributeSimilarityValidator', 96 | }, 97 | { 98 | 'NAME': 'django.contrib.auth.password_validation.MinimumLengthValidator', 99 | }, 100 | { 101 | 'NAME': 'django.contrib.auth.password_validation.CommonPasswordValidator', 102 | }, 103 | { 104 | 'NAME': 'django.contrib.auth.password_validation.NumericPasswordValidator', 105 | }, 106 | ] 107 | 108 | 109 | # Internationalization 110 | # https://docs.djangoproject.com/en/dev/topics/i18n/ 111 | 112 | LANGUAGE_CODE = 'en-us' 113 | 114 | TIME_ZONE = 'UTC' 115 | 116 | USE_I18N = True 117 | 118 | USE_L10N = True 119 | 120 | USE_TZ = True 121 | 122 | 123 | # Static files (CSS, JavaScript, Images) 124 | # https://docs.djangoproject.com/en/dev/howto/static-files/ 125 | 126 | STATIC_URL = '/static/' 127 | 128 | REST_FRAMEWORK = { 129 | 'DEFAULT_PERMISSION_CLASSES': [ 130 | 'rest_framework.permissions.AllowAny', 131 | ] 132 | } 133 | -------------------------------------------------------------------------------- /ch5-blog-api/config/urls.py: -------------------------------------------------------------------------------- 1 | from django.contrib import admin 2 | from django.urls import include, path # new 3 | 4 | 5 | urlpatterns = [ 6 | path('admin/', admin.site.urls), 7 | path('api/v1/', include('posts.urls')), # new 8 | ] 9 | -------------------------------------------------------------------------------- /ch5-blog-api/config/wsgi.py: -------------------------------------------------------------------------------- 1 | """ 2 | WSGI config for config 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/dev/howto/deployment/wsgi/ 8 | """ 9 | 10 | import os 11 | 12 | from django.core.wsgi import get_wsgi_application 13 | 14 | os.environ.setdefault('DJANGO_SETTINGS_MODULE', 'config.settings') 15 | 16 | application = get_wsgi_application() 17 | -------------------------------------------------------------------------------- /ch5-blog-api/db.sqlite3: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/wsvincent/djangoforapis_31/c678537076d809f4fea19f0beb5e20e3be8661e2/ch5-blog-api/db.sqlite3 -------------------------------------------------------------------------------- /ch5-blog-api/manage.py: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env python 2 | """Django's command-line utility for administrative tasks.""" 3 | import os 4 | import sys 5 | 6 | 7 | def main(): 8 | """Run administrative tasks.""" 9 | os.environ.setdefault('DJANGO_SETTINGS_MODULE', 'config.settings') 10 | try: 11 | from django.core.management import execute_from_command_line 12 | except ImportError as exc: 13 | raise ImportError( 14 | "Couldn't import Django. Are you sure it's installed and " 15 | "available on your PYTHONPATH environment variable? Did you " 16 | "forget to activate a virtual environment?" 17 | ) from exc 18 | execute_from_command_line(sys.argv) 19 | 20 | 21 | if __name__ == '__main__': 22 | main() 23 | -------------------------------------------------------------------------------- /ch5-blog-api/posts/__init__.py: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/wsvincent/djangoforapis_31/c678537076d809f4fea19f0beb5e20e3be8661e2/ch5-blog-api/posts/__init__.py -------------------------------------------------------------------------------- /ch5-blog-api/posts/admin.py: -------------------------------------------------------------------------------- 1 | from django.contrib import admin 2 | from .models import Post 3 | 4 | admin.site.register(Post) 5 | -------------------------------------------------------------------------------- /ch5-blog-api/posts/apps.py: -------------------------------------------------------------------------------- 1 | from django.apps import AppConfig 2 | 3 | 4 | class PostsConfig(AppConfig): 5 | name = 'posts' 6 | -------------------------------------------------------------------------------- /ch5-blog-api/posts/migrations/0001_initial.py: -------------------------------------------------------------------------------- 1 | # Generated by Django 3.1rc1 on 2020-07-29 17:28 2 | 3 | from django.conf import settings 4 | from django.db import migrations, models 5 | import django.db.models.deletion 6 | 7 | 8 | class Migration(migrations.Migration): 9 | 10 | initial = True 11 | 12 | dependencies = [ 13 | migrations.swappable_dependency(settings.AUTH_USER_MODEL), 14 | ] 15 | 16 | operations = [ 17 | migrations.CreateModel( 18 | name='Post', 19 | fields=[ 20 | ('id', models.AutoField(auto_created=True, primary_key=True, serialize=False, verbose_name='ID')), 21 | ('title', models.CharField(max_length=50)), 22 | ('body', models.TextField()), 23 | ('created_at', models.DateTimeField(auto_now_add=True)), 24 | ('updated_at', models.DateTimeField(auto_now=True)), 25 | ('author', models.ForeignKey(on_delete=django.db.models.deletion.CASCADE, to=settings.AUTH_USER_MODEL)), 26 | ], 27 | ), 28 | ] 29 | -------------------------------------------------------------------------------- /ch5-blog-api/posts/migrations/__init__.py: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/wsvincent/djangoforapis_31/c678537076d809f4fea19f0beb5e20e3be8661e2/ch5-blog-api/posts/migrations/__init__.py -------------------------------------------------------------------------------- /ch5-blog-api/posts/models.py: -------------------------------------------------------------------------------- 1 | from django.db import models 2 | from django.contrib.auth.models import User 3 | 4 | 5 | class Post(models.Model): 6 | author = models.ForeignKey(User, on_delete=models.CASCADE) 7 | title = models.CharField(max_length=50) 8 | body = models.TextField() 9 | created_at = models.DateTimeField(auto_now_add=True) 10 | updated_at = models.DateTimeField(auto_now=True) 11 | 12 | def __str__(self): 13 | return self.title 14 | -------------------------------------------------------------------------------- /ch5-blog-api/posts/serializers.py: -------------------------------------------------------------------------------- 1 | from rest_framework import serializers 2 | from .models import Post 3 | 4 | 5 | class PostSerializer(serializers.ModelSerializer): 6 | 7 | class Meta: 8 | fields = ('id', 'author', 'title', 'body', 'created_at',) 9 | model = Post 10 | -------------------------------------------------------------------------------- /ch5-blog-api/posts/tests.py: -------------------------------------------------------------------------------- 1 | from django.test import TestCase 2 | from django.contrib.auth.models import User 3 | from .models import Post 4 | 5 | 6 | class BlogTests(TestCase): 7 | 8 | @classmethod 9 | def setUpTestData(cls): 10 | # Create a user 11 | testuser1 = User.objects.create_user( 12 | username='testuser1', password='abc123') 13 | testuser1.save() 14 | 15 | # Create a blog post 16 | test_post = Post.objects.create( 17 | author=testuser1, title='Blog title', body='Body content...') 18 | test_post.save() 19 | 20 | def test_blog_content(self): 21 | post = Post.objects.get(id=1) 22 | author = f'{post.author}' 23 | title = f'{post.title}' 24 | body = f'{post.body}' 25 | self.assertEqual(author, 'testuser1') 26 | self.assertEqual(title, 'Blog title') 27 | self.assertEqual(body, 'Body content...') 28 | -------------------------------------------------------------------------------- /ch5-blog-api/posts/urls.py: -------------------------------------------------------------------------------- 1 | from django.urls import path 2 | from .views import PostList, PostDetail 3 | 4 | urlpatterns = [ 5 | path('/', PostDetail.as_view()), 6 | path('', PostList.as_view()), 7 | ] 8 | -------------------------------------------------------------------------------- /ch5-blog-api/posts/views.py: -------------------------------------------------------------------------------- 1 | from rest_framework import generics 2 | from .models import Post 3 | from .serializers import PostSerializer 4 | 5 | 6 | class PostList(generics.ListCreateAPIView): 7 | queryset = Post.objects.all() 8 | serializer_class = PostSerializer 9 | 10 | 11 | class PostDetail(generics.RetrieveUpdateDestroyAPIView): 12 | queryset = Post.objects.all() 13 | serializer_class = PostSerializer 14 | -------------------------------------------------------------------------------- /ch6-blog-api-permissions/Pipfile: -------------------------------------------------------------------------------- 1 | [[source]] 2 | name = "pypi" 3 | url = "https://pypi.org/simple" 4 | verify_ssl = true 5 | 6 | [dev-packages] 7 | 8 | [packages] 9 | django = "~=3.1.0" 10 | djangorestframework = "==3.11.0" 11 | 12 | [requires] 13 | python_version = "3.8" 14 | -------------------------------------------------------------------------------- /ch6-blog-api-permissions/Pipfile.lock: -------------------------------------------------------------------------------- 1 | { 2 | "_meta": { 3 | "hash": { 4 | "sha256": "d50f113a63968a27a3bb3975146f499e12e0e53154a5f727aef7fd473504f888" 5 | }, 6 | "pipfile-spec": 6, 7 | "requires": { 8 | "python_version": "3.8" 9 | }, 10 | "sources": [ 11 | { 12 | "name": "pypi", 13 | "url": "https://pypi.org/simple", 14 | "verify_ssl": true 15 | } 16 | ] 17 | }, 18 | "default": { 19 | "asgiref": { 20 | "hashes": [ 21 | "sha256:7e51911ee147dd685c3c8b805c0ad0cb58d360987b56953878f8c06d2d1c6f1a", 22 | "sha256:9fc6fb5d39b8af147ba40765234fa822b39818b12cc80b35ad9b0cef3a476aed" 23 | ], 24 | "markers": "python_version >= '3.5'", 25 | "version": "==3.2.10" 26 | }, 27 | "django": { 28 | "hashes": [ 29 | "sha256:1a63f5bb6ff4d7c42f62a519edc2adbb37f9b78068a5a862beff858b68e3dc8b", 30 | "sha256:2d390268a13c655c97e0e2ede9d117007996db692c1bb93eabebd4fb7ea7012b" 31 | ], 32 | "index": "pypi", 33 | "version": "==3.1" 34 | }, 35 | "djangorestframework": { 36 | "hashes": [ 37 | "sha256:05809fc66e1c997fd9a32ea5730d9f4ba28b109b9da71fccfa5ff241201fd0a4", 38 | "sha256:e782087823c47a26826ee5b6fa0c542968219263fb3976ec3c31edab23a4001f" 39 | ], 40 | "index": "pypi", 41 | "version": "==3.11.0" 42 | }, 43 | "pytz": { 44 | "hashes": [ 45 | "sha256:a494d53b6d39c3c6e44c3bec237336e14305e4f29bbf800b599253057fbb79ed", 46 | "sha256:c35965d010ce31b23eeb663ed3cc8c906275d6be1a34393a1d73a41febf4a048" 47 | ], 48 | "version": "==2020.1" 49 | }, 50 | "sqlparse": { 51 | "hashes": [ 52 | "sha256:022fb9c87b524d1f7862b3037e541f68597a730a8843245c349fc93e1643dc4e", 53 | "sha256:e162203737712307dfe78860cc56c8da8a852ab2ee33750e33aeadf38d12c548" 54 | ], 55 | "markers": "python_version >= '2.7' and python_version not in '3.0, 3.1, 3.2, 3.3'", 56 | "version": "==0.3.1" 57 | } 58 | }, 59 | "develop": {} 60 | } 61 | -------------------------------------------------------------------------------- /ch6-blog-api-permissions/config/__init__.py: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/wsvincent/djangoforapis_31/c678537076d809f4fea19f0beb5e20e3be8661e2/ch6-blog-api-permissions/config/__init__.py -------------------------------------------------------------------------------- /ch6-blog-api-permissions/config/asgi.py: -------------------------------------------------------------------------------- 1 | """ 2 | ASGI config for config project. 3 | 4 | It exposes the ASGI callable as a module-level variable named ``application``. 5 | 6 | For more information on this file, see 7 | https://docs.djangoproject.com/en/dev/howto/deployment/asgi/ 8 | """ 9 | 10 | import os 11 | 12 | from django.core.asgi import get_asgi_application 13 | 14 | os.environ.setdefault('DJANGO_SETTINGS_MODULE', 'config.settings') 15 | 16 | application = get_asgi_application() 17 | -------------------------------------------------------------------------------- /ch6-blog-api-permissions/config/settings.py: -------------------------------------------------------------------------------- 1 | """ 2 | Django settings for config project. 3 | 4 | Generated by 'django-admin startproject' using Django 3.1rc1. 5 | 6 | For more information on this file, see 7 | https://docs.djangoproject.com/en/dev/topics/settings/ 8 | 9 | For the full list of settings and their values, see 10 | https://docs.djangoproject.com/en/dev/ref/settings/ 11 | """ 12 | 13 | from pathlib import Path 14 | 15 | # Build paths inside the project like this: BASE_DIR / 'subdir'. 16 | BASE_DIR = Path(__file__).resolve(strict=True).parent.parent 17 | 18 | 19 | # Quick-start development settings - unsuitable for production 20 | # See https://docs.djangoproject.com/en/dev/howto/deployment/checklist/ 21 | 22 | # SECURITY WARNING: keep the secret key used in production secret! 23 | SECRET_KEY = '5kji9d4+u2=pw4k2bh+v4pt7(57)$hsns-g_ma0oskgf*aa16&' 24 | 25 | # SECURITY WARNING: don't run with debug turned on in production! 26 | DEBUG = True 27 | 28 | ALLOWED_HOSTS = [] 29 | 30 | 31 | # Application definition 32 | 33 | INSTALLED_APPS = [ 34 | 'django.contrib.admin', 35 | 'django.contrib.auth', 36 | 'django.contrib.contenttypes', 37 | 'django.contrib.sessions', 38 | 'django.contrib.messages', 39 | 'django.contrib.staticfiles', 40 | 41 | # 3rd-party apps 42 | 'rest_framework', # new 43 | 44 | # Local 45 | 'posts', # new 46 | ] 47 | 48 | MIDDLEWARE = [ 49 | 'django.middleware.security.SecurityMiddleware', 50 | 'django.contrib.sessions.middleware.SessionMiddleware', 51 | 'django.middleware.common.CommonMiddleware', 52 | 'django.middleware.csrf.CsrfViewMiddleware', 53 | 'django.contrib.auth.middleware.AuthenticationMiddleware', 54 | 'django.contrib.messages.middleware.MessageMiddleware', 55 | 'django.middleware.clickjacking.XFrameOptionsMiddleware', 56 | ] 57 | 58 | ROOT_URLCONF = 'config.urls' 59 | 60 | TEMPLATES = [ 61 | { 62 | 'BACKEND': 'django.template.backends.django.DjangoTemplates', 63 | 'DIRS': [], 64 | 'APP_DIRS': True, 65 | 'OPTIONS': { 66 | 'context_processors': [ 67 | 'django.template.context_processors.debug', 68 | 'django.template.context_processors.request', 69 | 'django.contrib.auth.context_processors.auth', 70 | 'django.contrib.messages.context_processors.messages', 71 | ], 72 | }, 73 | }, 74 | ] 75 | 76 | WSGI_APPLICATION = 'config.wsgi.application' 77 | 78 | 79 | # Database 80 | # https://docs.djangoproject.com/en/dev/ref/settings/#databases 81 | 82 | DATABASES = { 83 | 'default': { 84 | 'ENGINE': 'django.db.backends.sqlite3', 85 | 'NAME': BASE_DIR / 'db.sqlite3', 86 | } 87 | } 88 | 89 | 90 | # Password validation 91 | # https://docs.djangoproject.com/en/dev/ref/settings/#auth-password-validators 92 | 93 | AUTH_PASSWORD_VALIDATORS = [ 94 | { 95 | 'NAME': 'django.contrib.auth.password_validation.UserAttributeSimilarityValidator', 96 | }, 97 | { 98 | 'NAME': 'django.contrib.auth.password_validation.MinimumLengthValidator', 99 | }, 100 | { 101 | 'NAME': 'django.contrib.auth.password_validation.CommonPasswordValidator', 102 | }, 103 | { 104 | 'NAME': 'django.contrib.auth.password_validation.NumericPasswordValidator', 105 | }, 106 | ] 107 | 108 | 109 | # Internationalization 110 | # https://docs.djangoproject.com/en/dev/topics/i18n/ 111 | 112 | LANGUAGE_CODE = 'en-us' 113 | 114 | TIME_ZONE = 'UTC' 115 | 116 | USE_I18N = True 117 | 118 | USE_L10N = True 119 | 120 | USE_TZ = True 121 | 122 | 123 | # Static files (CSS, JavaScript, Images) 124 | # https://docs.djangoproject.com/en/dev/howto/static-files/ 125 | 126 | STATIC_URL = '/static/' 127 | 128 | REST_FRAMEWORK = { 129 | 'DEFAULT_PERMISSION_CLASSES': [ 130 | 'rest_framework.permissions.IsAuthenticated', # new 131 | ] 132 | } 133 | -------------------------------------------------------------------------------- /ch6-blog-api-permissions/config/urls.py: -------------------------------------------------------------------------------- 1 | from django.contrib import admin 2 | from django.urls import include, path 3 | 4 | 5 | urlpatterns = [ 6 | path('admin/', admin.site.urls), 7 | path('api/v1/', include('posts.urls')), 8 | path('api-auth/', include('rest_framework.urls')), # new 9 | ] 10 | -------------------------------------------------------------------------------- /ch6-blog-api-permissions/config/wsgi.py: -------------------------------------------------------------------------------- 1 | """ 2 | WSGI config for config 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/dev/howto/deployment/wsgi/ 8 | """ 9 | 10 | import os 11 | 12 | from django.core.wsgi import get_wsgi_application 13 | 14 | os.environ.setdefault('DJANGO_SETTINGS_MODULE', 'config.settings') 15 | 16 | application = get_wsgi_application() 17 | -------------------------------------------------------------------------------- /ch6-blog-api-permissions/db.sqlite3: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/wsvincent/djangoforapis_31/c678537076d809f4fea19f0beb5e20e3be8661e2/ch6-blog-api-permissions/db.sqlite3 -------------------------------------------------------------------------------- /ch6-blog-api-permissions/manage.py: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env python 2 | """Django's command-line utility for administrative tasks.""" 3 | import os 4 | import sys 5 | 6 | 7 | def main(): 8 | """Run administrative tasks.""" 9 | os.environ.setdefault('DJANGO_SETTINGS_MODULE', 'config.settings') 10 | try: 11 | from django.core.management import execute_from_command_line 12 | except ImportError as exc: 13 | raise ImportError( 14 | "Couldn't import Django. Are you sure it's installed and " 15 | "available on your PYTHONPATH environment variable? Did you " 16 | "forget to activate a virtual environment?" 17 | ) from exc 18 | execute_from_command_line(sys.argv) 19 | 20 | 21 | if __name__ == '__main__': 22 | main() 23 | -------------------------------------------------------------------------------- /ch6-blog-api-permissions/posts/__init__.py: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/wsvincent/djangoforapis_31/c678537076d809f4fea19f0beb5e20e3be8661e2/ch6-blog-api-permissions/posts/__init__.py -------------------------------------------------------------------------------- /ch6-blog-api-permissions/posts/admin.py: -------------------------------------------------------------------------------- 1 | from django.contrib import admin 2 | from .models import Post 3 | 4 | admin.site.register(Post) 5 | -------------------------------------------------------------------------------- /ch6-blog-api-permissions/posts/apps.py: -------------------------------------------------------------------------------- 1 | from django.apps import AppConfig 2 | 3 | 4 | class PostsConfig(AppConfig): 5 | name = 'posts' 6 | -------------------------------------------------------------------------------- /ch6-blog-api-permissions/posts/migrations/0001_initial.py: -------------------------------------------------------------------------------- 1 | # Generated by Django 3.1rc1 on 2020-07-29 17:28 2 | 3 | from django.conf import settings 4 | from django.db import migrations, models 5 | import django.db.models.deletion 6 | 7 | 8 | class Migration(migrations.Migration): 9 | 10 | initial = True 11 | 12 | dependencies = [ 13 | migrations.swappable_dependency(settings.AUTH_USER_MODEL), 14 | ] 15 | 16 | operations = [ 17 | migrations.CreateModel( 18 | name='Post', 19 | fields=[ 20 | ('id', models.AutoField(auto_created=True, primary_key=True, serialize=False, verbose_name='ID')), 21 | ('title', models.CharField(max_length=50)), 22 | ('body', models.TextField()), 23 | ('created_at', models.DateTimeField(auto_now_add=True)), 24 | ('updated_at', models.DateTimeField(auto_now=True)), 25 | ('author', models.ForeignKey(on_delete=django.db.models.deletion.CASCADE, to=settings.AUTH_USER_MODEL)), 26 | ], 27 | ), 28 | ] 29 | -------------------------------------------------------------------------------- /ch6-blog-api-permissions/posts/migrations/__init__.py: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/wsvincent/djangoforapis_31/c678537076d809f4fea19f0beb5e20e3be8661e2/ch6-blog-api-permissions/posts/migrations/__init__.py -------------------------------------------------------------------------------- /ch6-blog-api-permissions/posts/models.py: -------------------------------------------------------------------------------- 1 | from django.db import models 2 | from django.contrib.auth.models import User 3 | 4 | 5 | class Post(models.Model): 6 | author = models.ForeignKey(User, on_delete=models.CASCADE) 7 | title = models.CharField(max_length=50) 8 | body = models.TextField() 9 | created_at = models.DateTimeField(auto_now_add=True) 10 | updated_at = models.DateTimeField(auto_now=True) 11 | 12 | def __str__(self): 13 | return self.title 14 | -------------------------------------------------------------------------------- /ch6-blog-api-permissions/posts/permissions.py: -------------------------------------------------------------------------------- 1 | from rest_framework import permissions 2 | 3 | 4 | class IsAuthorOrReadOnly(permissions.BasePermission): 5 | 6 | def has_object_permission(self, request, view, obj): 7 | # Read-only permissions are allowed for any request 8 | if request.method in permissions.SAFE_METHODS: 9 | return True 10 | 11 | # Write permissions are only allowed to the author of a post 12 | return obj.author == request.user 13 | -------------------------------------------------------------------------------- /ch6-blog-api-permissions/posts/serializers.py: -------------------------------------------------------------------------------- 1 | from rest_framework import serializers 2 | from .models import Post 3 | 4 | 5 | class PostSerializer(serializers.ModelSerializer): 6 | 7 | class Meta: 8 | fields = ('id', 'author', 'title', 'body', 'created_at',) 9 | model = Post 10 | -------------------------------------------------------------------------------- /ch6-blog-api-permissions/posts/tests.py: -------------------------------------------------------------------------------- 1 | from django.test import TestCase 2 | from django.contrib.auth.models import User 3 | from .models import Post 4 | 5 | 6 | class BlogTests(TestCase): 7 | 8 | @classmethod 9 | def setUpTestData(cls): 10 | # Create a user 11 | testuser1 = User.objects.create_user( 12 | username='testuser1', password='abc123') 13 | testuser1.save() 14 | 15 | # Create a blog post 16 | test_post = Post.objects.create( 17 | author=testuser1, title='Blog title', body='Body content...') 18 | test_post.save() 19 | 20 | def test_blog_content(self): 21 | post = Post.objects.get(id=1) 22 | author = f'{post.author}' 23 | title = f'{post.title}' 24 | body = f'{post.body}' 25 | self.assertEqual(author, 'testuser1') 26 | self.assertEqual(title, 'Blog title') 27 | self.assertEqual(body, 'Body content...') 28 | -------------------------------------------------------------------------------- /ch6-blog-api-permissions/posts/urls.py: -------------------------------------------------------------------------------- 1 | from django.urls import path 2 | from .views import PostList, PostDetail 3 | 4 | urlpatterns = [ 5 | path('/', PostDetail.as_view()), 6 | path('', PostList.as_view()), 7 | ] 8 | -------------------------------------------------------------------------------- /ch6-blog-api-permissions/posts/views.py: -------------------------------------------------------------------------------- 1 | from rest_framework import generics 2 | from .models import Post 3 | from .permissions import IsAuthorOrReadOnly # new 4 | from .serializers import PostSerializer 5 | 6 | 7 | class PostList(generics.ListCreateAPIView): 8 | queryset = Post.objects.all() 9 | serializer_class = PostSerializer 10 | 11 | 12 | class PostDetail(generics.RetrieveUpdateDestroyAPIView): 13 | permission_classes = (IsAuthorOrReadOnly,) # new 14 | queryset = Post.objects.all() 15 | serializer_class = PostSerializer 16 | -------------------------------------------------------------------------------- /ch7-user-authentication/Pipfile: -------------------------------------------------------------------------------- 1 | [[source]] 2 | name = "pypi" 3 | url = "https://pypi.org/simple" 4 | verify_ssl = true 5 | 6 | [dev-packages] 7 | 8 | [packages] 9 | django = "~=3.1.0" 10 | djangorestframework = "==3.11.0" 11 | dj-rest-auth = "==1.1.0" 12 | django-allauth = "==0.42.0" 13 | 14 | [requires] 15 | python_version = "3.8" 16 | -------------------------------------------------------------------------------- /ch7-user-authentication/Pipfile.lock: -------------------------------------------------------------------------------- 1 | { 2 | "_meta": { 3 | "hash": { 4 | "sha256": "fb3781b26021056fc12f3ac404b43c8a29828064dfd593b34b1c9b3e6a4af2a6" 5 | }, 6 | "pipfile-spec": 6, 7 | "requires": { 8 | "python_version": "3.8" 9 | }, 10 | "sources": [ 11 | { 12 | "name": "pypi", 13 | "url": "https://pypi.org/simple", 14 | "verify_ssl": true 15 | } 16 | ] 17 | }, 18 | "default": { 19 | "asgiref": { 20 | "hashes": [ 21 | "sha256:7e51911ee147dd685c3c8b805c0ad0cb58d360987b56953878f8c06d2d1c6f1a", 22 | "sha256:9fc6fb5d39b8af147ba40765234fa822b39818b12cc80b35ad9b0cef3a476aed" 23 | ], 24 | "markers": "python_version >= '3.5'", 25 | "version": "==3.2.10" 26 | }, 27 | "certifi": { 28 | "hashes": [ 29 | "sha256:5930595817496dd21bb8dc35dad090f1c2cd0adfaf21204bf6732ca5d8ee34d3", 30 | "sha256:8fc0819f1f30ba15bdb34cceffb9ef04d99f420f68eb75d901e9560b8749fc41" 31 | ], 32 | "version": "==2020.6.20" 33 | }, 34 | "chardet": { 35 | "hashes": [ 36 | "sha256:84ab92ed1c4d4f16916e05906b6b75a6c0fb5db821cc65e70cbd64a3e2a5eaae", 37 | "sha256:fc323ffcaeaed0e0a02bf4d117757b98aed530d9ed4531e3e15460124c106691" 38 | ], 39 | "version": "==3.0.4" 40 | }, 41 | "defusedxml": { 42 | "hashes": [ 43 | "sha256:6687150770438374ab581bb7a1b327a847dd9c5749e396102de3fad4e8a3ef93", 44 | "sha256:f684034d135af4c6cbb949b8a4d2ed61634515257a67299e5f940fbaa34377f5" 45 | ], 46 | "markers": "python_version >= '2.7' and python_version not in '3.0, 3.1, 3.2, 3.3, 3.4'", 47 | "version": "==0.6.0" 48 | }, 49 | "dj-rest-auth": { 50 | "hashes": [ 51 | "sha256:f058a822ffa8764dea58750d415d3a43f038fe79acd0f5dc473704393cc5d65d" 52 | ], 53 | "index": "pypi", 54 | "version": "==1.1.0" 55 | }, 56 | "django": { 57 | "hashes": [ 58 | "sha256:1a63f5bb6ff4d7c42f62a519edc2adbb37f9b78068a5a862beff858b68e3dc8b", 59 | "sha256:2d390268a13c655c97e0e2ede9d117007996db692c1bb93eabebd4fb7ea7012b" 60 | ], 61 | "index": "pypi", 62 | "version": "==3.1" 63 | }, 64 | "django-allauth": { 65 | "hashes": [ 66 | "sha256:f17209410b7f87da0a84639fd79d3771b596a6d3fc1a8e48ce50dabc7f441d30" 67 | ], 68 | "index": "pypi", 69 | "version": "==0.42.0" 70 | }, 71 | "djangorestframework": { 72 | "hashes": [ 73 | "sha256:05809fc66e1c997fd9a32ea5730d9f4ba28b109b9da71fccfa5ff241201fd0a4", 74 | "sha256:e782087823c47a26826ee5b6fa0c542968219263fb3976ec3c31edab23a4001f" 75 | ], 76 | "index": "pypi", 77 | "version": "==3.11.0" 78 | }, 79 | "idna": { 80 | "hashes": [ 81 | "sha256:b307872f855b18632ce0c21c5e45be78c0ea7ae4c15c828c20788b26921eb3f6", 82 | "sha256:b97d804b1e9b523befed77c48dacec60e6dcb0b5391d57af6a65a312a90648c0" 83 | ], 84 | "markers": "python_version >= '2.7' and python_version not in '3.0, 3.1, 3.2, 3.3'", 85 | "version": "==2.10" 86 | }, 87 | "oauthlib": { 88 | "hashes": [ 89 | "sha256:bee41cc35fcca6e988463cacc3bcb8a96224f470ca547e697b604cc697b2f889", 90 | "sha256:df884cd6cbe20e32633f1db1072e9356f53638e4361bef4e8b03c9127c9328ea" 91 | ], 92 | "markers": "python_version >= '2.7' and python_version not in '3.0, 3.1, 3.2, 3.3'", 93 | "version": "==3.1.0" 94 | }, 95 | "python3-openid": { 96 | "hashes": [ 97 | "sha256:33fbf6928f401e0b790151ed2b5290b02545e8775f982485205a066f874aaeaf", 98 | "sha256:6626f771e0417486701e0b4daff762e7212e820ca5b29fcc0d05f6f8736dfa6b" 99 | ], 100 | "version": "==3.2.0" 101 | }, 102 | "pytz": { 103 | "hashes": [ 104 | "sha256:a494d53b6d39c3c6e44c3bec237336e14305e4f29bbf800b599253057fbb79ed", 105 | "sha256:c35965d010ce31b23eeb663ed3cc8c906275d6be1a34393a1d73a41febf4a048" 106 | ], 107 | "version": "==2020.1" 108 | }, 109 | "requests": { 110 | "hashes": [ 111 | "sha256:b3559a131db72c33ee969480840fff4bb6dd111de7dd27c8ee1f820f4f00231b", 112 | "sha256:fe75cc94a9443b9246fc7049224f75604b113c36acb93f87b80ed42c44cbb898" 113 | ], 114 | "markers": "python_version >= '2.7' and python_version not in '3.0, 3.1, 3.2, 3.3, 3.4'", 115 | "version": "==2.24.0" 116 | }, 117 | "requests-oauthlib": { 118 | "hashes": [ 119 | "sha256:7f71572defaecd16372f9006f33c2ec8c077c3cfa6f5911a9a90202beb513f3d", 120 | "sha256:b4261601a71fd721a8bd6d7aa1cc1d6a8a93b4a9f5e96626f8e4d91e8beeaa6a", 121 | "sha256:fa6c47b933f01060936d87ae9327fead68768b69c6c9ea2109c48be30f2d4dbc" 122 | ], 123 | "version": "==1.3.0" 124 | }, 125 | "sqlparse": { 126 | "hashes": [ 127 | "sha256:022fb9c87b524d1f7862b3037e541f68597a730a8843245c349fc93e1643dc4e", 128 | "sha256:e162203737712307dfe78860cc56c8da8a852ab2ee33750e33aeadf38d12c548" 129 | ], 130 | "markers": "python_version >= '2.7' and python_version not in '3.0, 3.1, 3.2, 3.3'", 131 | "version": "==0.3.1" 132 | }, 133 | "urllib3": { 134 | "hashes": [ 135 | "sha256:91056c15fa70756691db97756772bb1eb9678fa585d9184f24534b100dc60f4a", 136 | "sha256:e7983572181f5e1522d9c98453462384ee92a0be7fac5f1413a1e35c56cc0461" 137 | ], 138 | "markers": "python_version >= '2.7' and python_version not in '3.0, 3.1, 3.2, 3.3, 3.4' and python_version < '4'", 139 | "version": "==1.25.10" 140 | } 141 | }, 142 | "develop": {} 143 | } 144 | -------------------------------------------------------------------------------- /ch7-user-authentication/config/__init__.py: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/wsvincent/djangoforapis_31/c678537076d809f4fea19f0beb5e20e3be8661e2/ch7-user-authentication/config/__init__.py -------------------------------------------------------------------------------- /ch7-user-authentication/config/asgi.py: -------------------------------------------------------------------------------- 1 | """ 2 | ASGI config for config project. 3 | 4 | It exposes the ASGI callable as a module-level variable named ``application``. 5 | 6 | For more information on this file, see 7 | https://docs.djangoproject.com/en/dev/howto/deployment/asgi/ 8 | """ 9 | 10 | import os 11 | 12 | from django.core.asgi import get_asgi_application 13 | 14 | os.environ.setdefault('DJANGO_SETTINGS_MODULE', 'config.settings') 15 | 16 | application = get_asgi_application() 17 | -------------------------------------------------------------------------------- /ch7-user-authentication/config/settings.py: -------------------------------------------------------------------------------- 1 | """ 2 | Django settings for config project. 3 | 4 | Generated by 'django-admin startproject' using Django 3.1rc1. 5 | 6 | For more information on this file, see 7 | https://docs.djangoproject.com/en/dev/topics/settings/ 8 | 9 | For the full list of settings and their values, see 10 | https://docs.djangoproject.com/en/dev/ref/settings/ 11 | """ 12 | 13 | from pathlib import Path 14 | 15 | # Build paths inside the project like this: BASE_DIR / 'subdir'. 16 | BASE_DIR = Path(__file__).resolve(strict=True).parent.parent 17 | 18 | 19 | # Quick-start development settings - unsuitable for production 20 | # See https://docs.djangoproject.com/en/dev/howto/deployment/checklist/ 21 | 22 | # SECURITY WARNING: keep the secret key used in production secret! 23 | SECRET_KEY = '5kji9d4+u2=pw4k2bh+v4pt7(57)$hsns-g_ma0oskgf*aa16&' 24 | 25 | # SECURITY WARNING: don't run with debug turned on in production! 26 | DEBUG = True 27 | 28 | ALLOWED_HOSTS = [] 29 | 30 | 31 | # Application definition 32 | 33 | INSTALLED_APPS = [ 34 | 'django.contrib.admin', 35 | 'django.contrib.auth', 36 | 'django.contrib.contenttypes', 37 | 'django.contrib.sessions', 38 | 'django.contrib.messages', 39 | 'django.contrib.staticfiles', 40 | 'django.contrib.sites', # new 41 | 42 | # 3rd-party apps 43 | 'rest_framework', 44 | 'rest_framework.authtoken', 45 | 'allauth', # new 46 | 'allauth.account', # new 47 | 'allauth.socialaccount', # new 48 | 'dj_rest_auth', 49 | 'dj_rest_auth.registration', # new 50 | 51 | # Local 52 | 'posts', 53 | ] 54 | 55 | MIDDLEWARE = [ 56 | 'django.middleware.security.SecurityMiddleware', 57 | 'django.contrib.sessions.middleware.SessionMiddleware', 58 | 'django.middleware.common.CommonMiddleware', 59 | 'django.middleware.csrf.CsrfViewMiddleware', 60 | 'django.contrib.auth.middleware.AuthenticationMiddleware', 61 | 'django.contrib.messages.middleware.MessageMiddleware', 62 | 'django.middleware.clickjacking.XFrameOptionsMiddleware', 63 | ] 64 | 65 | ROOT_URLCONF = 'config.urls' 66 | 67 | TEMPLATES = [ 68 | { 69 | 'BACKEND': 'django.template.backends.django.DjangoTemplates', 70 | 'DIRS': [], 71 | 'APP_DIRS': True, 72 | 'OPTIONS': { 73 | 'context_processors': [ 74 | 'django.template.context_processors.debug', 75 | 'django.template.context_processors.request', 76 | 'django.contrib.auth.context_processors.auth', 77 | 'django.contrib.messages.context_processors.messages', 78 | ], 79 | }, 80 | }, 81 | ] 82 | 83 | WSGI_APPLICATION = 'config.wsgi.application' 84 | 85 | 86 | # Database 87 | # https://docs.djangoproject.com/en/dev/ref/settings/#databases 88 | 89 | DATABASES = { 90 | 'default': { 91 | 'ENGINE': 'django.db.backends.sqlite3', 92 | 'NAME': BASE_DIR / 'db.sqlite3', 93 | } 94 | } 95 | 96 | 97 | # Password validation 98 | # https://docs.djangoproject.com/en/dev/ref/settings/#auth-password-validators 99 | 100 | AUTH_PASSWORD_VALIDATORS = [ 101 | { 102 | 'NAME': 'django.contrib.auth.password_validation.UserAttributeSimilarityValidator', 103 | }, 104 | { 105 | 'NAME': 'django.contrib.auth.password_validation.MinimumLengthValidator', 106 | }, 107 | { 108 | 'NAME': 'django.contrib.auth.password_validation.CommonPasswordValidator', 109 | }, 110 | { 111 | 'NAME': 'django.contrib.auth.password_validation.NumericPasswordValidator', 112 | }, 113 | ] 114 | 115 | 116 | # Internationalization 117 | # https://docs.djangoproject.com/en/dev/topics/i18n/ 118 | 119 | LANGUAGE_CODE = 'en-us' 120 | 121 | TIME_ZONE = 'UTC' 122 | 123 | USE_I18N = True 124 | 125 | USE_L10N = True 126 | 127 | USE_TZ = True 128 | 129 | 130 | # Static files (CSS, JavaScript, Images) 131 | # https://docs.djangoproject.com/en/dev/howto/static-files/ 132 | 133 | STATIC_URL = '/static/' 134 | 135 | REST_FRAMEWORK = { 136 | 'DEFAULT_PERMISSION_CLASSES': [ 137 | 'rest_framework.permissions.IsAuthenticated', 138 | ], 139 | 'DEFAULT_AUTHENTICATION_CLASSES': [ 140 | 'rest_framework.authentication.SessionAuthentication', 141 | 'rest_framework.authentication.TokenAuthentication', # new 142 | ], 143 | } 144 | 145 | EMAIL_BACKEND = 'django.core.mail.backends.console.EmailBackend' # new 146 | 147 | SITE_ID = 1 # new 148 | -------------------------------------------------------------------------------- /ch7-user-authentication/config/urls.py: -------------------------------------------------------------------------------- 1 | from django.contrib import admin 2 | from django.urls import include, path 3 | 4 | 5 | urlpatterns = [ 6 | path('admin/', admin.site.urls), 7 | path('api/v1/', include('posts.urls')), 8 | path('api-auth/', include('rest_framework.urls')), 9 | path('api/v1/dj-rest-auth/', include('dj_rest_auth.urls')), 10 | path('api/v1/dj-rest-auth/registration/', # new 11 | include('dj_rest_auth.registration.urls')), 12 | ] 13 | -------------------------------------------------------------------------------- /ch7-user-authentication/config/wsgi.py: -------------------------------------------------------------------------------- 1 | """ 2 | WSGI config for config 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/dev/howto/deployment/wsgi/ 8 | """ 9 | 10 | import os 11 | 12 | from django.core.wsgi import get_wsgi_application 13 | 14 | os.environ.setdefault('DJANGO_SETTINGS_MODULE', 'config.settings') 15 | 16 | application = get_wsgi_application() 17 | -------------------------------------------------------------------------------- /ch7-user-authentication/db.sqlite3: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/wsvincent/djangoforapis_31/c678537076d809f4fea19f0beb5e20e3be8661e2/ch7-user-authentication/db.sqlite3 -------------------------------------------------------------------------------- /ch7-user-authentication/manage.py: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env python 2 | """Django's command-line utility for administrative tasks.""" 3 | import os 4 | import sys 5 | 6 | 7 | def main(): 8 | """Run administrative tasks.""" 9 | os.environ.setdefault('DJANGO_SETTINGS_MODULE', 'config.settings') 10 | try: 11 | from django.core.management import execute_from_command_line 12 | except ImportError as exc: 13 | raise ImportError( 14 | "Couldn't import Django. Are you sure it's installed and " 15 | "available on your PYTHONPATH environment variable? Did you " 16 | "forget to activate a virtual environment?" 17 | ) from exc 18 | execute_from_command_line(sys.argv) 19 | 20 | 21 | if __name__ == '__main__': 22 | main() 23 | -------------------------------------------------------------------------------- /ch7-user-authentication/posts/__init__.py: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/wsvincent/djangoforapis_31/c678537076d809f4fea19f0beb5e20e3be8661e2/ch7-user-authentication/posts/__init__.py -------------------------------------------------------------------------------- /ch7-user-authentication/posts/admin.py: -------------------------------------------------------------------------------- 1 | from django.contrib import admin 2 | from .models import Post 3 | 4 | admin.site.register(Post) 5 | -------------------------------------------------------------------------------- /ch7-user-authentication/posts/apps.py: -------------------------------------------------------------------------------- 1 | from django.apps import AppConfig 2 | 3 | 4 | class PostsConfig(AppConfig): 5 | name = 'posts' 6 | -------------------------------------------------------------------------------- /ch7-user-authentication/posts/migrations/0001_initial.py: -------------------------------------------------------------------------------- 1 | # Generated by Django 3.1rc1 on 2020-07-29 17:28 2 | 3 | from django.conf import settings 4 | from django.db import migrations, models 5 | import django.db.models.deletion 6 | 7 | 8 | class Migration(migrations.Migration): 9 | 10 | initial = True 11 | 12 | dependencies = [ 13 | migrations.swappable_dependency(settings.AUTH_USER_MODEL), 14 | ] 15 | 16 | operations = [ 17 | migrations.CreateModel( 18 | name='Post', 19 | fields=[ 20 | ('id', models.AutoField(auto_created=True, primary_key=True, serialize=False, verbose_name='ID')), 21 | ('title', models.CharField(max_length=50)), 22 | ('body', models.TextField()), 23 | ('created_at', models.DateTimeField(auto_now_add=True)), 24 | ('updated_at', models.DateTimeField(auto_now=True)), 25 | ('author', models.ForeignKey(on_delete=django.db.models.deletion.CASCADE, to=settings.AUTH_USER_MODEL)), 26 | ], 27 | ), 28 | ] 29 | -------------------------------------------------------------------------------- /ch7-user-authentication/posts/migrations/__init__.py: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/wsvincent/djangoforapis_31/c678537076d809f4fea19f0beb5e20e3be8661e2/ch7-user-authentication/posts/migrations/__init__.py -------------------------------------------------------------------------------- /ch7-user-authentication/posts/models.py: -------------------------------------------------------------------------------- 1 | from django.db import models 2 | from django.contrib.auth.models import User 3 | 4 | 5 | class Post(models.Model): 6 | author = models.ForeignKey(User, on_delete=models.CASCADE) 7 | title = models.CharField(max_length=50) 8 | body = models.TextField() 9 | created_at = models.DateTimeField(auto_now_add=True) 10 | updated_at = models.DateTimeField(auto_now=True) 11 | 12 | def __str__(self): 13 | return self.title 14 | -------------------------------------------------------------------------------- /ch7-user-authentication/posts/permissions.py: -------------------------------------------------------------------------------- 1 | from rest_framework import permissions 2 | 3 | 4 | class IsAuthorOrReadOnly(permissions.BasePermission): 5 | 6 | def has_object_permission(self, request, view, obj): 7 | # Read-only permissions are allowed for any request 8 | if request.method in permissions.SAFE_METHODS: 9 | return True 10 | 11 | # Write permissions are only allowed to the author of a post 12 | return obj.author == request.user 13 | -------------------------------------------------------------------------------- /ch7-user-authentication/posts/serializers.py: -------------------------------------------------------------------------------- 1 | from rest_framework import serializers 2 | from .models import Post 3 | 4 | 5 | class PostSerializer(serializers.ModelSerializer): 6 | 7 | class Meta: 8 | fields = ('id', 'author', 'title', 'body', 'created_at',) 9 | model = Post 10 | -------------------------------------------------------------------------------- /ch7-user-authentication/posts/tests.py: -------------------------------------------------------------------------------- 1 | from django.test import TestCase 2 | from django.contrib.auth.models import User 3 | from .models import Post 4 | 5 | 6 | class BlogTests(TestCase): 7 | 8 | @classmethod 9 | def setUpTestData(cls): 10 | # Create a user 11 | testuser1 = User.objects.create_user( 12 | username='testuser1', password='abc123') 13 | testuser1.save() 14 | 15 | # Create a blog post 16 | test_post = Post.objects.create( 17 | author=testuser1, title='Blog title', body='Body content...') 18 | test_post.save() 19 | 20 | def test_blog_content(self): 21 | post = Post.objects.get(id=1) 22 | author = f'{post.author}' 23 | title = f'{post.title}' 24 | body = f'{post.body}' 25 | self.assertEqual(author, 'testuser1') 26 | self.assertEqual(title, 'Blog title') 27 | self.assertEqual(body, 'Body content...') 28 | -------------------------------------------------------------------------------- /ch7-user-authentication/posts/urls.py: -------------------------------------------------------------------------------- 1 | from django.urls import path 2 | from .views import PostList, PostDetail 3 | 4 | urlpatterns = [ 5 | path('/', PostDetail.as_view()), 6 | path('', PostList.as_view()), 7 | ] 8 | -------------------------------------------------------------------------------- /ch7-user-authentication/posts/views.py: -------------------------------------------------------------------------------- 1 | from rest_framework import generics 2 | from .models import Post 3 | from .permissions import IsAuthorOrReadOnly # new 4 | from .serializers import PostSerializer 5 | 6 | 7 | class PostList(generics.ListCreateAPIView): 8 | queryset = Post.objects.all() 9 | serializer_class = PostSerializer 10 | 11 | 12 | class PostDetail(generics.RetrieveUpdateDestroyAPIView): 13 | permission_classes = (IsAuthorOrReadOnly,) # new 14 | queryset = Post.objects.all() 15 | serializer_class = PostSerializer 16 | -------------------------------------------------------------------------------- /ch8-viewsets-and-routers/Pipfile: -------------------------------------------------------------------------------- 1 | [[source]] 2 | name = "pypi" 3 | url = "https://pypi.org/simple" 4 | verify_ssl = true 5 | 6 | [dev-packages] 7 | 8 | [packages] 9 | django = "~=3.1.0" 10 | djangorestframework = "==3.11.0" 11 | dj-rest-auth = "==1.1.0" 12 | django-allauth = "~=0.42.0" 13 | 14 | [requires] 15 | python_version = "3.8" 16 | -------------------------------------------------------------------------------- /ch8-viewsets-and-routers/Pipfile.lock: -------------------------------------------------------------------------------- 1 | { 2 | "_meta": { 3 | "hash": { 4 | "sha256": "1ea139dc2bd198cb5e43732c8ab669d8f2c9444f61123cb10ce1b4a76961269b" 5 | }, 6 | "pipfile-spec": 6, 7 | "requires": { 8 | "python_version": "3.8" 9 | }, 10 | "sources": [ 11 | { 12 | "name": "pypi", 13 | "url": "https://pypi.org/simple", 14 | "verify_ssl": true 15 | } 16 | ] 17 | }, 18 | "default": { 19 | "asgiref": { 20 | "hashes": [ 21 | "sha256:7e51911ee147dd685c3c8b805c0ad0cb58d360987b56953878f8c06d2d1c6f1a", 22 | "sha256:9fc6fb5d39b8af147ba40765234fa822b39818b12cc80b35ad9b0cef3a476aed" 23 | ], 24 | "markers": "python_version >= '3.5'", 25 | "version": "==3.2.10" 26 | }, 27 | "certifi": { 28 | "hashes": [ 29 | "sha256:5930595817496dd21bb8dc35dad090f1c2cd0adfaf21204bf6732ca5d8ee34d3", 30 | "sha256:8fc0819f1f30ba15bdb34cceffb9ef04d99f420f68eb75d901e9560b8749fc41" 31 | ], 32 | "version": "==2020.6.20" 33 | }, 34 | "chardet": { 35 | "hashes": [ 36 | "sha256:84ab92ed1c4d4f16916e05906b6b75a6c0fb5db821cc65e70cbd64a3e2a5eaae", 37 | "sha256:fc323ffcaeaed0e0a02bf4d117757b98aed530d9ed4531e3e15460124c106691" 38 | ], 39 | "version": "==3.0.4" 40 | }, 41 | "defusedxml": { 42 | "hashes": [ 43 | "sha256:6687150770438374ab581bb7a1b327a847dd9c5749e396102de3fad4e8a3ef93", 44 | "sha256:f684034d135af4c6cbb949b8a4d2ed61634515257a67299e5f940fbaa34377f5" 45 | ], 46 | "markers": "python_version >= '2.7' and python_version not in '3.0, 3.1, 3.2, 3.3, 3.4'", 47 | "version": "==0.6.0" 48 | }, 49 | "dj-rest-auth": { 50 | "hashes": [ 51 | "sha256:f058a822ffa8764dea58750d415d3a43f038fe79acd0f5dc473704393cc5d65d" 52 | ], 53 | "index": "pypi", 54 | "version": "==1.1.0" 55 | }, 56 | "django": { 57 | "hashes": [ 58 | "sha256:1a63f5bb6ff4d7c42f62a519edc2adbb37f9b78068a5a862beff858b68e3dc8b", 59 | "sha256:2d390268a13c655c97e0e2ede9d117007996db692c1bb93eabebd4fb7ea7012b" 60 | ], 61 | "index": "pypi", 62 | "version": "==3.1" 63 | }, 64 | "django-allauth": { 65 | "hashes": [ 66 | "sha256:f17209410b7f87da0a84639fd79d3771b596a6d3fc1a8e48ce50dabc7f441d30" 67 | ], 68 | "index": "pypi", 69 | "version": "==0.42.0" 70 | }, 71 | "djangorestframework": { 72 | "hashes": [ 73 | "sha256:05809fc66e1c997fd9a32ea5730d9f4ba28b109b9da71fccfa5ff241201fd0a4", 74 | "sha256:e782087823c47a26826ee5b6fa0c542968219263fb3976ec3c31edab23a4001f" 75 | ], 76 | "index": "pypi", 77 | "version": "==3.11.0" 78 | }, 79 | "idna": { 80 | "hashes": [ 81 | "sha256:b307872f855b18632ce0c21c5e45be78c0ea7ae4c15c828c20788b26921eb3f6", 82 | "sha256:b97d804b1e9b523befed77c48dacec60e6dcb0b5391d57af6a65a312a90648c0" 83 | ], 84 | "markers": "python_version >= '2.7' and python_version not in '3.0, 3.1, 3.2, 3.3'", 85 | "version": "==2.10" 86 | }, 87 | "oauthlib": { 88 | "hashes": [ 89 | "sha256:bee41cc35fcca6e988463cacc3bcb8a96224f470ca547e697b604cc697b2f889", 90 | "sha256:df884cd6cbe20e32633f1db1072e9356f53638e4361bef4e8b03c9127c9328ea" 91 | ], 92 | "markers": "python_version >= '2.7' and python_version not in '3.0, 3.1, 3.2, 3.3'", 93 | "version": "==3.1.0" 94 | }, 95 | "python3-openid": { 96 | "hashes": [ 97 | "sha256:33fbf6928f401e0b790151ed2b5290b02545e8775f982485205a066f874aaeaf", 98 | "sha256:6626f771e0417486701e0b4daff762e7212e820ca5b29fcc0d05f6f8736dfa6b" 99 | ], 100 | "version": "==3.2.0" 101 | }, 102 | "pytz": { 103 | "hashes": [ 104 | "sha256:a494d53b6d39c3c6e44c3bec237336e14305e4f29bbf800b599253057fbb79ed", 105 | "sha256:c35965d010ce31b23eeb663ed3cc8c906275d6be1a34393a1d73a41febf4a048" 106 | ], 107 | "version": "==2020.1" 108 | }, 109 | "requests": { 110 | "hashes": [ 111 | "sha256:b3559a131db72c33ee969480840fff4bb6dd111de7dd27c8ee1f820f4f00231b", 112 | "sha256:fe75cc94a9443b9246fc7049224f75604b113c36acb93f87b80ed42c44cbb898" 113 | ], 114 | "markers": "python_version >= '2.7' and python_version not in '3.0, 3.1, 3.2, 3.3, 3.4'", 115 | "version": "==2.24.0" 116 | }, 117 | "requests-oauthlib": { 118 | "hashes": [ 119 | "sha256:7f71572defaecd16372f9006f33c2ec8c077c3cfa6f5911a9a90202beb513f3d", 120 | "sha256:b4261601a71fd721a8bd6d7aa1cc1d6a8a93b4a9f5e96626f8e4d91e8beeaa6a", 121 | "sha256:fa6c47b933f01060936d87ae9327fead68768b69c6c9ea2109c48be30f2d4dbc" 122 | ], 123 | "version": "==1.3.0" 124 | }, 125 | "sqlparse": { 126 | "hashes": [ 127 | "sha256:022fb9c87b524d1f7862b3037e541f68597a730a8843245c349fc93e1643dc4e", 128 | "sha256:e162203737712307dfe78860cc56c8da8a852ab2ee33750e33aeadf38d12c548" 129 | ], 130 | "markers": "python_version >= '2.7' and python_version not in '3.0, 3.1, 3.2, 3.3'", 131 | "version": "==0.3.1" 132 | }, 133 | "urllib3": { 134 | "hashes": [ 135 | "sha256:91056c15fa70756691db97756772bb1eb9678fa585d9184f24534b100dc60f4a", 136 | "sha256:e7983572181f5e1522d9c98453462384ee92a0be7fac5f1413a1e35c56cc0461" 137 | ], 138 | "markers": "python_version >= '2.7' and python_version not in '3.0, 3.1, 3.2, 3.3, 3.4' and python_version < '4'", 139 | "version": "==1.25.10" 140 | } 141 | }, 142 | "develop": {} 143 | } 144 | -------------------------------------------------------------------------------- /ch8-viewsets-and-routers/config/__init__.py: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/wsvincent/djangoforapis_31/c678537076d809f4fea19f0beb5e20e3be8661e2/ch8-viewsets-and-routers/config/__init__.py -------------------------------------------------------------------------------- /ch8-viewsets-and-routers/config/asgi.py: -------------------------------------------------------------------------------- 1 | """ 2 | ASGI config for config project. 3 | 4 | It exposes the ASGI callable as a module-level variable named ``application``. 5 | 6 | For more information on this file, see 7 | https://docs.djangoproject.com/en/dev/howto/deployment/asgi/ 8 | """ 9 | 10 | import os 11 | 12 | from django.core.asgi import get_asgi_application 13 | 14 | os.environ.setdefault('DJANGO_SETTINGS_MODULE', 'config.settings') 15 | 16 | application = get_asgi_application() 17 | -------------------------------------------------------------------------------- /ch8-viewsets-and-routers/config/settings.py: -------------------------------------------------------------------------------- 1 | """ 2 | Django settings for config project. 3 | 4 | Generated by 'django-admin startproject' using Django 3.1rc1. 5 | 6 | For more information on this file, see 7 | https://docs.djangoproject.com/en/dev/topics/settings/ 8 | 9 | For the full list of settings and their values, see 10 | https://docs.djangoproject.com/en/dev/ref/settings/ 11 | """ 12 | 13 | from pathlib import Path 14 | 15 | # Build paths inside the project like this: BASE_DIR / 'subdir'. 16 | BASE_DIR = Path(__file__).resolve(strict=True).parent.parent 17 | 18 | 19 | # Quick-start development settings - unsuitable for production 20 | # See https://docs.djangoproject.com/en/dev/howto/deployment/checklist/ 21 | 22 | # SECURITY WARNING: keep the secret key used in production secret! 23 | SECRET_KEY = '5kji9d4+u2=pw4k2bh+v4pt7(57)$hsns-g_ma0oskgf*aa16&' 24 | 25 | # SECURITY WARNING: don't run with debug turned on in production! 26 | DEBUG = True 27 | 28 | ALLOWED_HOSTS = [] 29 | 30 | 31 | # Application definition 32 | 33 | INSTALLED_APPS = [ 34 | 'django.contrib.admin', 35 | 'django.contrib.auth', 36 | 'django.contrib.contenttypes', 37 | 'django.contrib.sessions', 38 | 'django.contrib.messages', 39 | 'django.contrib.staticfiles', 40 | 'django.contrib.sites', # new 41 | 42 | # 3rd-party apps 43 | 'rest_framework', 44 | 'rest_framework.authtoken', 45 | 'allauth', # new 46 | 'allauth.account', # new 47 | 'allauth.socialaccount', # new 48 | 'dj_rest_auth', 49 | 'dj_rest_auth.registration', # new 50 | 51 | # Local 52 | 'posts', 53 | ] 54 | 55 | MIDDLEWARE = [ 56 | 'django.middleware.security.SecurityMiddleware', 57 | 'django.contrib.sessions.middleware.SessionMiddleware', 58 | 'django.middleware.common.CommonMiddleware', 59 | 'django.middleware.csrf.CsrfViewMiddleware', 60 | 'django.contrib.auth.middleware.AuthenticationMiddleware', 61 | 'django.contrib.messages.middleware.MessageMiddleware', 62 | 'django.middleware.clickjacking.XFrameOptionsMiddleware', 63 | ] 64 | 65 | ROOT_URLCONF = 'config.urls' 66 | 67 | TEMPLATES = [ 68 | { 69 | 'BACKEND': 'django.template.backends.django.DjangoTemplates', 70 | 'DIRS': [], 71 | 'APP_DIRS': True, 72 | 'OPTIONS': { 73 | 'context_processors': [ 74 | 'django.template.context_processors.debug', 75 | 'django.template.context_processors.request', 76 | 'django.contrib.auth.context_processors.auth', 77 | 'django.contrib.messages.context_processors.messages', 78 | ], 79 | }, 80 | }, 81 | ] 82 | 83 | WSGI_APPLICATION = 'config.wsgi.application' 84 | 85 | 86 | # Database 87 | # https://docs.djangoproject.com/en/dev/ref/settings/#databases 88 | 89 | DATABASES = { 90 | 'default': { 91 | 'ENGINE': 'django.db.backends.sqlite3', 92 | 'NAME': BASE_DIR / 'db.sqlite3', 93 | } 94 | } 95 | 96 | 97 | # Password validation 98 | # https://docs.djangoproject.com/en/dev/ref/settings/#auth-password-validators 99 | 100 | AUTH_PASSWORD_VALIDATORS = [ 101 | { 102 | 'NAME': 'django.contrib.auth.password_validation.UserAttributeSimilarityValidator', 103 | }, 104 | { 105 | 'NAME': 'django.contrib.auth.password_validation.MinimumLengthValidator', 106 | }, 107 | { 108 | 'NAME': 'django.contrib.auth.password_validation.CommonPasswordValidator', 109 | }, 110 | { 111 | 'NAME': 'django.contrib.auth.password_validation.NumericPasswordValidator', 112 | }, 113 | ] 114 | 115 | 116 | # Internationalization 117 | # https://docs.djangoproject.com/en/dev/topics/i18n/ 118 | 119 | LANGUAGE_CODE = 'en-us' 120 | 121 | TIME_ZONE = 'UTC' 122 | 123 | USE_I18N = True 124 | 125 | USE_L10N = True 126 | 127 | USE_TZ = True 128 | 129 | 130 | # Static files (CSS, JavaScript, Images) 131 | # https://docs.djangoproject.com/en/dev/howto/static-files/ 132 | 133 | STATIC_URL = '/static/' 134 | 135 | REST_FRAMEWORK = { 136 | 'DEFAULT_PERMISSION_CLASSES': [ 137 | 'rest_framework.permissions.IsAuthenticated', 138 | ], 139 | 'DEFAULT_AUTHENTICATION_CLASSES': [ 140 | 'rest_framework.authentication.SessionAuthentication', 141 | 'rest_framework.authentication.TokenAuthentication', # new 142 | ], 143 | } 144 | 145 | EMAIL_BACKEND = 'django.core.mail.backends.console.EmailBackend' # new 146 | 147 | SITE_ID = 1 # new 148 | -------------------------------------------------------------------------------- /ch8-viewsets-and-routers/config/urls.py: -------------------------------------------------------------------------------- 1 | from django.contrib import admin 2 | from django.urls import include, path 3 | 4 | 5 | urlpatterns = [ 6 | path('admin/', admin.site.urls), 7 | path('api/v1/', include('posts.urls')), 8 | path('api-auth/', include('rest_framework.urls')), 9 | path('api/v1/dj-rest-auth/', include('dj_rest_auth.urls')), 10 | path('api/v1/dj-rest-auth/registration/', # new 11 | include('dj_rest_auth.registration.urls')), 12 | ] 13 | -------------------------------------------------------------------------------- /ch8-viewsets-and-routers/config/wsgi.py: -------------------------------------------------------------------------------- 1 | """ 2 | WSGI config for config 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/dev/howto/deployment/wsgi/ 8 | """ 9 | 10 | import os 11 | 12 | from django.core.wsgi import get_wsgi_application 13 | 14 | os.environ.setdefault('DJANGO_SETTINGS_MODULE', 'config.settings') 15 | 16 | application = get_wsgi_application() 17 | -------------------------------------------------------------------------------- /ch8-viewsets-and-routers/db.sqlite3: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/wsvincent/djangoforapis_31/c678537076d809f4fea19f0beb5e20e3be8661e2/ch8-viewsets-and-routers/db.sqlite3 -------------------------------------------------------------------------------- /ch8-viewsets-and-routers/manage.py: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env python 2 | """Django's command-line utility for administrative tasks.""" 3 | import os 4 | import sys 5 | 6 | 7 | def main(): 8 | """Run administrative tasks.""" 9 | os.environ.setdefault('DJANGO_SETTINGS_MODULE', 'config.settings') 10 | try: 11 | from django.core.management import execute_from_command_line 12 | except ImportError as exc: 13 | raise ImportError( 14 | "Couldn't import Django. Are you sure it's installed and " 15 | "available on your PYTHONPATH environment variable? Did you " 16 | "forget to activate a virtual environment?" 17 | ) from exc 18 | execute_from_command_line(sys.argv) 19 | 20 | 21 | if __name__ == '__main__': 22 | main() 23 | -------------------------------------------------------------------------------- /ch8-viewsets-and-routers/posts/__init__.py: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/wsvincent/djangoforapis_31/c678537076d809f4fea19f0beb5e20e3be8661e2/ch8-viewsets-and-routers/posts/__init__.py -------------------------------------------------------------------------------- /ch8-viewsets-and-routers/posts/admin.py: -------------------------------------------------------------------------------- 1 | from django.contrib import admin 2 | from .models import Post 3 | 4 | admin.site.register(Post) 5 | -------------------------------------------------------------------------------- /ch8-viewsets-and-routers/posts/apps.py: -------------------------------------------------------------------------------- 1 | from django.apps import AppConfig 2 | 3 | 4 | class PostsConfig(AppConfig): 5 | name = 'posts' 6 | -------------------------------------------------------------------------------- /ch8-viewsets-and-routers/posts/migrations/0001_initial.py: -------------------------------------------------------------------------------- 1 | # Generated by Django 3.1rc1 on 2020-07-29 17:28 2 | 3 | from django.conf import settings 4 | from django.db import migrations, models 5 | import django.db.models.deletion 6 | 7 | 8 | class Migration(migrations.Migration): 9 | 10 | initial = True 11 | 12 | dependencies = [ 13 | migrations.swappable_dependency(settings.AUTH_USER_MODEL), 14 | ] 15 | 16 | operations = [ 17 | migrations.CreateModel( 18 | name='Post', 19 | fields=[ 20 | ('id', models.AutoField(auto_created=True, primary_key=True, serialize=False, verbose_name='ID')), 21 | ('title', models.CharField(max_length=50)), 22 | ('body', models.TextField()), 23 | ('created_at', models.DateTimeField(auto_now_add=True)), 24 | ('updated_at', models.DateTimeField(auto_now=True)), 25 | ('author', models.ForeignKey(on_delete=django.db.models.deletion.CASCADE, to=settings.AUTH_USER_MODEL)), 26 | ], 27 | ), 28 | ] 29 | -------------------------------------------------------------------------------- /ch8-viewsets-and-routers/posts/migrations/__init__.py: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/wsvincent/djangoforapis_31/c678537076d809f4fea19f0beb5e20e3be8661e2/ch8-viewsets-and-routers/posts/migrations/__init__.py -------------------------------------------------------------------------------- /ch8-viewsets-and-routers/posts/models.py: -------------------------------------------------------------------------------- 1 | from django.db import models 2 | from django.contrib.auth.models import User 3 | 4 | 5 | class Post(models.Model): 6 | author = models.ForeignKey(User, on_delete=models.CASCADE) 7 | title = models.CharField(max_length=50) 8 | body = models.TextField() 9 | created_at = models.DateTimeField(auto_now_add=True) 10 | updated_at = models.DateTimeField(auto_now=True) 11 | 12 | def __str__(self): 13 | return self.title 14 | -------------------------------------------------------------------------------- /ch8-viewsets-and-routers/posts/permissions.py: -------------------------------------------------------------------------------- 1 | from rest_framework import permissions 2 | 3 | 4 | class IsAuthorOrReadOnly(permissions.BasePermission): 5 | 6 | def has_object_permission(self, request, view, obj): 7 | # Read-only permissions are allowed for any request 8 | if request.method in permissions.SAFE_METHODS: 9 | return True 10 | 11 | # Write permissions are only allowed to the author of a post 12 | return obj.author == request.user 13 | -------------------------------------------------------------------------------- /ch8-viewsets-and-routers/posts/serializers.py: -------------------------------------------------------------------------------- 1 | from django.contrib.auth import get_user_model # new 2 | from rest_framework import serializers 3 | from .models import Post 4 | 5 | 6 | class PostSerializer(serializers.ModelSerializer): 7 | 8 | class Meta: 9 | fields = ('id', 'author', 'title', 'body', 'created_at',) 10 | model = Post 11 | 12 | 13 | class UserSerializer(serializers.ModelSerializer): # new 14 | 15 | class Meta: 16 | model = get_user_model() 17 | fields = ('id', 'username',) 18 | -------------------------------------------------------------------------------- /ch8-viewsets-and-routers/posts/tests.py: -------------------------------------------------------------------------------- 1 | from django.test import TestCase 2 | from django.contrib.auth.models import User 3 | from .models import Post 4 | 5 | 6 | class BlogTests(TestCase): 7 | 8 | @classmethod 9 | def setUpTestData(cls): 10 | # Create a user 11 | testuser1 = User.objects.create_user( 12 | username='testuser1', password='abc123') 13 | testuser1.save() 14 | 15 | # Create a blog post 16 | test_post = Post.objects.create( 17 | author=testuser1, title='Blog title', body='Body content...') 18 | test_post.save() 19 | 20 | def test_blog_content(self): 21 | post = Post.objects.get(id=1) 22 | author = f'{post.author}' 23 | title = f'{post.title}' 24 | body = f'{post.body}' 25 | self.assertEqual(author, 'testuser1') 26 | self.assertEqual(title, 'Blog title') 27 | self.assertEqual(body, 'Body content...') 28 | -------------------------------------------------------------------------------- /ch8-viewsets-and-routers/posts/urls.py: -------------------------------------------------------------------------------- 1 | from django.urls import path 2 | from rest_framework.routers import SimpleRouter 3 | from .views import UserViewSet, PostViewSet 4 | 5 | router = SimpleRouter() 6 | router.register('users', UserViewSet, basename='users') 7 | router.register('', PostViewSet, basename='posts') 8 | 9 | urlpatterns = router.urls 10 | -------------------------------------------------------------------------------- /ch8-viewsets-and-routers/posts/views.py: -------------------------------------------------------------------------------- 1 | from django.contrib.auth import get_user_model 2 | from rest_framework import viewsets # new 3 | from .models import Post 4 | from .permissions import IsAuthorOrReadOnly 5 | from .serializers import PostSerializer, UserSerializer 6 | 7 | 8 | class PostViewSet(viewsets.ModelViewSet): # new 9 | permission_classes = (IsAuthorOrReadOnly,) 10 | queryset = Post.objects.all() 11 | serializer_class = PostSerializer 12 | 13 | 14 | class UserViewSet(viewsets.ModelViewSet): # new 15 | queryset = get_user_model().objects.all() 16 | serializer_class = UserSerializer 17 | -------------------------------------------------------------------------------- /ch9-schemas-and-documentation/Pipfile: -------------------------------------------------------------------------------- 1 | [[source]] 2 | name = "pypi" 3 | url = "https://pypi.org/simple" 4 | verify_ssl = true 5 | 6 | [dev-packages] 7 | 8 | [packages] 9 | django = "~=3.1.0" 10 | djangorestframework = "==3.11.0" 11 | dj-rest-auth = "==1.1.0" 12 | django-allauth = "~=0.42.0" 13 | pyyaml = "==5.3.1" 14 | uritemplate = "==3.0.1" 15 | drf-yasg = "==1.17.1" 16 | 17 | [requires] 18 | python_version = "3.8" 19 | -------------------------------------------------------------------------------- /ch9-schemas-and-documentation/Pipfile.lock: -------------------------------------------------------------------------------- 1 | { 2 | "_meta": { 3 | "hash": { 4 | "sha256": "636607643e63d342b61e7df3cdcf6385cabc2f93d356c521b2f66b12e3a46649" 5 | }, 6 | "pipfile-spec": 6, 7 | "requires": { 8 | "python_version": "3.8" 9 | }, 10 | "sources": [ 11 | { 12 | "name": "pypi", 13 | "url": "https://pypi.org/simple", 14 | "verify_ssl": true 15 | } 16 | ] 17 | }, 18 | "default": { 19 | "asgiref": { 20 | "hashes": [ 21 | "sha256:7e51911ee147dd685c3c8b805c0ad0cb58d360987b56953878f8c06d2d1c6f1a", 22 | "sha256:9fc6fb5d39b8af147ba40765234fa822b39818b12cc80b35ad9b0cef3a476aed" 23 | ], 24 | "markers": "python_version >= '3.5'", 25 | "version": "==3.2.10" 26 | }, 27 | "certifi": { 28 | "hashes": [ 29 | "sha256:5930595817496dd21bb8dc35dad090f1c2cd0adfaf21204bf6732ca5d8ee34d3", 30 | "sha256:8fc0819f1f30ba15bdb34cceffb9ef04d99f420f68eb75d901e9560b8749fc41" 31 | ], 32 | "version": "==2020.6.20" 33 | }, 34 | "chardet": { 35 | "hashes": [ 36 | "sha256:84ab92ed1c4d4f16916e05906b6b75a6c0fb5db821cc65e70cbd64a3e2a5eaae", 37 | "sha256:fc323ffcaeaed0e0a02bf4d117757b98aed530d9ed4531e3e15460124c106691" 38 | ], 39 | "version": "==3.0.4" 40 | }, 41 | "coreapi": { 42 | "hashes": [ 43 | "sha256:46145fcc1f7017c076a2ef684969b641d18a2991051fddec9458ad3f78ffc1cb", 44 | "sha256:bf39d118d6d3e171f10df9ede5666f63ad80bba9a29a8ec17726a66cf52ee6f3" 45 | ], 46 | "version": "==2.3.3" 47 | }, 48 | "coreschema": { 49 | "hashes": [ 50 | "sha256:5e6ef7bf38c1525d5e55a895934ab4273548629f16aed5c0a6caa74ebf45551f", 51 | "sha256:9503506007d482ab0867ba14724b93c18a33b22b6d19fb419ef2d239dd4a1607" 52 | ], 53 | "version": "==0.0.4" 54 | }, 55 | "defusedxml": { 56 | "hashes": [ 57 | "sha256:6687150770438374ab581bb7a1b327a847dd9c5749e396102de3fad4e8a3ef93", 58 | "sha256:f684034d135af4c6cbb949b8a4d2ed61634515257a67299e5f940fbaa34377f5" 59 | ], 60 | "markers": "python_version >= '2.7' and python_version not in '3.0, 3.1, 3.2, 3.3, 3.4'", 61 | "version": "==0.6.0" 62 | }, 63 | "dj-rest-auth": { 64 | "hashes": [ 65 | "sha256:f058a822ffa8764dea58750d415d3a43f038fe79acd0f5dc473704393cc5d65d" 66 | ], 67 | "index": "pypi", 68 | "version": "==1.1.0" 69 | }, 70 | "django": { 71 | "hashes": [ 72 | "sha256:1a63f5bb6ff4d7c42f62a519edc2adbb37f9b78068a5a862beff858b68e3dc8b", 73 | "sha256:2d390268a13c655c97e0e2ede9d117007996db692c1bb93eabebd4fb7ea7012b" 74 | ], 75 | "index": "pypi", 76 | "version": "==3.1" 77 | }, 78 | "django-allauth": { 79 | "hashes": [ 80 | "sha256:f17209410b7f87da0a84639fd79d3771b596a6d3fc1a8e48ce50dabc7f441d30" 81 | ], 82 | "index": "pypi", 83 | "version": "==0.42.0" 84 | }, 85 | "djangorestframework": { 86 | "hashes": [ 87 | "sha256:05809fc66e1c997fd9a32ea5730d9f4ba28b109b9da71fccfa5ff241201fd0a4", 88 | "sha256:e782087823c47a26826ee5b6fa0c542968219263fb3976ec3c31edab23a4001f" 89 | ], 90 | "index": "pypi", 91 | "version": "==3.11.0" 92 | }, 93 | "drf-yasg": { 94 | "hashes": [ 95 | "sha256:5572e9d5baab9f6b49318169df9789f7399d0e3c7bdac8fdb8dfccf1d5d2b1ca", 96 | "sha256:7d7af27ad16e18507e9392b2afd6b218fbffc432ec8dbea053099a2241e184ff" 97 | ], 98 | "index": "pypi", 99 | "version": "==1.17.1" 100 | }, 101 | "idna": { 102 | "hashes": [ 103 | "sha256:b307872f855b18632ce0c21c5e45be78c0ea7ae4c15c828c20788b26921eb3f6", 104 | "sha256:b97d804b1e9b523befed77c48dacec60e6dcb0b5391d57af6a65a312a90648c0" 105 | ], 106 | "markers": "python_version >= '2.7' and python_version not in '3.0, 3.1, 3.2, 3.3'", 107 | "version": "==2.10" 108 | }, 109 | "inflection": { 110 | "hashes": [ 111 | "sha256:88b101b2668a1d81d6d72d4c2018e53bc6c7fc544c987849da1c7f77545c3bc9", 112 | "sha256:f576e85132d34f5bf7df5183c2c6f94cfb32e528f53065345cf71329ba0b8924" 113 | ], 114 | "markers": "python_version >= '3.5'", 115 | "version": "==0.5.0" 116 | }, 117 | "itypes": { 118 | "hashes": [ 119 | "sha256:03da6872ca89d29aef62773672b2d408f490f80db48b23079a4b194c86dd04c6", 120 | "sha256:af886f129dea4a2a1e3d36595a2d139589e4dd287f5cab0b40e799ee81570ff1" 121 | ], 122 | "version": "==1.2.0" 123 | }, 124 | "jinja2": { 125 | "hashes": [ 126 | "sha256:89aab215427ef59c34ad58735269eb58b1a5808103067f7bb9d5836c651b3bb0", 127 | "sha256:f0a4641d3cf955324a89c04f3d94663aa4d638abe8f733ecd3582848e1c37035" 128 | ], 129 | "markers": "python_version >= '2.7' and python_version not in '3.0, 3.1, 3.2, 3.3, 3.4'", 130 | "version": "==2.11.2" 131 | }, 132 | "markupsafe": { 133 | "hashes": [ 134 | "sha256:00bc623926325b26bb9605ae9eae8a215691f33cae5df11ca5424f06f2d1f473", 135 | "sha256:09027a7803a62ca78792ad89403b1b7a73a01c8cb65909cd876f7fcebd79b161", 136 | "sha256:09c4b7f37d6c648cb13f9230d847adf22f8171b1ccc4d5682398e77f40309235", 137 | "sha256:1027c282dad077d0bae18be6794e6b6b8c91d58ed8a8d89a89d59693b9131db5", 138 | "sha256:13d3144e1e340870b25e7b10b98d779608c02016d5184cfb9927a9f10c689f42", 139 | "sha256:24982cc2533820871eba85ba648cd53d8623687ff11cbb805be4ff7b4c971aff", 140 | "sha256:29872e92839765e546828bb7754a68c418d927cd064fd4708fab9fe9c8bb116b", 141 | "sha256:43a55c2930bbc139570ac2452adf3d70cdbb3cfe5912c71cdce1c2c6bbd9c5d1", 142 | "sha256:46c99d2de99945ec5cb54f23c8cd5689f6d7177305ebff350a58ce5f8de1669e", 143 | "sha256:500d4957e52ddc3351cabf489e79c91c17f6e0899158447047588650b5e69183", 144 | "sha256:535f6fc4d397c1563d08b88e485c3496cf5784e927af890fb3c3aac7f933ec66", 145 | "sha256:596510de112c685489095da617b5bcbbac7dd6384aeebeda4df6025d0256a81b", 146 | "sha256:62fe6c95e3ec8a7fad637b7f3d372c15ec1caa01ab47926cfdf7a75b40e0eac1", 147 | "sha256:6788b695d50a51edb699cb55e35487e430fa21f1ed838122d722e0ff0ac5ba15", 148 | "sha256:6dd73240d2af64df90aa7c4e7481e23825ea70af4b4922f8ede5b9e35f78a3b1", 149 | "sha256:717ba8fe3ae9cc0006d7c451f0bb265ee07739daf76355d06366154ee68d221e", 150 | "sha256:79855e1c5b8da654cf486b830bd42c06e8780cea587384cf6545b7d9ac013a0b", 151 | "sha256:7c1699dfe0cf8ff607dbdcc1e9b9af1755371f92a68f706051cc8c37d447c905", 152 | "sha256:88e5fcfb52ee7b911e8bb6d6aa2fd21fbecc674eadd44118a9cc3863f938e735", 153 | "sha256:8defac2f2ccd6805ebf65f5eeb132adcf2ab57aa11fdf4c0dd5169a004710e7d", 154 | "sha256:98c7086708b163d425c67c7a91bad6e466bb99d797aa64f965e9d25c12111a5e", 155 | "sha256:9add70b36c5666a2ed02b43b335fe19002ee5235efd4b8a89bfcf9005bebac0d", 156 | "sha256:9bf40443012702a1d2070043cb6291650a0841ece432556f784f004937f0f32c", 157 | "sha256:ade5e387d2ad0d7ebf59146cc00c8044acbd863725f887353a10df825fc8ae21", 158 | "sha256:b00c1de48212e4cc9603895652c5c410df699856a2853135b3967591e4beebc2", 159 | "sha256:b1282f8c00509d99fef04d8ba936b156d419be841854fe901d8ae224c59f0be5", 160 | "sha256:b2051432115498d3562c084a49bba65d97cf251f5a331c64a12ee7e04dacc51b", 161 | "sha256:ba59edeaa2fc6114428f1637ffff42da1e311e29382d81b339c1817d37ec93c6", 162 | "sha256:c8716a48d94b06bb3b2524c2b77e055fb313aeb4ea620c8dd03a105574ba704f", 163 | "sha256:cd5df75523866410809ca100dc9681e301e3c27567cf498077e8551b6d20e42f", 164 | "sha256:cdb132fc825c38e1aeec2c8aa9338310d29d337bebbd7baa06889d09a60a1fa2", 165 | "sha256:e249096428b3ae81b08327a63a485ad0878de3fb939049038579ac0ef61e17e7", 166 | "sha256:e8313f01ba26fbbe36c7be1966a7b7424942f670f38e666995b88d012765b9be" 167 | ], 168 | "markers": "python_version >= '2.7' and python_version not in '3.0, 3.1, 3.2, 3.3'", 169 | "version": "==1.1.1" 170 | }, 171 | "oauthlib": { 172 | "hashes": [ 173 | "sha256:bee41cc35fcca6e988463cacc3bcb8a96224f470ca547e697b604cc697b2f889", 174 | "sha256:df884cd6cbe20e32633f1db1072e9356f53638e4361bef4e8b03c9127c9328ea" 175 | ], 176 | "markers": "python_version >= '2.7' and python_version not in '3.0, 3.1, 3.2, 3.3'", 177 | "version": "==3.1.0" 178 | }, 179 | "packaging": { 180 | "hashes": [ 181 | "sha256:4357f74f47b9c12db93624a82154e9b120fa8293699949152b22065d556079f8", 182 | "sha256:998416ba6962ae7fbd6596850b80e17859a5753ba17c32284f67bfff33784181" 183 | ], 184 | "markers": "python_version >= '2.7' and python_version not in '3.0, 3.1, 3.2, 3.3'", 185 | "version": "==20.4" 186 | }, 187 | "pyparsing": { 188 | "hashes": [ 189 | "sha256:c203ec8783bf771a155b207279b9bccb8dea02d8f0c9e5f8ead507bc3246ecc1", 190 | "sha256:ef9d7589ef3c200abe66653d3f1ab1033c3c419ae9b9bdb1240a85b024efc88b" 191 | ], 192 | "markers": "python_version >= '2.6' and python_version not in '3.0, 3.1, 3.2'", 193 | "version": "==2.4.7" 194 | }, 195 | "python3-openid": { 196 | "hashes": [ 197 | "sha256:33fbf6928f401e0b790151ed2b5290b02545e8775f982485205a066f874aaeaf", 198 | "sha256:6626f771e0417486701e0b4daff762e7212e820ca5b29fcc0d05f6f8736dfa6b" 199 | ], 200 | "version": "==3.2.0" 201 | }, 202 | "pytz": { 203 | "hashes": [ 204 | "sha256:a494d53b6d39c3c6e44c3bec237336e14305e4f29bbf800b599253057fbb79ed", 205 | "sha256:c35965d010ce31b23eeb663ed3cc8c906275d6be1a34393a1d73a41febf4a048" 206 | ], 207 | "version": "==2020.1" 208 | }, 209 | "pyyaml": { 210 | "hashes": [ 211 | "sha256:06a0d7ba600ce0b2d2fe2e78453a470b5a6e000a985dd4a4e54e436cc36b0e97", 212 | "sha256:240097ff019d7c70a4922b6869d8a86407758333f02203e0fc6ff79c5dcede76", 213 | "sha256:4f4b913ca1a7319b33cfb1369e91e50354d6f07a135f3b901aca02aa95940bd2", 214 | "sha256:69f00dca373f240f842b2931fb2c7e14ddbacd1397d57157a9b005a6a9942648", 215 | "sha256:73f099454b799e05e5ab51423c7bcf361c58d3206fa7b0d555426b1f4d9a3eaf", 216 | "sha256:74809a57b329d6cc0fdccee6318f44b9b8649961fa73144a98735b0aaf029f1f", 217 | "sha256:7739fc0fa8205b3ee8808aea45e968bc90082c10aef6ea95e855e10abf4a37b2", 218 | "sha256:95f71d2af0ff4227885f7a6605c37fd53d3a106fcab511b8860ecca9fcf400ee", 219 | "sha256:b8eac752c5e14d3eca0e6dd9199cd627518cb5ec06add0de9d32baeee6fe645d", 220 | "sha256:cc8955cfbfc7a115fa81d85284ee61147059a753344bc51098f3ccd69b0d7e0c", 221 | "sha256:d13155f591e6fcc1ec3b30685d50bf0711574e2c0dfffd7644babf8b5102ca1a" 222 | ], 223 | "index": "pypi", 224 | "version": "==5.3.1" 225 | }, 226 | "requests": { 227 | "hashes": [ 228 | "sha256:b3559a131db72c33ee969480840fff4bb6dd111de7dd27c8ee1f820f4f00231b", 229 | "sha256:fe75cc94a9443b9246fc7049224f75604b113c36acb93f87b80ed42c44cbb898" 230 | ], 231 | "markers": "python_version >= '2.7' and python_version not in '3.0, 3.1, 3.2, 3.3, 3.4'", 232 | "version": "==2.24.0" 233 | }, 234 | "requests-oauthlib": { 235 | "hashes": [ 236 | "sha256:7f71572defaecd16372f9006f33c2ec8c077c3cfa6f5911a9a90202beb513f3d", 237 | "sha256:b4261601a71fd721a8bd6d7aa1cc1d6a8a93b4a9f5e96626f8e4d91e8beeaa6a", 238 | "sha256:fa6c47b933f01060936d87ae9327fead68768b69c6c9ea2109c48be30f2d4dbc" 239 | ], 240 | "version": "==1.3.0" 241 | }, 242 | "ruamel.yaml": { 243 | "hashes": [ 244 | "sha256:0962fd7999e064c4865f96fb1e23079075f4a2a14849bcdc5cdba53a24f9759b", 245 | "sha256:099c644a778bf72ffa00524f78dd0b6476bca94a1da344130f4bf3381ce5b954" 246 | ], 247 | "version": "==0.16.10" 248 | }, 249 | "ruamel.yaml.clib": { 250 | "hashes": [ 251 | "sha256:1e77424825caba5553bbade750cec2277ef130647d685c2b38f68bc03453bac6", 252 | "sha256:392b7c371312abf27fb549ec2d5e0092f7ef6e6c9f767bfb13e83cb903aca0fd", 253 | "sha256:4d55386129291b96483edcb93b381470f7cd69f97585829b048a3d758d31210a", 254 | "sha256:550168c02d8de52ee58c3d8a8193d5a8a9491a5e7b2462d27ac5bf63717574c9", 255 | "sha256:57933a6986a3036257ad7bf283529e7c19c2810ff24c86f4a0cfeb49d2099919", 256 | "sha256:615b0396a7fad02d1f9a0dcf9f01202bf9caefee6265198f252c865f4227fcc6", 257 | "sha256:77556a7aa190be9a2bd83b7ee075d3df5f3c5016d395613671487e79b082d784", 258 | "sha256:7aee724e1ff424757b5bd8f6c5bbdb033a570b2b4683b17ace4dbe61a99a657b", 259 | "sha256:8073c8b92b06b572e4057b583c3d01674ceaf32167801fe545a087d7a1e8bf52", 260 | "sha256:9c6d040d0396c28d3eaaa6cb20152cb3b2f15adf35a0304f4f40a3cf9f1d2448", 261 | "sha256:a0ff786d2a7dbe55f9544b3f6ebbcc495d7e730df92a08434604f6f470b899c5", 262 | "sha256:b1b7fcee6aedcdc7e62c3a73f238b3d080c7ba6650cd808bce8d7761ec484070", 263 | "sha256:b66832ea8077d9b3f6e311c4a53d06273db5dc2db6e8a908550f3c14d67e718c", 264 | "sha256:be018933c2f4ee7de55e7bd7d0d801b3dfb09d21dad0cce8a97995fd3e44be30", 265 | "sha256:d0d3ac228c9bbab08134b4004d748cf9f8743504875b3603b3afbb97e3472947", 266 | "sha256:d10e9dd744cf85c219bf747c75194b624cc7a94f0c80ead624b06bfa9f61d3bc", 267 | "sha256:ea4362548ee0cbc266949d8a441238d9ad3600ca9910c3fe4e82ee3a50706973", 268 | "sha256:ed5b3698a2bb241b7f5cbbe277eaa7fe48b07a58784fba4f75224fd066d253ad", 269 | "sha256:f9dcc1ae73f36e8059589b601e8e4776b9976effd76c21ad6a855a74318efd6e" 270 | ], 271 | "markers": "platform_python_implementation == 'CPython' and python_version < '3.9'", 272 | "version": "==0.2.0" 273 | }, 274 | "six": { 275 | "hashes": [ 276 | "sha256:30639c035cdb23534cd4aa2dd52c3bf48f06e5f4a941509c8bafd8ce11080259", 277 | "sha256:8b74bedcbbbaca38ff6d7491d76f2b06b3592611af620f8426e82dddb04a5ced" 278 | ], 279 | "markers": "python_version >= '2.7' and python_version not in '3.0, 3.1, 3.2'", 280 | "version": "==1.15.0" 281 | }, 282 | "sqlparse": { 283 | "hashes": [ 284 | "sha256:022fb9c87b524d1f7862b3037e541f68597a730a8843245c349fc93e1643dc4e", 285 | "sha256:e162203737712307dfe78860cc56c8da8a852ab2ee33750e33aeadf38d12c548" 286 | ], 287 | "markers": "python_version >= '2.7' and python_version not in '3.0, 3.1, 3.2, 3.3'", 288 | "version": "==0.3.1" 289 | }, 290 | "uritemplate": { 291 | "hashes": [ 292 | "sha256:07620c3f3f8eed1f12600845892b0e036a2420acf513c53f7de0abd911a5894f", 293 | "sha256:5af8ad10cec94f215e3f48112de2022e1d5a37ed427fbd88652fa908f2ab7cae" 294 | ], 295 | "index": "pypi", 296 | "version": "==3.0.1" 297 | }, 298 | "urllib3": { 299 | "hashes": [ 300 | "sha256:91056c15fa70756691db97756772bb1eb9678fa585d9184f24534b100dc60f4a", 301 | "sha256:e7983572181f5e1522d9c98453462384ee92a0be7fac5f1413a1e35c56cc0461" 302 | ], 303 | "markers": "python_version >= '2.7' and python_version not in '3.0, 3.1, 3.2, 3.3, 3.4' and python_version < '4'", 304 | "version": "==1.25.10" 305 | } 306 | }, 307 | "develop": {} 308 | } 309 | -------------------------------------------------------------------------------- /ch9-schemas-and-documentation/config/__init__.py: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/wsvincent/djangoforapis_31/c678537076d809f4fea19f0beb5e20e3be8661e2/ch9-schemas-and-documentation/config/__init__.py -------------------------------------------------------------------------------- /ch9-schemas-and-documentation/config/asgi.py: -------------------------------------------------------------------------------- 1 | """ 2 | ASGI config for config project. 3 | 4 | It exposes the ASGI callable as a module-level variable named ``application``. 5 | 6 | For more information on this file, see 7 | https://docs.djangoproject.com/en/dev/howto/deployment/asgi/ 8 | """ 9 | 10 | import os 11 | 12 | from django.core.asgi import get_asgi_application 13 | 14 | os.environ.setdefault('DJANGO_SETTINGS_MODULE', 'config.settings') 15 | 16 | application = get_asgi_application() 17 | -------------------------------------------------------------------------------- /ch9-schemas-and-documentation/config/settings.py: -------------------------------------------------------------------------------- 1 | """ 2 | Django settings for config project. 3 | 4 | Generated by 'django-admin startproject' using Django 3.1rc1. 5 | 6 | For more information on this file, see 7 | https://docs.djangoproject.com/en/dev/topics/settings/ 8 | 9 | For the full list of settings and their values, see 10 | https://docs.djangoproject.com/en/dev/ref/settings/ 11 | """ 12 | 13 | from pathlib import Path 14 | 15 | # Build paths inside the project like this: BASE_DIR / 'subdir'. 16 | BASE_DIR = Path(__file__).resolve(strict=True).parent.parent 17 | 18 | 19 | # Quick-start development settings - unsuitable for production 20 | # See https://docs.djangoproject.com/en/dev/howto/deployment/checklist/ 21 | 22 | # SECURITY WARNING: keep the secret key used in production secret! 23 | SECRET_KEY = '5kji9d4+u2=pw4k2bh+v4pt7(57)$hsns-g_ma0oskgf*aa16&' 24 | 25 | # SECURITY WARNING: don't run with debug turned on in production! 26 | DEBUG = True 27 | 28 | ALLOWED_HOSTS = [] 29 | 30 | 31 | # Application definition 32 | 33 | INSTALLED_APPS = [ 34 | 'django.contrib.admin', 35 | 'django.contrib.auth', 36 | 'django.contrib.contenttypes', 37 | 'django.contrib.sessions', 38 | 'django.contrib.messages', 39 | 'django.contrib.staticfiles', 40 | 'django.contrib.sites', 41 | 42 | # 3rd-party apps 43 | 'rest_framework', 44 | 'rest_framework.authtoken', 45 | 'allauth', 46 | 'allauth.account', 47 | 'allauth.socialaccount', 48 | 'dj_rest_auth', 49 | 'dj_rest_auth.registration', 50 | 'drf_yasg', # new 51 | 52 | # Local 53 | 'posts', 54 | ] 55 | 56 | MIDDLEWARE = [ 57 | 'django.middleware.security.SecurityMiddleware', 58 | 'django.contrib.sessions.middleware.SessionMiddleware', 59 | 'django.middleware.common.CommonMiddleware', 60 | 'django.middleware.csrf.CsrfViewMiddleware', 61 | 'django.contrib.auth.middleware.AuthenticationMiddleware', 62 | 'django.contrib.messages.middleware.MessageMiddleware', 63 | 'django.middleware.clickjacking.XFrameOptionsMiddleware', 64 | ] 65 | 66 | ROOT_URLCONF = 'config.urls' 67 | 68 | TEMPLATES = [ 69 | { 70 | 'BACKEND': 'django.template.backends.django.DjangoTemplates', 71 | 'DIRS': [], 72 | 'APP_DIRS': True, 73 | 'OPTIONS': { 74 | 'context_processors': [ 75 | 'django.template.context_processors.debug', 76 | 'django.template.context_processors.request', 77 | 'django.contrib.auth.context_processors.auth', 78 | 'django.contrib.messages.context_processors.messages', 79 | ], 80 | }, 81 | }, 82 | ] 83 | 84 | WSGI_APPLICATION = 'config.wsgi.application' 85 | 86 | 87 | # Database 88 | # https://docs.djangoproject.com/en/dev/ref/settings/#databases 89 | 90 | DATABASES = { 91 | 'default': { 92 | 'ENGINE': 'django.db.backends.sqlite3', 93 | 'NAME': BASE_DIR / 'db.sqlite3', 94 | } 95 | } 96 | 97 | 98 | # Password validation 99 | # https://docs.djangoproject.com/en/dev/ref/settings/#auth-password-validators 100 | 101 | AUTH_PASSWORD_VALIDATORS = [ 102 | { 103 | 'NAME': 'django.contrib.auth.password_validation.UserAttributeSimilarityValidator', 104 | }, 105 | { 106 | 'NAME': 'django.contrib.auth.password_validation.MinimumLengthValidator', 107 | }, 108 | { 109 | 'NAME': 'django.contrib.auth.password_validation.CommonPasswordValidator', 110 | }, 111 | { 112 | 'NAME': 'django.contrib.auth.password_validation.NumericPasswordValidator', 113 | }, 114 | ] 115 | 116 | 117 | # Internationalization 118 | # https://docs.djangoproject.com/en/dev/topics/i18n/ 119 | 120 | LANGUAGE_CODE = 'en-us' 121 | 122 | TIME_ZONE = 'UTC' 123 | 124 | USE_I18N = True 125 | 126 | USE_L10N = True 127 | 128 | USE_TZ = True 129 | 130 | 131 | # Static files (CSS, JavaScript, Images) 132 | # https://docs.djangoproject.com/en/dev/howto/static-files/ 133 | 134 | STATIC_URL = '/static/' 135 | 136 | REST_FRAMEWORK = { 137 | 'DEFAULT_PERMISSION_CLASSES': [ 138 | 'rest_framework.permissions.IsAuthenticated', 139 | ], 140 | 'DEFAULT_AUTHENTICATION_CLASSES': [ 141 | 'rest_framework.authentication.SessionAuthentication', 142 | 'rest_framework.authentication.TokenAuthentication', # new 143 | ], 144 | } 145 | 146 | EMAIL_BACKEND = 'django.core.mail.backends.console.EmailBackend' # new 147 | 148 | SITE_ID = 1 # new 149 | -------------------------------------------------------------------------------- /ch9-schemas-and-documentation/config/urls.py: -------------------------------------------------------------------------------- 1 | from django.contrib import admin 2 | from django.urls import include, path 3 | from rest_framework import permissions # new 4 | from drf_yasg.views import get_schema_view # new 5 | from drf_yasg import openapi # new 6 | 7 | schema_view = get_schema_view( # new 8 | openapi.Info( 9 | title="Blog API", 10 | default_version="v1", 11 | description="A sample API for learning DRF", 12 | terms_of_service="https://www.google.com/policies/terms/", 13 | contact=openapi.Contact(email="hello@example.com"), 14 | license=openapi.License(name="BSD License"), 15 | ), 16 | public=True, 17 | permission_classes=(permissions.AllowAny,), 18 | ) 19 | 20 | urlpatterns = [ 21 | path('admin/', admin.site.urls), 22 | path('api/v1/', include('posts.urls')), 23 | path('api-auth/', include('rest_framework.urls')), 24 | path('api/v1/dj-rest-auth/', include('dj_rest_auth.urls')), 25 | path('api/v1/dj-rest-auth/registration/', 26 | include('dj_rest_auth.registration.urls')), 27 | path('swagger/', schema_view.with_ui( # new 28 | 'swagger', cache_timeout=0), name='schema-swagger-ui'), 29 | path('redoc/', schema_view.with_ui( # new 30 | 'redoc', cache_timeout=0), name='schema-redoc'), 31 | ] 32 | -------------------------------------------------------------------------------- /ch9-schemas-and-documentation/config/wsgi.py: -------------------------------------------------------------------------------- 1 | """ 2 | WSGI config for config 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/dev/howto/deployment/wsgi/ 8 | """ 9 | 10 | import os 11 | 12 | from django.core.wsgi import get_wsgi_application 13 | 14 | os.environ.setdefault('DJANGO_SETTINGS_MODULE', 'config.settings') 15 | 16 | application = get_wsgi_application() 17 | -------------------------------------------------------------------------------- /ch9-schemas-and-documentation/db.sqlite3: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/wsvincent/djangoforapis_31/c678537076d809f4fea19f0beb5e20e3be8661e2/ch9-schemas-and-documentation/db.sqlite3 -------------------------------------------------------------------------------- /ch9-schemas-and-documentation/manage.py: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env python 2 | """Django's command-line utility for administrative tasks.""" 3 | import os 4 | import sys 5 | 6 | 7 | def main(): 8 | """Run administrative tasks.""" 9 | os.environ.setdefault('DJANGO_SETTINGS_MODULE', 'config.settings') 10 | try: 11 | from django.core.management import execute_from_command_line 12 | except ImportError as exc: 13 | raise ImportError( 14 | "Couldn't import Django. Are you sure it's installed and " 15 | "available on your PYTHONPATH environment variable? Did you " 16 | "forget to activate a virtual environment?" 17 | ) from exc 18 | execute_from_command_line(sys.argv) 19 | 20 | 21 | if __name__ == '__main__': 22 | main() 23 | -------------------------------------------------------------------------------- /ch9-schemas-and-documentation/openapi-schema.yml: -------------------------------------------------------------------------------- 1 | openapi: 3.0.2 2 | info: 3 | title: '' 4 | version: '' 5 | paths: 6 | /api/v1/users/: 7 | get: 8 | operationId: listUsers 9 | description: '' 10 | parameters: [] 11 | responses: 12 | '200': 13 | content: 14 | application/json: 15 | schema: 16 | type: array 17 | items: 18 | properties: 19 | id: 20 | type: integer 21 | readOnly: true 22 | username: 23 | type: string 24 | description: Required. 150 characters or fewer. Letters, digits 25 | and @/./+/-/_ only. 26 | pattern: ^[\w.@+-]+\Z 27 | maxLength: 150 28 | required: 29 | - username 30 | description: '' 31 | post: 32 | operationId: createUser 33 | description: '' 34 | parameters: [] 35 | requestBody: 36 | content: 37 | application/json: 38 | schema: &id001 39 | properties: 40 | username: 41 | type: string 42 | description: Required. 150 characters or fewer. Letters, digits 43 | and @/./+/-/_ only. 44 | pattern: ^[\w.@+-]+\Z 45 | maxLength: 150 46 | required: 47 | - username 48 | application/x-www-form-urlencoded: 49 | schema: *id001 50 | multipart/form-data: 51 | schema: *id001 52 | responses: 53 | '200': 54 | content: 55 | application/json: 56 | schema: 57 | properties: 58 | id: 59 | type: integer 60 | readOnly: true 61 | username: 62 | type: string 63 | description: Required. 150 characters or fewer. Letters, digits 64 | and @/./+/-/_ only. 65 | pattern: ^[\w.@+-]+\Z 66 | maxLength: 150 67 | required: 68 | - username 69 | description: '' 70 | /api/v1/users/{id}/: 71 | get: 72 | operationId: retrieveUser 73 | description: '' 74 | parameters: 75 | - name: id 76 | in: path 77 | required: true 78 | description: A unique integer value identifying this user. 79 | schema: 80 | type: string 81 | responses: 82 | '200': 83 | content: 84 | application/json: 85 | schema: 86 | properties: 87 | id: 88 | type: integer 89 | readOnly: true 90 | username: 91 | type: string 92 | description: Required. 150 characters or fewer. Letters, digits 93 | and @/./+/-/_ only. 94 | pattern: ^[\w.@+-]+\Z 95 | maxLength: 150 96 | required: 97 | - username 98 | description: '' 99 | put: 100 | operationId: updateUser 101 | description: '' 102 | parameters: 103 | - name: id 104 | in: path 105 | required: true 106 | description: A unique integer value identifying this user. 107 | schema: 108 | type: string 109 | requestBody: 110 | content: 111 | application/json: 112 | schema: &id002 113 | properties: 114 | username: 115 | type: string 116 | description: Required. 150 characters or fewer. Letters, digits 117 | and @/./+/-/_ only. 118 | pattern: ^[\w.@+-]+\Z 119 | maxLength: 150 120 | required: 121 | - username 122 | application/x-www-form-urlencoded: 123 | schema: *id002 124 | multipart/form-data: 125 | schema: *id002 126 | responses: 127 | '200': 128 | content: 129 | application/json: 130 | schema: 131 | properties: 132 | id: 133 | type: integer 134 | readOnly: true 135 | username: 136 | type: string 137 | description: Required. 150 characters or fewer. Letters, digits 138 | and @/./+/-/_ only. 139 | pattern: ^[\w.@+-]+\Z 140 | maxLength: 150 141 | required: 142 | - username 143 | description: '' 144 | patch: 145 | operationId: partial_updateUser 146 | description: '' 147 | parameters: 148 | - name: id 149 | in: path 150 | required: true 151 | description: A unique integer value identifying this user. 152 | schema: 153 | type: string 154 | requestBody: 155 | content: 156 | application/json: 157 | schema: &id003 158 | properties: 159 | username: 160 | type: string 161 | description: Required. 150 characters or fewer. Letters, digits 162 | and @/./+/-/_ only. 163 | pattern: ^[\w.@+-]+\Z 164 | maxLength: 150 165 | application/x-www-form-urlencoded: 166 | schema: *id003 167 | multipart/form-data: 168 | schema: *id003 169 | responses: 170 | '200': 171 | content: 172 | application/json: 173 | schema: 174 | properties: 175 | id: 176 | type: integer 177 | readOnly: true 178 | username: 179 | type: string 180 | description: Required. 150 characters or fewer. Letters, digits 181 | and @/./+/-/_ only. 182 | pattern: ^[\w.@+-]+\Z 183 | maxLength: 150 184 | required: 185 | - username 186 | description: '' 187 | delete: 188 | operationId: destroyUser 189 | description: '' 190 | parameters: 191 | - name: id 192 | in: path 193 | required: true 194 | description: A unique integer value identifying this user. 195 | schema: 196 | type: string 197 | responses: 198 | '204': 199 | description: '' 200 | /api/v1/: 201 | get: 202 | operationId: listPosts 203 | description: '' 204 | parameters: [] 205 | responses: 206 | '200': 207 | content: 208 | application/json: 209 | schema: 210 | type: array 211 | items: 212 | properties: 213 | id: 214 | type: integer 215 | readOnly: true 216 | author: 217 | type: integer 218 | title: 219 | type: string 220 | maxLength: 50 221 | body: 222 | type: string 223 | created_at: 224 | type: string 225 | format: date-time 226 | readOnly: true 227 | required: 228 | - author 229 | - title 230 | - body 231 | description: '' 232 | post: 233 | operationId: createPost 234 | description: '' 235 | parameters: [] 236 | requestBody: 237 | content: 238 | application/json: 239 | schema: &id004 240 | properties: 241 | author: 242 | type: integer 243 | title: 244 | type: string 245 | maxLength: 50 246 | body: 247 | type: string 248 | required: 249 | - author 250 | - title 251 | - body 252 | application/x-www-form-urlencoded: 253 | schema: *id004 254 | multipart/form-data: 255 | schema: *id004 256 | responses: 257 | '200': 258 | content: 259 | application/json: 260 | schema: 261 | properties: 262 | id: 263 | type: integer 264 | readOnly: true 265 | author: 266 | type: integer 267 | title: 268 | type: string 269 | maxLength: 50 270 | body: 271 | type: string 272 | created_at: 273 | type: string 274 | format: date-time 275 | readOnly: true 276 | required: 277 | - author 278 | - title 279 | - body 280 | description: '' 281 | /api/v1/{id}/: 282 | get: 283 | operationId: retrievePost 284 | description: '' 285 | parameters: 286 | - name: id 287 | in: path 288 | required: true 289 | description: A unique integer value identifying this post. 290 | schema: 291 | type: string 292 | responses: 293 | '200': 294 | content: 295 | application/json: 296 | schema: 297 | properties: 298 | id: 299 | type: integer 300 | readOnly: true 301 | author: 302 | type: integer 303 | title: 304 | type: string 305 | maxLength: 50 306 | body: 307 | type: string 308 | created_at: 309 | type: string 310 | format: date-time 311 | readOnly: true 312 | required: 313 | - author 314 | - title 315 | - body 316 | description: '' 317 | put: 318 | operationId: updatePost 319 | description: '' 320 | parameters: 321 | - name: id 322 | in: path 323 | required: true 324 | description: A unique integer value identifying this post. 325 | schema: 326 | type: string 327 | requestBody: 328 | content: 329 | application/json: 330 | schema: &id005 331 | properties: 332 | author: 333 | type: integer 334 | title: 335 | type: string 336 | maxLength: 50 337 | body: 338 | type: string 339 | required: 340 | - author 341 | - title 342 | - body 343 | application/x-www-form-urlencoded: 344 | schema: *id005 345 | multipart/form-data: 346 | schema: *id005 347 | responses: 348 | '200': 349 | content: 350 | application/json: 351 | schema: 352 | properties: 353 | id: 354 | type: integer 355 | readOnly: true 356 | author: 357 | type: integer 358 | title: 359 | type: string 360 | maxLength: 50 361 | body: 362 | type: string 363 | created_at: 364 | type: string 365 | format: date-time 366 | readOnly: true 367 | required: 368 | - author 369 | - title 370 | - body 371 | description: '' 372 | patch: 373 | operationId: partial_updatePost 374 | description: '' 375 | parameters: 376 | - name: id 377 | in: path 378 | required: true 379 | description: A unique integer value identifying this post. 380 | schema: 381 | type: string 382 | requestBody: 383 | content: 384 | application/json: 385 | schema: &id006 386 | properties: 387 | author: 388 | type: integer 389 | title: 390 | type: string 391 | maxLength: 50 392 | body: 393 | type: string 394 | application/x-www-form-urlencoded: 395 | schema: *id006 396 | multipart/form-data: 397 | schema: *id006 398 | responses: 399 | '200': 400 | content: 401 | application/json: 402 | schema: 403 | properties: 404 | id: 405 | type: integer 406 | readOnly: true 407 | author: 408 | type: integer 409 | title: 410 | type: string 411 | maxLength: 50 412 | body: 413 | type: string 414 | created_at: 415 | type: string 416 | format: date-time 417 | readOnly: true 418 | required: 419 | - author 420 | - title 421 | - body 422 | description: '' 423 | delete: 424 | operationId: destroyPost 425 | description: '' 426 | parameters: 427 | - name: id 428 | in: path 429 | required: true 430 | description: A unique integer value identifying this post. 431 | schema: 432 | type: string 433 | responses: 434 | '204': 435 | description: '' 436 | /api/v1/dj-rest-auth/logout/: 437 | get: 438 | operationId: listLogouts 439 | description: 'Calls Django logout method and delete the Token object 440 | 441 | assigned to the current User object. 442 | 443 | 444 | Accepts/Returns nothing.' 445 | parameters: [] 446 | responses: 447 | '200': 448 | content: 449 | application/json: 450 | schema: 451 | type: array 452 | items: {} 453 | description: '' 454 | post: 455 | operationId: CreateLogout 456 | description: 'Calls Django logout method and delete the Token object 457 | 458 | assigned to the current User object. 459 | 460 | 461 | Accepts/Returns nothing.' 462 | parameters: [] 463 | responses: 464 | '200': 465 | content: 466 | application/json: 467 | schema: {} 468 | description: '' 469 | /api/v1/dj-rest-auth/user/: 470 | get: 471 | operationId: RetrieveUserDetails 472 | description: 'Reads and updates UserModel fields 473 | 474 | Accepts GET, PUT, PATCH methods. 475 | 476 | 477 | Default accepted fields: username, first_name, last_name 478 | 479 | Default display fields: pk, username, email, first_name, last_name 480 | 481 | Read-only fields: pk, email 482 | 483 | 484 | Returns UserModel fields.' 485 | parameters: [] 486 | responses: 487 | '200': 488 | content: 489 | application/json: 490 | schema: 491 | properties: 492 | pk: 493 | type: integer 494 | readOnly: true 495 | username: 496 | type: string 497 | description: Required. 150 characters or fewer. Letters, digits 498 | and @/./+/-/_ only. 499 | pattern: ^[\w.@+-]+\Z 500 | maxLength: 150 501 | email: 502 | type: string 503 | format: email 504 | readOnly: true 505 | first_name: 506 | type: string 507 | maxLength: 30 508 | last_name: 509 | type: string 510 | maxLength: 150 511 | required: 512 | - username 513 | description: '' 514 | put: 515 | operationId: UpdateUserDetails 516 | description: 'Reads and updates UserModel fields 517 | 518 | Accepts GET, PUT, PATCH methods. 519 | 520 | 521 | Default accepted fields: username, first_name, last_name 522 | 523 | Default display fields: pk, username, email, first_name, last_name 524 | 525 | Read-only fields: pk, email 526 | 527 | 528 | Returns UserModel fields.' 529 | parameters: [] 530 | requestBody: 531 | content: 532 | application/json: 533 | schema: &id007 534 | properties: 535 | username: 536 | type: string 537 | description: Required. 150 characters or fewer. Letters, digits 538 | and @/./+/-/_ only. 539 | pattern: ^[\w.@+-]+\Z 540 | maxLength: 150 541 | first_name: 542 | type: string 543 | maxLength: 30 544 | last_name: 545 | type: string 546 | maxLength: 150 547 | required: 548 | - username 549 | application/x-www-form-urlencoded: 550 | schema: *id007 551 | multipart/form-data: 552 | schema: *id007 553 | responses: 554 | '200': 555 | content: 556 | application/json: 557 | schema: 558 | properties: 559 | pk: 560 | type: integer 561 | readOnly: true 562 | username: 563 | type: string 564 | description: Required. 150 characters or fewer. Letters, digits 565 | and @/./+/-/_ only. 566 | pattern: ^[\w.@+-]+\Z 567 | maxLength: 150 568 | email: 569 | type: string 570 | format: email 571 | readOnly: true 572 | first_name: 573 | type: string 574 | maxLength: 30 575 | last_name: 576 | type: string 577 | maxLength: 150 578 | required: 579 | - username 580 | description: '' 581 | patch: 582 | operationId: PartialUpdateUserDetails 583 | description: 'Reads and updates UserModel fields 584 | 585 | Accepts GET, PUT, PATCH methods. 586 | 587 | 588 | Default accepted fields: username, first_name, last_name 589 | 590 | Default display fields: pk, username, email, first_name, last_name 591 | 592 | Read-only fields: pk, email 593 | 594 | 595 | Returns UserModel fields.' 596 | parameters: [] 597 | requestBody: 598 | content: 599 | application/json: 600 | schema: &id008 601 | properties: 602 | username: 603 | type: string 604 | description: Required. 150 characters or fewer. Letters, digits 605 | and @/./+/-/_ only. 606 | pattern: ^[\w.@+-]+\Z 607 | maxLength: 150 608 | first_name: 609 | type: string 610 | maxLength: 30 611 | last_name: 612 | type: string 613 | maxLength: 150 614 | application/x-www-form-urlencoded: 615 | schema: *id008 616 | multipart/form-data: 617 | schema: *id008 618 | responses: 619 | '200': 620 | content: 621 | application/json: 622 | schema: 623 | properties: 624 | pk: 625 | type: integer 626 | readOnly: true 627 | username: 628 | type: string 629 | description: Required. 150 characters or fewer. Letters, digits 630 | and @/./+/-/_ only. 631 | pattern: ^[\w.@+-]+\Z 632 | maxLength: 150 633 | email: 634 | type: string 635 | format: email 636 | readOnly: true 637 | first_name: 638 | type: string 639 | maxLength: 30 640 | last_name: 641 | type: string 642 | maxLength: 150 643 | required: 644 | - username 645 | description: '' 646 | /api/v1/dj-rest-auth/password/reset/: 647 | post: 648 | operationId: CreatePasswordReset 649 | description: 'Calls Django Auth PasswordResetForm save method. 650 | 651 | 652 | Accepts the following POST parameters: email 653 | 654 | Returns the success/fail message.' 655 | parameters: [] 656 | requestBody: 657 | content: 658 | application/json: 659 | schema: &id009 660 | properties: 661 | email: 662 | type: string 663 | format: email 664 | required: 665 | - email 666 | application/x-www-form-urlencoded: 667 | schema: *id009 668 | multipart/form-data: 669 | schema: *id009 670 | responses: 671 | '200': 672 | content: 673 | application/json: 674 | schema: 675 | properties: 676 | email: 677 | type: string 678 | format: email 679 | required: 680 | - email 681 | description: '' 682 | /api/v1/dj-rest-auth/password/reset/confirm/: 683 | post: 684 | operationId: CreatePasswordResetConfirm 685 | description: "Password reset e-mail link is confirmed, therefore\nthis resets\ 686 | \ the user's password.\n\nAccepts the following POST parameters: token, uid,\n\ 687 | \ new_password1, new_password2\nReturns the success/fail message." 688 | parameters: [] 689 | requestBody: 690 | content: 691 | application/json: 692 | schema: &id010 693 | properties: 694 | new_password1: 695 | type: string 696 | maxLength: 128 697 | new_password2: 698 | type: string 699 | maxLength: 128 700 | uid: 701 | type: string 702 | token: 703 | type: string 704 | required: 705 | - new_password1 706 | - new_password2 707 | - uid 708 | - token 709 | application/x-www-form-urlencoded: 710 | schema: *id010 711 | multipart/form-data: 712 | schema: *id010 713 | responses: 714 | '200': 715 | content: 716 | application/json: 717 | schema: 718 | properties: 719 | new_password1: 720 | type: string 721 | maxLength: 128 722 | new_password2: 723 | type: string 724 | maxLength: 128 725 | uid: 726 | type: string 727 | token: 728 | type: string 729 | required: 730 | - new_password1 731 | - new_password2 732 | - uid 733 | - token 734 | description: '' 735 | /api/v1/dj-rest-auth/login/: 736 | post: 737 | operationId: CreateLogin 738 | description: 'Check the credentials and return the REST Token 739 | 740 | if the credentials are valid and authenticated. 741 | 742 | Calls Django Auth login method to register User ID 743 | 744 | in Django session framework 745 | 746 | 747 | Accept the following POST parameters: username, password 748 | 749 | Return the REST Framework Token Object''s key.' 750 | parameters: [] 751 | requestBody: 752 | content: 753 | application/json: 754 | schema: &id011 755 | properties: 756 | username: 757 | type: string 758 | email: 759 | type: string 760 | format: email 761 | password: 762 | type: string 763 | required: 764 | - password 765 | application/x-www-form-urlencoded: 766 | schema: *id011 767 | multipart/form-data: 768 | schema: *id011 769 | responses: 770 | '200': 771 | content: 772 | application/json: 773 | schema: 774 | properties: 775 | username: 776 | type: string 777 | email: 778 | type: string 779 | format: email 780 | password: 781 | type: string 782 | required: 783 | - password 784 | description: '' 785 | /api/v1/dj-rest-auth/password/change/: 786 | post: 787 | operationId: CreatePasswordChange 788 | description: 'Calls Django Auth SetPasswordForm save method. 789 | 790 | 791 | Accepts the following POST parameters: new_password1, new_password2 792 | 793 | Returns the success/fail message.' 794 | parameters: [] 795 | requestBody: 796 | content: 797 | application/json: 798 | schema: &id012 799 | properties: 800 | new_password1: 801 | type: string 802 | maxLength: 128 803 | new_password2: 804 | type: string 805 | maxLength: 128 806 | required: 807 | - new_password1 808 | - new_password2 809 | application/x-www-form-urlencoded: 810 | schema: *id012 811 | multipart/form-data: 812 | schema: *id012 813 | responses: 814 | '200': 815 | content: 816 | application/json: 817 | schema: 818 | properties: 819 | new_password1: 820 | type: string 821 | maxLength: 128 822 | new_password2: 823 | type: string 824 | maxLength: 128 825 | required: 826 | - new_password1 827 | - new_password2 828 | description: '' 829 | /api/v1/dj-rest-auth/registration/: 830 | post: 831 | operationId: CreateRegister 832 | description: '' 833 | parameters: [] 834 | requestBody: 835 | content: 836 | application/json: 837 | schema: &id013 838 | properties: 839 | username: 840 | type: string 841 | maxLength: 150 842 | minLength: 1 843 | email: 844 | type: string 845 | format: email 846 | password1: 847 | type: string 848 | writeOnly: true 849 | password2: 850 | type: string 851 | writeOnly: true 852 | required: 853 | - username 854 | - password1 855 | - password2 856 | application/x-www-form-urlencoded: 857 | schema: *id013 858 | multipart/form-data: 859 | schema: *id013 860 | responses: 861 | '200': 862 | content: 863 | application/json: 864 | schema: 865 | properties: 866 | username: 867 | type: string 868 | maxLength: 150 869 | minLength: 1 870 | email: 871 | type: string 872 | format: email 873 | required: 874 | - username 875 | description: '' 876 | /api/v1/dj-rest-auth/registration/verify-email/: 877 | post: 878 | operationId: CreateVerifyEmail 879 | description: '' 880 | parameters: [] 881 | requestBody: 882 | content: 883 | application/json: 884 | schema: &id014 885 | properties: 886 | key: 887 | type: string 888 | required: 889 | - key 890 | application/x-www-form-urlencoded: 891 | schema: *id014 892 | multipart/form-data: 893 | schema: *id014 894 | responses: 895 | '200': 896 | content: 897 | application/json: 898 | schema: 899 | properties: 900 | key: 901 | type: string 902 | required: 903 | - key 904 | description: '' 905 | -------------------------------------------------------------------------------- /ch9-schemas-and-documentation/posts/__init__.py: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/wsvincent/djangoforapis_31/c678537076d809f4fea19f0beb5e20e3be8661e2/ch9-schemas-and-documentation/posts/__init__.py -------------------------------------------------------------------------------- /ch9-schemas-and-documentation/posts/admin.py: -------------------------------------------------------------------------------- 1 | from django.contrib import admin 2 | from .models import Post 3 | 4 | admin.site.register(Post) 5 | -------------------------------------------------------------------------------- /ch9-schemas-and-documentation/posts/apps.py: -------------------------------------------------------------------------------- 1 | from django.apps import AppConfig 2 | 3 | 4 | class PostsConfig(AppConfig): 5 | name = 'posts' 6 | -------------------------------------------------------------------------------- /ch9-schemas-and-documentation/posts/migrations/0001_initial.py: -------------------------------------------------------------------------------- 1 | # Generated by Django 3.1rc1 on 2020-07-29 17:28 2 | 3 | from django.conf import settings 4 | from django.db import migrations, models 5 | import django.db.models.deletion 6 | 7 | 8 | class Migration(migrations.Migration): 9 | 10 | initial = True 11 | 12 | dependencies = [ 13 | migrations.swappable_dependency(settings.AUTH_USER_MODEL), 14 | ] 15 | 16 | operations = [ 17 | migrations.CreateModel( 18 | name='Post', 19 | fields=[ 20 | ('id', models.AutoField(auto_created=True, primary_key=True, serialize=False, verbose_name='ID')), 21 | ('title', models.CharField(max_length=50)), 22 | ('body', models.TextField()), 23 | ('created_at', models.DateTimeField(auto_now_add=True)), 24 | ('updated_at', models.DateTimeField(auto_now=True)), 25 | ('author', models.ForeignKey(on_delete=django.db.models.deletion.CASCADE, to=settings.AUTH_USER_MODEL)), 26 | ], 27 | ), 28 | ] 29 | -------------------------------------------------------------------------------- /ch9-schemas-and-documentation/posts/migrations/__init__.py: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/wsvincent/djangoforapis_31/c678537076d809f4fea19f0beb5e20e3be8661e2/ch9-schemas-and-documentation/posts/migrations/__init__.py -------------------------------------------------------------------------------- /ch9-schemas-and-documentation/posts/models.py: -------------------------------------------------------------------------------- 1 | from django.db import models 2 | from django.contrib.auth.models import User 3 | 4 | 5 | class Post(models.Model): 6 | author = models.ForeignKey(User, on_delete=models.CASCADE) 7 | title = models.CharField(max_length=50) 8 | body = models.TextField() 9 | created_at = models.DateTimeField(auto_now_add=True) 10 | updated_at = models.DateTimeField(auto_now=True) 11 | 12 | def __str__(self): 13 | return self.title 14 | -------------------------------------------------------------------------------- /ch9-schemas-and-documentation/posts/permissions.py: -------------------------------------------------------------------------------- 1 | from rest_framework import permissions 2 | 3 | 4 | class IsAuthorOrReadOnly(permissions.BasePermission): 5 | 6 | def has_object_permission(self, request, view, obj): 7 | # Read-only permissions are allowed for any request 8 | if request.method in permissions.SAFE_METHODS: 9 | return True 10 | 11 | # Write permissions are only allowed to the author of a post 12 | return obj.author == request.user 13 | -------------------------------------------------------------------------------- /ch9-schemas-and-documentation/posts/serializers.py: -------------------------------------------------------------------------------- 1 | from django.contrib.auth import get_user_model # new 2 | from rest_framework import serializers 3 | from .models import Post 4 | 5 | 6 | class PostSerializer(serializers.ModelSerializer): 7 | 8 | class Meta: 9 | fields = ('id', 'author', 'title', 'body', 'created_at',) 10 | model = Post 11 | 12 | 13 | class UserSerializer(serializers.ModelSerializer): # new 14 | 15 | class Meta: 16 | model = get_user_model() 17 | fields = ('id', 'username',) 18 | -------------------------------------------------------------------------------- /ch9-schemas-and-documentation/posts/tests.py: -------------------------------------------------------------------------------- 1 | from django.test import TestCase 2 | from django.contrib.auth.models import User 3 | from .models import Post 4 | 5 | 6 | class BlogTests(TestCase): 7 | 8 | @classmethod 9 | def setUpTestData(cls): 10 | # Create a user 11 | testuser1 = User.objects.create_user( 12 | username='testuser1', password='abc123') 13 | testuser1.save() 14 | 15 | # Create a blog post 16 | test_post = Post.objects.create( 17 | author=testuser1, title='Blog title', body='Body content...') 18 | test_post.save() 19 | 20 | def test_blog_content(self): 21 | post = Post.objects.get(id=1) 22 | author = f'{post.author}' 23 | title = f'{post.title}' 24 | body = f'{post.body}' 25 | self.assertEqual(author, 'testuser1') 26 | self.assertEqual(title, 'Blog title') 27 | self.assertEqual(body, 'Body content...') 28 | -------------------------------------------------------------------------------- /ch9-schemas-and-documentation/posts/urls.py: -------------------------------------------------------------------------------- 1 | from django.urls import path 2 | from rest_framework.routers import SimpleRouter 3 | from .views import UserViewSet, PostViewSet 4 | 5 | router = SimpleRouter() 6 | router.register('users', UserViewSet, basename='users') 7 | router.register('', PostViewSet, basename='posts') 8 | 9 | urlpatterns = router.urls 10 | -------------------------------------------------------------------------------- /ch9-schemas-and-documentation/posts/views.py: -------------------------------------------------------------------------------- 1 | from django.contrib.auth import get_user_model 2 | from rest_framework import viewsets # new 3 | from .models import Post 4 | from .permissions import IsAuthorOrReadOnly 5 | from .serializers import PostSerializer, UserSerializer 6 | 7 | 8 | class PostViewSet(viewsets.ModelViewSet): # new 9 | permission_classes = (IsAuthorOrReadOnly,) 10 | queryset = Post.objects.all() 11 | serializer_class = PostSerializer 12 | 13 | 14 | class UserViewSet(viewsets.ModelViewSet): # new 15 | queryset = get_user_model().objects.all() 16 | serializer_class = UserSerializer 17 | -------------------------------------------------------------------------------- /cover.jpg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/wsvincent/djangoforapis_31/c678537076d809f4fea19f0beb5e20e3be8661e2/cover.jpg --------------------------------------------------------------------------------