├── author ├── __init__.py ├── tests │ ├── __init__.py │ └── tests.py ├── migrations │ └── __init__.py ├── urls.py ├── admin.py ├── views.py ├── serializers.py ├── apps.py └── models.py ├── author_service ├── __init__.py ├── asgi.py ├── wsgi.py ├── urls.py └── settings.py ├── .gitignore ├── .flake8 ├── requirements.txt ├── README.md ├── manage.py └── .github └── workflows └── test.yml /author/__init__.py: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /author/tests/__init__.py: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /author_service/__init__.py: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /author/migrations/__init__.py: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /author/urls.py: -------------------------------------------------------------------------------- 1 | # Create your urls here 2 | 3 | urlpatterns = [] 4 | -------------------------------------------------------------------------------- /author/admin.py: -------------------------------------------------------------------------------- 1 | from django.contrib import admin 2 | 3 | from author.models import Author 4 | 5 | admin.site.register(Author) 6 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | .idea/ 2 | .vscode/ 3 | *.iml 4 | .env 5 | .DS_Store 6 | venv/ 7 | .pytest_cache/ 8 | **__pycache__/ 9 | **db.sqlite3 10 | -------------------------------------------------------------------------------- /author/views.py: -------------------------------------------------------------------------------- 1 | from rest_framework import viewsets 2 | 3 | 4 | class AuthorViewSet(viewsets.ModelViewSet): 5 | # write your code here 6 | pass 7 | -------------------------------------------------------------------------------- /author/serializers.py: -------------------------------------------------------------------------------- 1 | from rest_framework import serializers 2 | 3 | 4 | class AuthorSerializer(serializers.ModelSerializer): 5 | # write your code here 6 | pass 7 | -------------------------------------------------------------------------------- /author/apps.py: -------------------------------------------------------------------------------- 1 | from django.apps import AppConfig 2 | 3 | 4 | class AuthorConfig(AppConfig): 5 | default_auto_field = "django.db.models.BigAutoField" 6 | name = "author" 7 | -------------------------------------------------------------------------------- /.flake8: -------------------------------------------------------------------------------- 1 | [flake8] 2 | inline-quotes = " 3 | ignore = E203, E266, W503, N807, N818, F401 4 | max-line-length = 79 5 | max-complexity = 18 6 | select = B,C,E,F,W,T4,B9,Q0,N8,VNE 7 | exclude = 8 | **migrations 9 | venv 10 | tests -------------------------------------------------------------------------------- /requirements.txt: -------------------------------------------------------------------------------- 1 | django==4.1 2 | flake8==5.0.4 3 | flake8-quotes==3.3.1 4 | flake8-variables-names==0.0.5 5 | pep8-naming==0.13.2 6 | django-debug-toolbar==3.2.4 7 | django-crispy-forms==1.14.0 8 | asgiref==3.5.2 9 | djangorestframework==3.13.1 10 | pytz==2022.2.1 11 | sqlparse==0.4.2 12 | -------------------------------------------------------------------------------- /author/models.py: -------------------------------------------------------------------------------- 1 | from django.db import models 2 | 3 | 4 | class Author(models.Model): 5 | first_name = models.CharField(max_length=64) 6 | last_name = models.CharField(max_length=64) 7 | pseudonym = models.CharField(max_length=64, null=True, blank=True) 8 | age = models.IntegerField() 9 | retired = models.BooleanField() 10 | -------------------------------------------------------------------------------- /author_service/asgi.py: -------------------------------------------------------------------------------- 1 | """ 2 | ASGI config for author_service 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/4.1/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", "author_service.settings") 15 | 16 | application = get_asgi_application() 17 | -------------------------------------------------------------------------------- /author_service/wsgi.py: -------------------------------------------------------------------------------- 1 | """ 2 | WSGI config for author_service 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/4.1/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", "author_service.settings") 15 | 16 | application = get_wsgi_application() 17 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # Author CRUD Implementation 2 | 3 | **Please note:** read [the guideline](https://github.com/mate-academy/py-task-guideline/blob/main/README.md) 4 | before starting. 5 | 6 | In this task, you should implement full CRUD API the `Author` model. 7 | 8 | The `Author` model has the following fields: 9 | - `first_name` (with the `max_length` of 64); 10 | - `last_name` (with the `max_length` of 64); 11 | - `pseudonym` (with the `max_length` of 64, can be null); 12 | - `age` (integer field); 13 | - `retired` (boolean field). 14 | 15 | **Please note:** you should also modify the `author/urls.py` file to make things work. 16 | -------------------------------------------------------------------------------- /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", "author_service.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 | -------------------------------------------------------------------------------- /author_service/urls.py: -------------------------------------------------------------------------------- 1 | """author_service URL Configuration 2 | 3 | The `urlpatterns` list routes URLs to views. For more information please see: 4 | https://docs.djangoproject.com/en/4.1/topics/http/urls/ 5 | Examples: 6 | Function views 7 | 1. Add an import: from my_app import views 8 | 2. Add a URL to urlpatterns: path('', views.home, name='home') 9 | Class-based views 10 | 1. Add an import: from other_app.views import Home 11 | 2. Add a URL to urlpatterns: path('', Home.as_view(), name='home') 12 | Including another URLconf 13 | 1. Import the include() function: from django.urls import include, path 14 | 2. Add a URL to urlpatterns: path('blog/', include('blog.urls')) 15 | """ 16 | from django.contrib import admin 17 | from django.urls import path, include 18 | 19 | urlpatterns = [ 20 | path("admin/", admin.site.urls), 21 | path("api/", include("author.urls", namespace="author")), 22 | ] 23 | -------------------------------------------------------------------------------- /.github/workflows/test.yml: -------------------------------------------------------------------------------- 1 | name: Test 2 | 3 | on: [push, pull_request_target] 4 | 5 | jobs: 6 | test: 7 | runs-on: ubuntu-latest 8 | 9 | steps: 10 | - name: Checkout repo 11 | uses: actions/checkout@v2 12 | with: 13 | ref: ${{github.event.pull_request.head.ref}} 14 | repository: ${{github.event.pull_request.head.repo.full_name}} 15 | 16 | - name: Set Up Python 3.10 17 | uses: actions/setup-python@v2 18 | with: 19 | python-version: "3.10" 20 | 21 | - name: Install requirements 22 | run: | 23 | python -m pip install --upgrade pip 24 | pip install -r requirements.txt 25 | - name: Run tests 26 | timeout-minutes: 5 27 | run: python manage.py test 28 | 29 | - name: Run flake8 30 | run: flake8 31 | 32 | - uses: mate-academy/auto-approve-action@v2 33 | if: ${{ github.event.pull_request && success() }} 34 | with: 35 | github-token: ${{ github.token }} 36 | - uses: mate-academy/auto-reject-action@v2 37 | if: ${{ github.event.pull_request && failure() }} 38 | with: 39 | github-token: ${{ github.token }} 40 | -------------------------------------------------------------------------------- /author_service/settings.py: -------------------------------------------------------------------------------- 1 | """ 2 | Django settings for author_service project. 3 | 4 | Generated by 'django-admin startproject' using Django 4.1. 5 | 6 | For more information on this file, see 7 | https://docs.djangoproject.com/en/4.1/topics/settings/ 8 | 9 | For the full list of settings and their values, see 10 | https://docs.djangoproject.com/en/4.1/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().parent.parent 17 | 18 | 19 | # Quick-start development settings - unsuitable for production 20 | # See https://docs.djangoproject.com/en/4.1/howto/deployment/checklist/ 21 | 22 | # SECURITY WARNING: keep the secret key used in production secret! 23 | SECRET_KEY = ( 24 | "django-insecure-4l*aqy-h!k0a$)68da!_zw#x8)jo0r&!5aqd9gcv9z3zr)h)*=" 25 | ) 26 | 27 | # SECURITY WARNING: don't run with debug turned on in production! 28 | DEBUG = True 29 | 30 | ALLOWED_HOSTS = [] 31 | 32 | 33 | # Application definition 34 | 35 | INSTALLED_APPS = [ 36 | "django.contrib.admin", 37 | "django.contrib.auth", 38 | "django.contrib.contenttypes", 39 | "django.contrib.sessions", 40 | "django.contrib.messages", 41 | "django.contrib.staticfiles", 42 | "rest_framework", 43 | "author", 44 | ] 45 | 46 | MIDDLEWARE = [ 47 | "django.middleware.security.SecurityMiddleware", 48 | "django.contrib.sessions.middleware.SessionMiddleware", 49 | "django.middleware.common.CommonMiddleware", 50 | "django.middleware.csrf.CsrfViewMiddleware", 51 | "django.contrib.auth.middleware.AuthenticationMiddleware", 52 | "django.contrib.messages.middleware.MessageMiddleware", 53 | "django.middleware.clickjacking.XFrameOptionsMiddleware", 54 | ] 55 | 56 | ROOT_URLCONF = "author_service.urls" 57 | 58 | TEMPLATES = [ 59 | { 60 | "BACKEND": "django.template.backends.django.DjangoTemplates", 61 | "DIRS": [], 62 | "APP_DIRS": True, 63 | "OPTIONS": { 64 | "context_processors": [ 65 | "django.template.context_processors.debug", 66 | "django.template.context_processors.request", 67 | "django.contrib.auth.context_processors.auth", 68 | "django.contrib.messages.context_processors.messages", 69 | ], 70 | }, 71 | }, 72 | ] 73 | 74 | WSGI_APPLICATION = "author_service.wsgi.application" 75 | 76 | 77 | # Database 78 | # https://docs.djangoproject.com/en/4.1/ref/settings/#databases 79 | 80 | DATABASES = { 81 | "default": { 82 | "ENGINE": "django.db.backends.sqlite3", 83 | "NAME": BASE_DIR / "db.sqlite3", 84 | } 85 | } 86 | 87 | 88 | # Password validation 89 | # https://docs.djangoproject.com/en/4.1/ref/settings/#auth-password-validators 90 | 91 | AUTH_PASSWORD_VALIDATORS = [ 92 | { 93 | "NAME": "django.contrib.auth.password_validation." 94 | "UserAttributeSimilarityValidator", 95 | }, 96 | { 97 | "NAME": "django.contrib.auth.password_validation." 98 | "MinimumLengthValidator", 99 | }, 100 | { 101 | "NAME": "django.contrib.auth.password_validation." 102 | "CommonPasswordValidator", 103 | }, 104 | { 105 | "NAME": "django.contrib.auth.password_validation." 106 | "NumericPasswordValidator", 107 | }, 108 | ] 109 | 110 | 111 | # Internationalization 112 | # https://docs.djangoproject.com/en/4.1/topics/i18n/ 113 | 114 | LANGUAGE_CODE = "en-us" 115 | 116 | TIME_ZONE = "UTC" 117 | 118 | USE_I18N = True 119 | 120 | USE_TZ = True 121 | 122 | 123 | # Static files (CSS, JavaScript, Images) 124 | # https://docs.djangoproject.com/en/4.1/howto/static-files/ 125 | 126 | STATIC_URL = "static/" 127 | 128 | # Default primary key field type 129 | # https://docs.djangoproject.com/en/4.1/ref/settings/#default-auto-field 130 | 131 | DEFAULT_AUTO_FIELD = "django.db.models.BigAutoField" 132 | -------------------------------------------------------------------------------- /author/tests/tests.py: -------------------------------------------------------------------------------- 1 | from django.test import TestCase 2 | from django.urls import reverse 3 | from rest_framework import status 4 | 5 | from rest_framework.test import APIClient 6 | 7 | from author.models import Author 8 | from author.serializers import AuthorSerializer 9 | 10 | AUTHORS_URL = reverse("author:manage-list") 11 | 12 | 13 | class AuthorApiTests(TestCase): 14 | def setUp(self) -> None: 15 | self.client = APIClient() 16 | Author.objects.create( 17 | first_name="Joanne", 18 | last_name="Rowling", 19 | pseudonym="J. K. Rowling", 20 | age=57, 21 | retired=False, 22 | ) 23 | Author.objects.create( 24 | first_name="Dan", last_name="Brown", age=58, retired=False 25 | ) 26 | 27 | def test_get_authors(self): 28 | authors = self.client.get(AUTHORS_URL) 29 | serializer = AuthorSerializer(Author.objects.all(), many=True) 30 | self.assertEqual(authors.status_code, status.HTTP_200_OK) 31 | self.assertEqual(authors.data, serializer.data) 32 | 33 | def test_post_authors(self): 34 | authors = self.client.post( 35 | AUTHORS_URL, 36 | { 37 | "first_name": "Serhii", 38 | "last_name": "Zhadan", 39 | "age": 47, 40 | "retired": False, 41 | }, 42 | ) 43 | db_authors = Author.objects.all() 44 | self.assertEqual(authors.status_code, status.HTTP_201_CREATED) 45 | self.assertEqual(db_authors.count(), 3) 46 | self.assertEqual(db_authors.filter(first_name="Serhii").count(), 1) 47 | 48 | def test_post_invalid_author(self): 49 | authors = self.client.post( 50 | AUTHORS_URL, 51 | { 52 | "first_name": "Serhii", 53 | "last_name": "Zhadan", 54 | "age": "extremely young", 55 | "retired": False, 56 | }, 57 | ) 58 | not_created_author = Author.objects.filter(first_name="Serhii") 59 | self.assertEqual(authors.status_code, status.HTTP_400_BAD_REQUEST) 60 | self.assertEqual(not_created_author.count(), 0) 61 | 62 | def test_get_author(self): 63 | response = self.client.get(f"{AUTHORS_URL}1/") 64 | serializer = AuthorSerializer( 65 | Author( 66 | id=1, 67 | first_name="Joanne", 68 | last_name="Rowling", 69 | pseudonym="J. K. Rowling", 70 | age=57, 71 | retired=False, 72 | ) 73 | ) 74 | self.assertEqual(response.status_code, status.HTTP_200_OK) 75 | self.assertEqual(response.data, serializer.data) 76 | 77 | def test_get_invalid_author(self): 78 | response = self.client.get(f"{AUTHORS_URL}50/") 79 | self.assertEqual(response.status_code, status.HTTP_404_NOT_FOUND) 80 | 81 | def test_put_author(self): 82 | self.client.put( 83 | f"{AUTHORS_URL}1/", 84 | { 85 | "first_name": "Serhii", 86 | "last_name": "Zhadan", 87 | "age": 47, 88 | "retired": True, 89 | }, 90 | ) 91 | db_author = Author.objects.get(id=1) 92 | self.assertEqual( 93 | [ 94 | db_author.first_name, 95 | db_author.last_name, 96 | db_author.age, 97 | db_author.retired, 98 | ], 99 | ["Serhii", "Zhadan", 47, True], 100 | ) 101 | 102 | def test_put_invalid_author(self): 103 | response = self.client.put( 104 | f"{AUTHORS_URL}1/", 105 | { 106 | "first_name": "Serhii", 107 | "last_name": "Zhadan", 108 | "age": "hundred years", 109 | }, 110 | ) 111 | db_author = Author.objects.get(id=1) 112 | self.assertEqual(db_author.age, 57) 113 | self.assertEqual(response.status_code, status.HTTP_400_BAD_REQUEST) 114 | 115 | def test_patch_author(self): 116 | response = self.client.patch( 117 | f"{AUTHORS_URL}1/", 118 | { 119 | "first_name": "Serhii", 120 | }, 121 | ) 122 | db_author = Author.objects.get(id=1) 123 | self.assertEqual(db_author.first_name, "Serhii") 124 | self.assertEqual(response.status_code, status.HTTP_200_OK) 125 | 126 | def test_patch_invalid_author(self): 127 | response = self.client.patch( 128 | f"{AUTHORS_URL}1/", 129 | { 130 | "age": "hundred years", 131 | }, 132 | ) 133 | db_author = Author.objects.get(id=1) 134 | self.assertEqual(db_author.age, 57) 135 | self.assertEqual(response.status_code, status.HTTP_400_BAD_REQUEST) 136 | 137 | def test_delete_author(self): 138 | response = self.client.delete( 139 | f"{AUTHORS_URL}1/", 140 | ) 141 | db_author_id_1 = Author.objects.filter(id=1) 142 | self.assertEqual(db_author_id_1.count(), 0) 143 | self.assertEqual(response.status_code, status.HTTP_204_NO_CONTENT) 144 | 145 | def test_delete_invalid_author(self): 146 | response = self.client.delete( 147 | f"{AUTHORS_URL}50/", 148 | ) 149 | self.assertEqual(response.status_code, status.HTTP_404_NOT_FOUND) 150 | --------------------------------------------------------------------------------