├── musician ├── __init__.py ├── tests │ ├── __init__.py │ ├── test_models.py │ └── test_musician_api.py ├── migrations │ └── __init__.py ├── urls.py ├── models.py ├── admin.py └── apps.py ├── music_school ├── __init__.py ├── asgi.py ├── wsgi.py ├── urls.py └── settings.py ├── .gitignore ├── .flake8 ├── requirements.txt ├── manage.py ├── README.md └── .github └── workflows └── test.yml /musician/__init__.py: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /music_school/__init__.py: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /musician/tests/__init__.py: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /musician/migrations/__init__.py: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /musician/urls.py: -------------------------------------------------------------------------------- 1 | urlpatterns = [] 2 | 3 | app_name = "musician" 4 | -------------------------------------------------------------------------------- /musician/models.py: -------------------------------------------------------------------------------- 1 | from django.db import models 2 | 3 | 4 | class Musician(models.Model): 5 | pass 6 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | .idea/ 2 | .vscode/ 3 | *.iml 4 | .env 5 | .DS_Store 6 | venv/ 7 | .pytest_cache/ 8 | **__pycache__/ 9 | **db.sqlite3 10 | -------------------------------------------------------------------------------- /musician/admin.py: -------------------------------------------------------------------------------- 1 | from django.contrib import admin 2 | 3 | from musician.models import Musician 4 | 5 | admin.site.register(Musician) 6 | -------------------------------------------------------------------------------- /musician/apps.py: -------------------------------------------------------------------------------- 1 | from django.apps import AppConfig 2 | 3 | 4 | class MusicianConfig(AppConfig): 5 | default_auto_field = "django.db.models.BigAutoField" 6 | name = "musician" 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 | asgiref==3.5.2 2 | Django==4.1 3 | djangorestframework==3.13.1 4 | pytz==2022.2.1 5 | sqlparse==0.4.2 6 | flake8==5.0.4 7 | flake8-annotations==2.9.1 8 | flake8-quotes==3.3.1 9 | flake8-variables-names==0.0.5 10 | pep8-naming==0.13.2 11 | -------------------------------------------------------------------------------- /music_school/asgi.py: -------------------------------------------------------------------------------- 1 | """ 2 | ASGI config for music_school 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", "music_school.settings") 15 | 16 | application = get_asgi_application() 17 | -------------------------------------------------------------------------------- /music_school/wsgi.py: -------------------------------------------------------------------------------- 1 | """ 2 | WSGI config for music_school 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", "music_school.settings") 15 | 16 | application = get_wsgi_application() 17 | -------------------------------------------------------------------------------- /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", "music_school.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 | -------------------------------------------------------------------------------- /music_school/urls.py: -------------------------------------------------------------------------------- 1 | """music_school 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("musician.urls", namespace="musician")), 22 | ] 23 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # Music School 2 | 3 | **Please note:** read [the guideline](https://github.com/mate-academy/py-task-guideline/blob/main/README.md) 4 | before starting. 5 | 6 | Welcome to our Music School! Many musicians apply to us every day, and we need you to create a model for it named `Musician`. 7 | 8 | This model should have such fields: 9 | * `first_name` — `CharField` with the `max_length` of 63; 10 | * `last_name` — `CharField` with the `max_length` of 63; 11 | * `instrument` — `CharField` with the `max_length` of 63; 12 | * `age` — `IntegerField` (we **do not** accept people who are under 14); 13 | * `date_of_applying` — `DateField` with `auto_now_add` parameter. 14 | 15 | Implement the `__str__` method in this model to return the string in the `"first_name last_name"` form. 16 | 17 | Also, we need to know whether the musician is an adult, so implement the `is_adult` property (an adult person is the one who is 21+). 18 | And last but not least, implement the most basic `CRUD` functionality! 19 | 20 | **Please note:** the `is_adult` property should be displayed in the `GET` requests. 21 | -------------------------------------------------------------------------------- /.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 flake8 26 | run: flake8 27 | - name: Run tests 28 | timeout-minutes: 5 29 | run: python manage.py test 30 | - uses: mate-academy/auto-approve-action@v2 31 | if: ${{ github.event.pull_request && success() }} 32 | with: 33 | github-token: ${{ github.token }} 34 | - uses: mate-academy/auto-reject-action@v2 35 | if: ${{ github.event.pull_request && failure() }} 36 | with: 37 | github-token: ${{ github.token }} -------------------------------------------------------------------------------- /musician/tests/test_models.py: -------------------------------------------------------------------------------- 1 | from django.test import TestCase 2 | from django.db.models import CharField, IntegerField, DateField 3 | 4 | from musician.models import Musician 5 | 6 | 7 | class MusicianModelTest(TestCase): 8 | def setUp(self) -> None: 9 | Musician.objects.create( 10 | first_name="Joseph", 11 | last_name="Green", 12 | instrument="the guitar", 13 | age=16, 14 | ) 15 | 16 | def test_types_of_fields(self): 17 | char_fields = ["first_name", "last_name", "instrument"] 18 | 19 | for field in char_fields: 20 | with self.subTest(field): 21 | print(Musician._meta.get_field(field)) 22 | self.assertEqual( 23 | isinstance(Musician._meta.get_field(field), CharField), 24 | True, 25 | ) 26 | 27 | self.assertEqual( 28 | isinstance(Musician._meta.get_field("age"), IntegerField), True 29 | ) 30 | self.assertEqual( 31 | isinstance( 32 | Musician._meta.get_field("date_of_applying"), DateField 33 | ), 34 | True, 35 | ) 36 | 37 | def test_fields_max_length(self): 38 | char_fields = ["first_name", "last_name", "instrument"] 39 | 40 | for field in char_fields: 41 | with self.subTest(f"{field} max_length"): 42 | self.assertEqual( 43 | Musician._meta.get_field(field).max_length, 63 44 | ) 45 | 46 | def test_str_method(self): 47 | musician = Musician.objects.get(first_name="Joseph") 48 | self.assertEqual(str(musician), "Joseph Green") 49 | 50 | def test_is_adult_property(self): 51 | self.assertTrue(hasattr(Musician, "is_adult")) 52 | -------------------------------------------------------------------------------- /music_school/settings.py: -------------------------------------------------------------------------------- 1 | """ 2 | Django settings for music_school 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-qh877b6vq5n&n#*d)$qx(7i381tvy)m=j9*i$=%3-5*4)pp(*e" 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 | "musician", 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 = "music_school.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 = "music_school.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 | -------------------------------------------------------------------------------- /musician/tests/test_musician_api.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 musician.models import Musician 8 | from musician.serializers import MusicianSerializer 9 | 10 | MUSICIAN_URL = reverse("musician:manage-list") 11 | 12 | 13 | class MusicianApiTests(TestCase): 14 | def setUp(self) -> None: 15 | self.client = APIClient() 16 | self.first_musician = Musician.objects.create( 17 | first_name="Joseph", 18 | last_name="Green", 19 | instrument="the guitar", 20 | age=17, 21 | ) 22 | self.second_musician = Musician.objects.create( 23 | first_name="Robert", 24 | last_name="Brown", 25 | instrument="the guitar", 26 | age=28, 27 | ) 28 | 29 | def test_age_has_min_value(self): 30 | payload = { 31 | "first_name": "Jim", 32 | "last_name": "Greg", 33 | "instrument": "the violin", 34 | "age": 10, 35 | } 36 | 37 | response = self.client.post(MUSICIAN_URL, payload) 38 | musicians = Musician.objects.all() 39 | self.assertEqual(response.status_code, status.HTTP_400_BAD_REQUEST) 40 | self.assertEqual(musicians.count(), 2) 41 | 42 | def test_get_musicians(self): 43 | musicians = self.client.get(MUSICIAN_URL) 44 | serializer = MusicianSerializer(Musician.objects.all(), many=True) 45 | self.assertEqual(musicians.status_code, status.HTTP_200_OK) 46 | self.assertEqual(musicians.data, serializer.data) 47 | 48 | def test_post_musicians(self): 49 | musicians = self.client.post( 50 | MUSICIAN_URL, 51 | { 52 | "first_name": "Bob", 53 | "last_name": "Yellow", 54 | "instrument": "the guitar", 55 | "age": 47, 56 | }, 57 | ) 58 | db_musicians = Musician.objects.all() 59 | self.assertEqual(musicians.status_code, status.HTTP_201_CREATED) 60 | self.assertEqual(db_musicians.count(), 3) 61 | self.assertEqual(db_musicians.filter(first_name="Bob").count(), 1) 62 | 63 | def test_get_musician(self): 64 | response = self.client.get(f"{MUSICIAN_URL}{self.first_musician.id}/") 65 | serializer = MusicianSerializer(self.first_musician) 66 | self.assertEqual(response.status_code, status.HTTP_200_OK) 67 | self.assertEqual(response.data, serializer.data) 68 | self.assertIn("is_adult", response.data) 69 | 70 | def test_get_invalid_musician(self): 71 | response = self.client.get(f"{MUSICIAN_URL}50/") 72 | self.assertEqual(response.status_code, status.HTTP_404_NOT_FOUND) 73 | 74 | def test_put_musician(self): 75 | self.client.put( 76 | f"{MUSICIAN_URL}1/", 77 | { 78 | "first_name": "Bob", 79 | "last_name": "Smith", 80 | "instrument": "the violin", 81 | "age": 22, 82 | }, 83 | ) 84 | db_musician = Musician.objects.get(id=1) 85 | self.assertEqual( 86 | [ 87 | db_musician.first_name, 88 | db_musician.last_name, 89 | db_musician.instrument, 90 | db_musician.age, 91 | ], 92 | ["Bob", "Smith", "the violin", 22], 93 | ) 94 | 95 | def test_put_invalid_musician(self): 96 | response = self.client.put( 97 | f"{MUSICIAN_URL}50/", 98 | { 99 | "first_name": "Bob", 100 | "last_name": "Smith", 101 | "instrument": "the violin", 102 | "age": 22, 103 | }, 104 | ) 105 | self.assertEqual(response.status_code, status.HTTP_404_NOT_FOUND) 106 | 107 | def test_patch_musician(self): 108 | response = self.client.patch( 109 | f"{MUSICIAN_URL}1/", 110 | { 111 | "first_name": "Leyla", 112 | }, 113 | ) 114 | db_musician = Musician.objects.get(id=1) 115 | self.assertEqual(db_musician.first_name, "Leyla") 116 | self.assertEqual(response.status_code, status.HTTP_200_OK) 117 | 118 | def test_patch_invalid_musician(self): 119 | response = self.client.patch( 120 | f"{MUSICIAN_URL}50/", 121 | { 122 | "first_name": "Leyla", 123 | }, 124 | ) 125 | self.assertEqual(response.status_code, status.HTTP_404_NOT_FOUND) 126 | 127 | def test_delete_musician(self): 128 | response = self.client.delete( 129 | f"{MUSICIAN_URL}1/", 130 | ) 131 | db_musician_id_1 = Musician.objects.filter(id=1) 132 | self.assertEqual(db_musician_id_1.count(), 0) 133 | self.assertEqual(response.status_code, status.HTTP_204_NO_CONTENT) 134 | 135 | def test_delete_invalid_musician(self): 136 | response = self.client.delete( 137 | f"{MUSICIAN_URL}50/", 138 | ) 139 | self.assertEqual(response.status_code, status.HTTP_404_NOT_FOUND) 140 | --------------------------------------------------------------------------------