├── .gitignore ├── LICENSE ├── README.md ├── api_crud ├── __init__.py ├── settings.py ├── urls.py ├── views.py └── wsgi.py ├── authentication ├── __init__.py ├── admin.py ├── apps.py ├── migrations │ └── __init__.py ├── models.py ├── serializers.py ├── tests.py ├── urls.py └── views.py ├── manage.py ├── movies ├── __init__.py ├── admin.py ├── apps.py ├── filters.py ├── migrations │ ├── 0001_initial.py │ └── __init__.py ├── models.py ├── pagination.py ├── permissions.py ├── serializers.py ├── urls.py └── views.py └── requirements.txt /.gitignore: -------------------------------------------------------------------------------- 1 | # Byte-compiled / optimized / DLL files 2 | __pycache__/ 3 | *.py[cod] 4 | *$py.class 5 | 6 | #PyCharm 7 | .idea/ 8 | .vscode/ 9 | 10 | # C extensions 11 | *.so 12 | 13 | # Distribution / packaging 14 | .Python 15 | build/ 16 | develop-eggs/ 17 | dist/ 18 | downloads/ 19 | eggs/ 20 | .eggs/ 21 | lib/ 22 | lib64/ 23 | parts/ 24 | sdist/ 25 | var/ 26 | wheels/ 27 | *.egg-info/ 28 | .installed.cfg 29 | *.egg 30 | MANIFEST 31 | 32 | # PyInstaller 33 | # Usually these files are written by a python script from a template 34 | # before PyInstaller builds the exe, so as to inject date/other infos into it. 35 | *.manifest 36 | *.spec 37 | 38 | # Installer logs 39 | pip-log.txt 40 | pip-delete-this-directory.txt 41 | 42 | # Unit test / coverage reports 43 | htmlcov/ 44 | .tox/ 45 | .coverage 46 | .coverage.* 47 | .cache 48 | nosetests.xml 49 | coverage.xml 50 | *.cover 51 | .hypothesis/ 52 | .pytest_cache/ 53 | 54 | # Translations 55 | *.mo 56 | *.pot 57 | 58 | # Django stuff: 59 | *.log 60 | local_settings.py 61 | 62 | 63 | # Flask stuff: 64 | instance/ 65 | .webassets-cache 66 | 67 | # Scrapy stuff: 68 | .scrapy 69 | 70 | # Sphinx documentation 71 | docs/_build/ 72 | 73 | # PyBuilder 74 | target/ 75 | 76 | # Jupyter Notebook 77 | .ipynb_checkpoints 78 | 79 | # pyenv 80 | .python-version 81 | 82 | # celery beat schedule file 83 | celerybeat-schedule 84 | 85 | # SageMath parsed files 86 | *.sage.py 87 | 88 | # Environments 89 | .env 90 | .venv 91 | env/ 92 | venv/ 93 | ENV/ 94 | env.bak/ 95 | venv.bak/ 96 | 97 | # Spyder project settings 98 | .spyderproject 99 | .spyproject 100 | 101 | # Rope project settings 102 | .ropeproject 103 | 104 | # mkdocs documentation 105 | /site 106 | 107 | # mypy 108 | .mypy_cache/ 109 | 110 | # Database 111 | db.sqlite3 112 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | MIT License 2 | 3 | Copyright (c) 2018 Juan Benitez 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 | # SIMPLE CRUD API WITH DJANGO REST FRAMEWORK 2 | [Django REST framework](http://www.django-rest-framework.org/) is a powerful and flexible toolkit for building Web APIs. 3 | 4 | ## Requirements 5 | - Python 3.6 6 | - Django 3.1 7 | - Django REST Framework 8 | 9 | ## Installation 10 | After you cloned the repository, you want to create a virtual environment, so you have a clean python installation. 11 | You can do this by running the command 12 | ``` 13 | python -m venv env 14 | ``` 15 | 16 | After this, it is necessary to activate the virtual environment, you can get more information about this [here](https://docs.python.org/3/tutorial/venv.html) 17 | 18 | You can install all the required dependencies by running 19 | ``` 20 | pip install -r requirements.txt 21 | ``` 22 | 23 | ## Structure 24 | In a RESTful API, endpoints (URLs) define the structure of the API and how end users access data from our application using the HTTP methods - GET, POST, PUT, DELETE. Endpoints should be logically organized around _collections_ and _elements_, both of which are resources. 25 | 26 | In our case, we have one single resource, `movies`, so we will use the following URLS - `/movies/` and `/movies/` for collections and elements, respectively: 27 | 28 | Endpoint |HTTP Method | CRUD Method | Result 29 | -- | -- |-- |-- 30 | `movies` | GET | READ | Get all movies 31 | `movies/:id` | GET | READ | Get a single movie 32 | `movies`| POST | CREATE | Create a new movie 33 | `movies/:id` | PUT | UPDATE | Update a movie 34 | `movies/:id` | DELETE | DELETE | Delete a movie 35 | 36 | ## Use 37 | We can test the API using [curl](https://curl.haxx.se/) or [httpie](https://github.com/jakubroztocil/httpie#installation), or we can use [Postman](https://www.postman.com/) 38 | 39 | Httpie is a user-friendly http client that's written in Python. Let's try and install that. 40 | 41 | You can install httpie using pip: 42 | ``` 43 | pip install httpie 44 | ``` 45 | 46 | First, we have to start up Django's development server. 47 | ``` 48 | python manage.py runserver 49 | ``` 50 | Only authenticated users can use the API services, for that reason if we try this: 51 | ``` 52 | http http://127.0.0.1:8000/api/v1/movies/ 53 | ``` 54 | we get: 55 | ``` 56 | { 57 | "detail": "Authentication credentials were not provided." 58 | } 59 | ``` 60 | Instead, if we try to access with credentials: 61 | ``` 62 | http http://127.0.0.1:8000/api/v1/movies/3 "Authorization: Bearer eyJ0eXAiOiJKV1QiLCJhbGciOiJIUzI1NiJ9.eyJ0b2tlbl90eXBlIjoiYWNjZXNzIiwiZXhwIjoxNjE2MjA4Mjk1LCJqdGkiOiI4NGNhZmMzMmFiZDA0MDQ2YjZhMzFhZjJjMmRiNjUyYyIsInVzZXJfaWQiOjJ9.NJrs-sXnghAwcMsIWyCvE2RuGcQ3Hiu5p3vBmLkHSvM" 63 | ``` 64 | we get the movie with id = 3 65 | ``` 66 | { "title": "Avengers", "genre": "Superheroes", "year": 2012, "creator": "admin" } 67 | ``` 68 | 69 | ## Create users and Tokens 70 | 71 | First we need to create a user, so we can log in 72 | ``` 73 | http POST http://127.0.0.1:8000/api/v1/auth/register/ email="email@email.com" username="USERNAME" password1="PASSWORD" password2="PASSWORD" 74 | ``` 75 | 76 | After we create an account we can use those credentials to get a token 77 | 78 | To get a token first we need to request 79 | ``` 80 | http http://127.0.0.1:8000/api/v1/auth/token/ username="username" password="password" 81 | ``` 82 | after that, we get the token 83 | ``` 84 | { 85 | "refresh": "eyJ0eXAiOiJKV1QiLCJhbGciOiJIUzI1NiJ9.eyJ0b2tlbl90eXBlIjoicmVmcmVzaCIsImV4cCI6MTYxNjI5MjMyMSwianRpIjoiNGNkODA3YTlkMmMxNDA2NWFhMzNhYzMxOTgyMzhkZTgiLCJ1c2VyX2lkIjozfQ.hP1wPOPvaPo2DYTC9M1AuOSogdRL_mGP30CHsbpf4zA", 86 | "access": "eyJ0eXAiOiJKV1QiLCJhbGciOiJIUzI1NiJ9.eyJ0b2tlbl90eXBlIjoiYWNjZXNzIiwiZXhwIjoxNjE2MjA2MjIxLCJqdGkiOiJjNTNlNThmYjE4N2Q0YWY2YTE5MGNiMzhlNjU5ZmI0NSIsInVzZXJfaWQiOjN9.Csz-SgXoItUbT3RgB3zXhjA2DAv77hpYjqlgEMNAHps" 87 | } 88 | ``` 89 | We got two tokens, the access token will be used to authenticated all the requests we need to make, this access token will expire after some time. 90 | We can use the refresh token to request a need access token. 91 | 92 | requesting new access token 93 | ``` 94 | http http://127.0.0.1:8000/api/v1/auth/token/refresh/ refresh="eyJ0eXAiOiJKV1QiLCJhbGciOiJIUzI1NiJ9.eyJ0b2tlbl90eXBlIjoicmVmcmVzaCIsImV4cCI6MTYxNjI5MjMyMSwianRpIjoiNGNkODA3YTlkMmMxNDA2NWFhMzNhYzMxOTgyMzhkZTgiLCJ1c2VyX2lkIjozfQ.hP1wPOPvaPo2DYTC9M1AuOSogdRL_mGP30CHsbpf4zA" 95 | ``` 96 | and we will get a new access token 97 | ``` 98 | { 99 | "access": "eyJ0eXAiOiJKV1QiLCJhbGciOiJIUzI1NiJ9.eyJ0b2tlbl90eXBlIjoiYWNjZXNzIiwiZXhwIjoxNjE2MjA4Mjk1LCJqdGkiOiI4NGNhZmMzMmFiZDA0MDQ2YjZhMzFhZjJjMmRiNjUyYyIsInVzZXJfaWQiOjJ9.NJrs-sXnghAwcMsIWyCvE2RuGcQ3Hiu5p3vBmLkHSvM" 100 | } 101 | ``` 102 | 103 | 104 | The API has some restrictions: 105 | - The movies are always associated with a creator (user who created it). 106 | - Only authenticated users may create and see movies. 107 | - Only the creator of a movie may update or delete it. 108 | - The API doesn't allow unauthenticated requests. 109 | 110 | ### Commands 111 | ``` 112 | Get all movies 113 | http http://127.0.0.1:8000/api/v1/movies/ "Authorization: Bearer {YOUR_TOKEN}" 114 | Get a single movie 115 | http GET http://127.0.0.1:8000/api/v1/movies/{movie_id}/ "Authorization: Bearer {YOUR_TOKEN}" 116 | Create a new movie 117 | http POST http://127.0.0.1:8000/api/v1/movies/ "Authorization: Bearer {YOUR_TOKEN}" title="Ant Man and The Wasp" genre="Action" year=2018 118 | Full update a movie 119 | http PUT http://127.0.0.1:8000/api/v1/movies/{movie_id}/ "Authorization: Bearer {YOUR_TOKEN}" title="AntMan and The Wasp" genre="Action" year=2018 120 | Partial update a movie 121 | http PATCH http://127.0.0.1:8000/api/v1/movies/{movie_id}/ "Authorization: Bearer {YOUR_TOKEN}" title="AntMan and The Wasp" 122 | Delete a movie 123 | http DELETE http://127.0.0.1:8000/api/v1/movies/{movie_id}/ "Authorization: Bearer {YOUR_TOKEN}" 124 | ``` 125 | 126 | ### Pagination 127 | The API supports pagination, by default responses have a page_size=10 but if you want change that you can pass through params page_size={your_page_size_number} 128 | ``` 129 | http http://127.0.0.1:8000/api/v1/movies/?page=1 "Authorization: Bearer {YOUR_TOKEN}" 130 | http http://127.0.0.1:8000/api/v1/movies/?page=3 "Authorization: Bearer {YOUR_TOKEN}" 131 | http http://127.0.0.1:8000/api/v1/movies/?page=3&page_size=15 "Authorization: Bearer {YOUR_TOKEN}" 132 | ``` 133 | 134 | ### Filters 135 | The API supports filtering, you can filter by the attributes of a movie like this 136 | ``` 137 | http http://127.0.0.1:8000/api/v1/movies/?title="AntMan" "Authorization: Bearer {YOUR_TOKEN}" 138 | http http://127.0.0.1:8000/api/v1/movies/?year=2020 "Authorization: Bearer {YOUR_TOKEN}" 139 | http http://127.0.0.1:8000/api/v1/movies/?year__gt=2019&year__lt=2022 "Authorization: Bearer {YOUR_TOKEN}" 140 | http http://127.0.0.1:8000/api/v1/movies/?genre="Action" "Authorization: Bearer {YOUR_TOKEN}" 141 | http http://127.0.0.1:8000/api/v1/movies/?creator__username="myUsername" "Authorization: Bearer {YOUR_TOKEN}" 142 | ``` 143 | 144 | You can also combine multiples filters like so 145 | ``` 146 | http http://127.0.0.1:8000/api/v1/movies/?title="AntMan"&year=2020 "Authorization: Bearer {YOUR_TOKEN}" 147 | http http://127.0.0.1:8000/api/v1/movies/?year__gt=2019&year__lt=2022&genre="Action" "Authorization: Bearer {YOUR_TOKEN}" 148 | ``` 149 | 150 | -------------------------------------------------------------------------------- /api_crud/__init__.py: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/juanbeniteza/django-rest-framework-crud/93854bc814bbf5e4f129e295611dedbbb297cecc/api_crud/__init__.py -------------------------------------------------------------------------------- /api_crud/settings.py: -------------------------------------------------------------------------------- 1 | """ 2 | Django settings for api_crud project. 3 | 4 | Generated by 'django-admin startproject' using Django 2.0.6. 5 | 6 | For more information on this file, see 7 | https://docs.djangoproject.com/en/2.0/topics/settings/ 8 | 9 | For the full list of settings and their values, see 10 | https://docs.djangoproject.com/en/2.0/ref/settings/ 11 | """ 12 | 13 | import os 14 | 15 | # Build paths inside the project like this: os.path.join(BASE_DIR, ...) 16 | BASE_DIR = os.path.dirname(os.path.dirname(os.path.abspath(__file__))) 17 | 18 | 19 | # Quick-start development settings - unsuitable for production 20 | # See https://docs.djangoproject.com/en/2.0/howto/deployment/checklist/ 21 | 22 | # SECURITY WARNING: keep the secret key used in production secret! 23 | SECRET_KEY = 'ftov1!91yf@7f7&g2%*@0_e^)ac&f&9jeloc@#v76#^b1dhbl#' 24 | 25 | # SECURITY WARNING: don't run with debug turned on in production! 26 | DEBUG = True 27 | 28 | ALLOWED_HOSTS = [] 29 | 30 | REST_FRAMEWORK = { 31 | 'DEFAULT_AUTHENTICATION_CLASSES': ( 32 | 'rest_framework_simplejwt.authentication.JWTAuthentication', 33 | ), 34 | 'DEFAULT_FILTER_BACKENDS': ( 35 | 'django_filters.rest_framework.DjangoFilterBackend', 36 | ), 37 | } 38 | 39 | 40 | # Application definition 41 | 42 | INSTALLED_APPS = [ 43 | 'django.contrib.admin', 44 | 'django.contrib.auth', 45 | 'django.contrib.contenttypes', 46 | 'django.contrib.sessions', 47 | 'django.contrib.messages', 48 | 'django.contrib.staticfiles', 49 | 'rest_framework', 50 | 'django_filters', 51 | 'authentication', 52 | 'movies', 53 | ] 54 | 55 | SITE_ID = 1 56 | 57 | MIDDLEWARE = [ 58 | 'django.middleware.security.SecurityMiddleware', 59 | 'django.contrib.sessions.middleware.SessionMiddleware', 60 | 'django.middleware.common.CommonMiddleware', 61 | 'django.middleware.csrf.CsrfViewMiddleware', 62 | 'django.contrib.auth.middleware.AuthenticationMiddleware', 63 | 'django.contrib.messages.middleware.MessageMiddleware', 64 | 'django.middleware.clickjacking.XFrameOptionsMiddleware', 65 | ] 66 | 67 | ROOT_URLCONF = 'api_crud.urls' 68 | 69 | TEMPLATES = [ 70 | { 71 | 'BACKEND': 'django.template.backends.django.DjangoTemplates', 72 | 'DIRS': [], 73 | 'APP_DIRS': True, 74 | 'OPTIONS': { 75 | 'context_processors': [ 76 | 'django.template.context_processors.debug', 77 | 'django.template.context_processors.request', 78 | 'django.contrib.auth.context_processors.auth', 79 | 'django.contrib.messages.context_processors.messages', 80 | ], 81 | }, 82 | }, 83 | ] 84 | 85 | WSGI_APPLICATION = 'api_crud.wsgi.application' 86 | 87 | 88 | # Database 89 | # https://docs.djangoproject.com/en/2.0/ref/settings/#databases 90 | 91 | DATABASES = { 92 | 'default': { 93 | 'ENGINE': 'django.db.backends.sqlite3', 94 | 'NAME': os.path.join(BASE_DIR, 'db.sqlite3'), 95 | } 96 | } 97 | 98 | 99 | # Password validation 100 | # https://docs.djangoproject.com/en/2.0/ref/settings/#auth-password-validators 101 | 102 | AUTH_PASSWORD_VALIDATORS = [ 103 | { 104 | 'NAME': 'django.contrib.auth.password_validation.UserAttributeSimilarityValidator', 105 | }, 106 | { 107 | 'NAME': 'django.contrib.auth.password_validation.MinimumLengthValidator', 108 | }, 109 | { 110 | 'NAME': 'django.contrib.auth.password_validation.CommonPasswordValidator', 111 | }, 112 | { 113 | 'NAME': 'django.contrib.auth.password_validation.NumericPasswordValidator', 114 | }, 115 | ] 116 | 117 | 118 | # Internationalization 119 | # https://docs.djangoproject.com/en/2.0/topics/i18n/ 120 | 121 | LANGUAGE_CODE = 'en-us' 122 | 123 | TIME_ZONE = 'UTC' 124 | 125 | USE_I18N = True 126 | 127 | USE_L10N = True 128 | 129 | USE_TZ = True 130 | 131 | 132 | # Static files (CSS, JavaScript, Images) 133 | # https://docs.djangoproject.com/en/2.0/howto/static-files/ 134 | 135 | STATIC_URL = '/static/' 136 | -------------------------------------------------------------------------------- /api_crud/urls.py: -------------------------------------------------------------------------------- 1 | 2 | from django.contrib import admin 3 | from django.urls import include, path 4 | 5 | # urls 6 | urlpatterns = [ 7 | path('api/v1/movies/', include('movies.urls')), 8 | path('api/v1/auth/', include('authentication.urls')), 9 | path('admin/', admin.site.urls), 10 | ] -------------------------------------------------------------------------------- /api_crud/views.py: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/juanbeniteza/django-rest-framework-crud/93854bc814bbf5e4f129e295611dedbbb297cecc/api_crud/views.py -------------------------------------------------------------------------------- /api_crud/wsgi.py: -------------------------------------------------------------------------------- 1 | """ 2 | WSGI config for api_crud 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/2.0/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", "api_crud.settings") 15 | 16 | application = get_wsgi_application() 17 | -------------------------------------------------------------------------------- /authentication/__init__.py: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/juanbeniteza/django-rest-framework-crud/93854bc814bbf5e4f129e295611dedbbb297cecc/authentication/__init__.py -------------------------------------------------------------------------------- /authentication/admin.py: -------------------------------------------------------------------------------- 1 | from django.contrib import admin 2 | 3 | # Register your models here. 4 | -------------------------------------------------------------------------------- /authentication/apps.py: -------------------------------------------------------------------------------- 1 | from django.apps import AppConfig 2 | 3 | 4 | class AuthConfig(AppConfig): 5 | name = 'authentication' 6 | -------------------------------------------------------------------------------- /authentication/migrations/__init__.py: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/juanbeniteza/django-rest-framework-crud/93854bc814bbf5e4f129e295611dedbbb297cecc/authentication/migrations/__init__.py -------------------------------------------------------------------------------- /authentication/models.py: -------------------------------------------------------------------------------- 1 | from django.db import models 2 | 3 | # Create your models here. 4 | -------------------------------------------------------------------------------- /authentication/serializers.py: -------------------------------------------------------------------------------- 1 | from rest_framework import serializers 2 | from django.contrib.auth.models import User 3 | from rest_framework.validators import UniqueValidator 4 | from django.contrib.auth.password_validation import validate_password 5 | 6 | 7 | class RegisterSerializer(serializers.ModelSerializer): 8 | email = serializers.EmailField( 9 | required=True, 10 | validators=[UniqueValidator(queryset=User.objects.all())] 11 | ) 12 | 13 | password = serializers.CharField(write_only=True, required=True, validators=[validate_password]) 14 | password2 = serializers.CharField(write_only=True, required=True) 15 | 16 | class Meta: 17 | model = User 18 | fields = ('username', 'password', 'password2', 'email', 'first_name', 'last_name') 19 | 20 | def validate(self, attrs): 21 | if attrs['password'] != attrs['password2']: 22 | raise serializers.ValidationError({"password": "Password fields didn't match."}) 23 | 24 | return attrs 25 | 26 | def create(self, validated_data): 27 | del validated_data['password2'] 28 | user = User.objects.create(**validated_data) 29 | 30 | user.set_password(validated_data['password']) 31 | user.save() 32 | 33 | return user 34 | -------------------------------------------------------------------------------- /authentication/tests.py: -------------------------------------------------------------------------------- 1 | from django.test import TestCase 2 | 3 | # Create your tests here. 4 | -------------------------------------------------------------------------------- /authentication/urls.py: -------------------------------------------------------------------------------- 1 | from django.urls import path 2 | from authentication.views import RegisterView 3 | from rest_framework_simplejwt.views import TokenObtainPairView, TokenRefreshView 4 | 5 | urlpatterns = [ 6 | path('token/', TokenObtainPairView.as_view(), name='token_obtain_pair'), 7 | path('token/refresh/', TokenRefreshView.as_view(), name='token_refresh'), 8 | path('register/', RegisterView.as_view(), name='auth_register'), 9 | ] -------------------------------------------------------------------------------- /authentication/views.py: -------------------------------------------------------------------------------- 1 | from django.contrib.auth.models import User 2 | from rest_framework import generics 3 | from rest_framework.permissions import AllowAny 4 | from .serializers import RegisterSerializer 5 | 6 | 7 | class RegisterView(generics.CreateAPIView): 8 | queryset = User.objects.all() 9 | permission_classes = (AllowAny,) 10 | serializer_class = RegisterSerializer 11 | -------------------------------------------------------------------------------- /manage.py: -------------------------------------------------------------------------------- 1 | import os 2 | import sys 3 | 4 | if __name__ == "__main__": 5 | os.environ.setdefault("DJANGO_SETTINGS_MODULE", "api_crud.settings") 6 | try: 7 | from django.core.management import execute_from_command_line 8 | except ImportError as exc: 9 | raise ImportError( 10 | "Couldn't import Django. Are you sure it's installed and " 11 | "available on your PYTHONPATH environment variable? Did you " 12 | "forget to activate a virtual environment?" 13 | ) from exc 14 | execute_from_command_line(sys.argv) 15 | -------------------------------------------------------------------------------- /movies/__init__.py: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/juanbeniteza/django-rest-framework-crud/93854bc814bbf5e4f129e295611dedbbb297cecc/movies/__init__.py -------------------------------------------------------------------------------- /movies/admin.py: -------------------------------------------------------------------------------- 1 | from django.contrib import admin 2 | from .models import Movie 3 | 4 | admin.site.register(Movie) 5 | -------------------------------------------------------------------------------- /movies/apps.py: -------------------------------------------------------------------------------- 1 | from django.apps import AppConfig 2 | 3 | 4 | class MoviesConfig(AppConfig): 5 | name = 'movies' 6 | -------------------------------------------------------------------------------- /movies/filters.py: -------------------------------------------------------------------------------- 1 | from django_filters import rest_framework as filters 2 | from .models import Movie 3 | 4 | 5 | # We create filters for each field we want to be able to filter on 6 | class MovieFilter(filters.FilterSet): 7 | title = filters.CharFilter(lookup_expr='icontains') 8 | genre = filters.CharFilter(lookup_expr='icontains') 9 | year = filters.NumberFilter() 10 | year__gt = filters.NumberFilter(field_name='year', lookup_expr='gt') 11 | year__lt = filters.NumberFilter(field_name='year', lookup_expr='lt') 12 | creator__username = filters.CharFilter(lookup_expr='icontains') 13 | 14 | class Meta: 15 | model = Movie 16 | fields = ['title', 'genre', 'year', 'year__gt', 'year__lt', 'creator__username'] 17 | 18 | -------------------------------------------------------------------------------- /movies/migrations/0001_initial.py: -------------------------------------------------------------------------------- 1 | # Generated by Django 2.0.6 on 2018-07-01 22:54 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='Movie', 19 | fields=[ 20 | ('id', models.AutoField(auto_created=True, primary_key=True, serialize=False, verbose_name='ID')), 21 | ('title', models.CharField(max_length=100)), 22 | ('genre', models.CharField(max_length=100)), 23 | ('year', models.IntegerField()), 24 | ('created_at', models.DateTimeField(auto_now_add=True)), 25 | ('updated_at', models.DateTimeField(auto_now=True)), 26 | ('creator', models.ForeignKey(on_delete=django.db.models.deletion.CASCADE, related_name='movies', to=settings.AUTH_USER_MODEL)), 27 | ], 28 | ), 29 | ] 30 | -------------------------------------------------------------------------------- /movies/migrations/__init__.py: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/juanbeniteza/django-rest-framework-crud/93854bc814bbf5e4f129e295611dedbbb297cecc/movies/migrations/__init__.py -------------------------------------------------------------------------------- /movies/models.py: -------------------------------------------------------------------------------- 1 | from django.db import models 2 | 3 | 4 | class Movie(models.Model): 5 | title = models.CharField(max_length=100) 6 | genre = models.CharField(max_length=100) 7 | year = models.IntegerField() 8 | created_at = models.DateTimeField(auto_now_add=True) 9 | updated_at = models.DateTimeField(auto_now=True) 10 | creator = models.ForeignKey('auth.User', related_name='movies', on_delete=models.CASCADE) 11 | 12 | class Meta: 13 | ordering = ['-id'] 14 | 15 | 16 | -------------------------------------------------------------------------------- /movies/pagination.py: -------------------------------------------------------------------------------- 1 | from rest_framework.pagination import PageNumberPagination 2 | 3 | 4 | class CustomPagination(PageNumberPagination): 5 | page_size = 10 6 | page_size_query_param = 'page_size' 7 | -------------------------------------------------------------------------------- /movies/permissions.py: -------------------------------------------------------------------------------- 1 | from rest_framework import permissions 2 | from rest_framework.exceptions import PermissionDenied 3 | 4 | 5 | class IsOwnerOrReadOnly(permissions.BasePermission): 6 | """ 7 | Custom permission to only allow creator of an object to edit it. 8 | """ 9 | 10 | def has_object_permission(self, request, view, obj): 11 | # Read permissions are allowed to any request, 12 | # so we'll always allow GET, HEAD or OPTIONS requests. 13 | if request.method in permissions.SAFE_METHODS: 14 | return True 15 | 16 | # Write permissions are only allowed to the creator of the movie 17 | return obj.creator == request.user 18 | -------------------------------------------------------------------------------- /movies/serializers.py: -------------------------------------------------------------------------------- 1 | from rest_framework import serializers 2 | from .models import Movie 3 | from django.contrib.auth.models import User 4 | 5 | 6 | class MovieSerializer(serializers.ModelSerializer): # create class to serializer model 7 | creator = serializers.ReadOnlyField(source='creator.username') 8 | 9 | class Meta: 10 | model = Movie 11 | fields = ('id', 'title', 'genre', 'year', 'creator') 12 | 13 | 14 | class UserSerializer(serializers.ModelSerializer): # create class to serializer user model 15 | movies = serializers.PrimaryKeyRelatedField(many=True, queryset=Movie.objects.all()) 16 | 17 | class Meta: 18 | model = User 19 | fields = ('id', 'username', 'movies') 20 | -------------------------------------------------------------------------------- /movies/urls.py: -------------------------------------------------------------------------------- 1 | from django.urls import path 2 | from . import views 3 | 4 | 5 | urlpatterns = [ 6 | path('', views.ListCreateMovieAPIView.as_view(), name='get_post_movies'), 7 | path('/', views.RetrieveUpdateDestroyMovieAPIView.as_view(), name='get_delete_update_movie'), 8 | ] -------------------------------------------------------------------------------- /movies/views.py: -------------------------------------------------------------------------------- 1 | from rest_framework.generics import RetrieveUpdateDestroyAPIView, ListCreateAPIView 2 | from rest_framework.permissions import IsAuthenticated 3 | from django_filters import rest_framework as filters 4 | from .models import Movie 5 | from .permissions import IsOwnerOrReadOnly 6 | from .serializers import MovieSerializer 7 | from .pagination import CustomPagination 8 | from .filters import MovieFilter 9 | 10 | 11 | class ListCreateMovieAPIView(ListCreateAPIView): 12 | serializer_class = MovieSerializer 13 | queryset = Movie.objects.all() 14 | permission_classes = [IsAuthenticated] 15 | pagination_class = CustomPagination 16 | filter_backends = (filters.DjangoFilterBackend,) 17 | filterset_class = MovieFilter 18 | 19 | def perform_create(self, serializer): 20 | # Assign the user who created the movie 21 | serializer.save(creator=self.request.user) 22 | 23 | 24 | class RetrieveUpdateDestroyMovieAPIView(RetrieveUpdateDestroyAPIView): 25 | serializer_class = MovieSerializer 26 | queryset = Movie.objects.all() 27 | permission_classes = [IsAuthenticated, IsOwnerOrReadOnly] 28 | 29 | 30 | 31 | 32 | 33 | -------------------------------------------------------------------------------- /requirements.txt: -------------------------------------------------------------------------------- 1 | asgiref==3.3.1 2 | Django==3.1.8 3 | django-filter==2.4.0 4 | djangorestframework==3.12.2 5 | djangorestframework-simplejwt==4.6.0 6 | pytz==2021.1 7 | sqlparse==0.4.1 8 | --------------------------------------------------------------------------------