27 |
28 | cd my_project
29 |
30 | git clone https://github.com/geoffreynyaga/django-and-djangorestframework-pytest-tutorial.git .
31 | ```
32 |
33 | ## Testing setup🧪🧪
34 |
35 | The projects uses pytest and black as the formatting option. The tests also check for consistencies on code format.
36 |
37 | To initiate tests follow the steps below:
38 |
39 | 1. Its advised to create a virtual environment in the root folder
40 |
41 | ```bash
42 | virtualenv venv
43 | ```
44 |
45 | 2. Activate the environent.
46 |
47 | a. For Linux/MacOS users use the command below
48 |
49 | ```bash
50 | source venv/bin/activate
51 | ```
52 |
53 | b. for windows users
54 |
55 | ```bash
56 | cd venv/Scripts
57 |
58 | activate.bat
59 | ```
60 |
61 | 3. Install the requirements
62 |
63 | ```bash
64 | pip install -r requirements.txt
65 | ```
66 |
67 | 4. Run the pytest command
68 |
69 | ```bash
70 | pytest
71 | ```
72 |
73 | The testing results will be displayed and there will also be a `htmlcov` folder generated inside the project that will contain the code coverage details.
74 |
75 |
76 | .
77 | ├── accounts
78 | │ ├── api
79 | │ │ └── tests
80 | │ └── migrations
81 | ├── classroom
82 | ├── htmlcov
83 | └── venv
84 |
85 |
86 | Open up the folder and open the `index.html` in your browser to see this information.
87 |
88 | ## Project layout
89 |
90 | ```bash
91 | .
92 | ├── accounts
93 | │ ├── api
94 | │ │ └── tests
95 | │ └── migrations
96 | ├── classroom
97 | ├── htmlcov
98 | └── venv # after you create the virtualenv
99 | ```
100 |
--------------------------------------------------------------------------------
/School/__init__.py:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/geoffreynyaga/django-and-djangorestframework-pytest-tutorial/6d1f60ae642b83d475db8718ea3ee45d5177d5eb/School/__init__.py
--------------------------------------------------------------------------------
/School/asgi.py:
--------------------------------------------------------------------------------
1 | """
2 | ASGI config for 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/3.0/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", "School.settings")
15 |
16 | application = get_asgi_application()
17 |
--------------------------------------------------------------------------------
/School/github_settings.py:
--------------------------------------------------------------------------------
1 | """
2 | Django settings for School project.
3 |
4 | Generated by 'django-admin startproject' using Django 3.0.2.
5 |
6 | For more information on this file, see
7 | https://docs.djangoproject.com/en/3.0/topics/settings/
8 |
9 | For the full list of settings and their values, see
10 | https://docs.djangoproject.com/en/3.0/ref/settings/
11 | """
12 |
13 | import os
14 |
15 | from decouple import config
16 |
17 |
18 | # Build paths inside the project like this: os.path.join(BASE_DIR, ...)
19 | BASE_DIR = os.path.dirname(os.path.dirname(os.path.abspath(__file__)))
20 |
21 |
22 | # Quick-start development settings - unsuitable for production
23 | # See https://docs.djangoproject.com/en/3.0/howto/deployment/checklist/
24 |
25 | # SECURITY WARNING: keep the secret key used in production secret!
26 | SECRET_KEY = "4o0(y15#k-+6+kln0pc4-=)!8()hgbeb)4cjibblzj^7qe3hzv"
27 |
28 | # SECURITY WARNING: don't run with debug turned on in production!
29 | DEBUG = True
30 |
31 | ALLOWED_HOSTS = []
32 |
33 |
34 | # Application definition
35 |
36 | INSTALLED_APPS = [
37 | "django.contrib.admin",
38 | "django.contrib.auth",
39 | "django.contrib.contenttypes",
40 | "django.contrib.sessions",
41 | "django.contrib.messages",
42 | "django.contrib.staticfiles",
43 | "classroom",
44 | "rest_framework",
45 | "rest_framework.authtoken",
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 = "School.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 = "School.wsgi.application"
77 |
78 |
79 | # Database
80 | # https://docs.djangoproject.com/en/3.0/ref/settings/#databases
81 |
82 | # DATABASES = {
83 | # "default": {
84 | # "ENGINE": "django.db.backends.sqlite3",
85 | # "NAME": os.path.join(BASE_DIR, "db.sqlite3"),
86 | # }
87 | # }
88 |
89 |
90 | DATABASES = {
91 | "default": {
92 | "ENGINE": "django.db.backends.postgresql",
93 | "NAME": "pytest_db",
94 | "USER": "postgres",
95 | "PASSWORD": config("DATABASE_PASSWORD"),
96 | "HOST": "localhost",
97 | "PORT": "5432",
98 | }
99 | }
100 |
101 |
102 | # Password validation
103 | # https://docs.djangoproject.com/en/3.0/ref/settings/#auth-password-validators
104 |
105 | AUTH_PASSWORD_VALIDATORS = [
106 | {
107 | "NAME": "django.contrib.auth.password_validation.UserAttributeSimilarityValidator",
108 | },
109 | {"NAME": "django.contrib.auth.password_validation.MinimumLengthValidator",},
110 | {"NAME": "django.contrib.auth.password_validation.CommonPasswordValidator",},
111 | {"NAME": "django.contrib.auth.password_validation.NumericPasswordValidator",},
112 | ]
113 |
114 |
115 | # Internationalization
116 | # https://docs.djangoproject.com/en/3.0/topics/i18n/
117 |
118 | LANGUAGE_CODE = "en-us"
119 |
120 | TIME_ZONE = "UTC"
121 |
122 | USE_I18N = True
123 |
124 | USE_L10N = True
125 |
126 | USE_TZ = True
127 |
128 |
129 | # Static files (CSS, JavaScript, Images)
130 | # https://docs.djangoproject.com/en/3.0/howto/static-files/
131 |
132 | STATIC_URL = "/static/"
133 |
134 |
135 | REST_FRAMEWORK = {
136 | "DEFAULT_AUTHENTICATION_CLASSES": [
137 | "rest_framework.authentication.BasicAuthentication",
138 | "rest_framework.authentication.SessionAuthentication",
139 | ]
140 | }
141 |
--------------------------------------------------------------------------------
/School/settings.py:
--------------------------------------------------------------------------------
1 | """
2 | Django settings for School project.
3 |
4 | Generated by 'django-admin startproject' using Django 3.0.2.
5 |
6 | For more information on this file, see
7 | https://docs.djangoproject.com/en/3.0/topics/settings/
8 |
9 | For the full list of settings and their values, see
10 | https://docs.djangoproject.com/en/3.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/3.0/howto/deployment/checklist/
21 |
22 | # SECURITY WARNING: keep the secret key used in production secret!
23 | SECRET_KEY = "4o0(y15#k-+6+kln0pc4-=)!8()hgbeb)4cjibblzj^7qe3hzv"
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 | "classroom",
41 | "rest_framework",
42 | "rest_framework.authtoken",
43 | ]
44 |
45 | MIDDLEWARE = [
46 | "django.middleware.security.SecurityMiddleware",
47 | "django.contrib.sessions.middleware.SessionMiddleware",
48 | "django.middleware.common.CommonMiddleware",
49 | "django.middleware.csrf.CsrfViewMiddleware",
50 | "django.contrib.auth.middleware.AuthenticationMiddleware",
51 | "django.contrib.messages.middleware.MessageMiddleware",
52 | "django.middleware.clickjacking.XFrameOptionsMiddleware",
53 | ]
54 |
55 | ROOT_URLCONF = "School.urls"
56 |
57 | TEMPLATES = [
58 | {
59 | "BACKEND": "django.template.backends.django.DjangoTemplates",
60 | "DIRS": [],
61 | "APP_DIRS": True,
62 | "OPTIONS": {
63 | "context_processors": [
64 | "django.template.context_processors.debug",
65 | "django.template.context_processors.request",
66 | "django.contrib.auth.context_processors.auth",
67 | "django.contrib.messages.context_processors.messages",
68 | ],
69 | },
70 | },
71 | ]
72 |
73 | WSGI_APPLICATION = "School.wsgi.application"
74 |
75 |
76 | # Database
77 | # https://docs.djangoproject.com/en/3.0/ref/settings/#databases
78 |
79 | DATABASES = {
80 | "default": {
81 | "ENGINE": "django.db.backends.sqlite3",
82 | "NAME": os.path.join(BASE_DIR, "db.sqlite3"),
83 | }
84 | }
85 |
86 | # DATABASES = {
87 | # 'default': {
88 | # 'ENGINE': 'django.db.backends.postgresql',
89 | # 'NAME': 'mydatabase',
90 | # 'USER': 'mydatabaseuser',
91 | # 'PASSWORD': 'mypassword',
92 | # 'HOST': '127.0.0.1',
93 | # 'PORT': '5432',
94 | # }
95 | # }
96 |
97 |
98 | # Password validation
99 | # https://docs.djangoproject.com/en/3.0/ref/settings/#auth-password-validators
100 |
101 | AUTH_PASSWORD_VALIDATORS = [
102 | {
103 | "NAME": "django.contrib.auth.password_validation.UserAttributeSimilarityValidator",
104 | },
105 | {"NAME": "django.contrib.auth.password_validation.MinimumLengthValidator",},
106 | {"NAME": "django.contrib.auth.password_validation.CommonPasswordValidator",},
107 | {"NAME": "django.contrib.auth.password_validation.NumericPasswordValidator",},
108 | ]
109 |
110 |
111 | # Internationalization
112 | # https://docs.djangoproject.com/en/3.0/topics/i18n/
113 |
114 | LANGUAGE_CODE = "en-us"
115 |
116 | TIME_ZONE = "UTC"
117 |
118 | USE_I18N = True
119 |
120 | USE_L10N = True
121 |
122 | USE_TZ = True
123 |
124 |
125 | # Static files (CSS, JavaScript, Images)
126 | # https://docs.djangoproject.com/en/3.0/howto/static-files/
127 |
128 | STATIC_URL = "/static/"
129 |
130 |
131 | REST_FRAMEWORK = {
132 | "DEFAULT_PERMISSION_CLASSES": [
133 | "rest_framework.permissions.IsAuthenticatedOrReadOnly"
134 | ],
135 | "DEFAULT_AUTHENTICATION_CLASSES": [
136 | "rest_framework.authentication.BasicAuthentication",
137 | "rest_framework.authentication.TokenAuthentication",
138 | ],
139 | }
140 |
--------------------------------------------------------------------------------
/School/urls.py:
--------------------------------------------------------------------------------
1 | """School URL Configuration
2 |
3 | The `urlpatterns` list routes URLs to views. For more information please see:
4 | https://docs.djangoproject.com/en/3.0/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, re_path
18 |
19 | from rest_framework.authtoken import views
20 |
21 |
22 | urlpatterns = [
23 | path("admin/", admin.site.urls),
24 | path("api/", include("classroom.api.urls")),
25 | re_path(r"^api-auth/", include("rest_framework.urls")),
26 | re_path(r"^api-token-auth/", views.obtain_auth_token),
27 | ]
28 |
--------------------------------------------------------------------------------
/School/wsgi.py:
--------------------------------------------------------------------------------
1 | """
2 | WSGI config for 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/3.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", "School.settings")
15 |
16 | application = get_wsgi_application()
17 |
--------------------------------------------------------------------------------
/classroom/__init__.py:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/geoffreynyaga/django-and-djangorestframework-pytest-tutorial/6d1f60ae642b83d475db8718ea3ee45d5177d5eb/classroom/__init__.py
--------------------------------------------------------------------------------
/classroom/admin.py:
--------------------------------------------------------------------------------
1 | from django.contrib import admin
2 | from .models import Student, Classroom
3 |
4 | # Register your models here.
5 | admin.site.register(Student)
6 | admin.site.register(Classroom)
7 |
--------------------------------------------------------------------------------
/classroom/api/__init__.py:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/geoffreynyaga/django-and-djangorestframework-pytest-tutorial/6d1f60ae642b83d475db8718ea3ee45d5177d5eb/classroom/api/__init__.py
--------------------------------------------------------------------------------
/classroom/api/serializers.py:
--------------------------------------------------------------------------------
1 | from rest_framework.serializers import ModelSerializer
2 |
3 | from classroom.models import Student, Classroom
4 |
5 |
6 | class StudentSerializer(ModelSerializer):
7 | class Meta:
8 | model = Student
9 | fields = (
10 | "first_name",
11 | "last_name",
12 | "username",
13 | "admission_number",
14 | "is_qualified",
15 | "average_score",
16 | )
17 |
18 |
19 | class ClassroomSerializer(ModelSerializer):
20 | class Meta:
21 | model = Classroom
22 | fields = (
23 | "name",
24 | "student_capacity",
25 | "students",
26 | )
27 |
--------------------------------------------------------------------------------
/classroom/api/tests/test_views.py:
--------------------------------------------------------------------------------
1 | import pytest
2 |
3 | from classroom.models import Student, Classroom
4 |
5 | from mixer.backend.django import mixer
6 |
7 | from django.test import TestCase
8 | from rest_framework.test import APIClient
9 | from rest_framework.reverse import reverse
10 |
11 | pytestmark = pytest.mark.django_db
12 |
13 |
14 | class TestStudentAPIViews(TestCase):
15 | def setUp(self):
16 | self.client = APIClient()
17 |
18 | print(self.client, "self.client")
19 |
20 | def test_student_list_works(self):
21 | # create a student
22 |
23 | student = mixer.blend(Student, first_name="Geoffrey")
24 |
25 | student2 = mixer.blend(Student, first_name="Naomi")
26 |
27 | url = reverse("student_list_api")
28 |
29 | # call the url
30 | response = self.client.get(url)
31 |
32 | # print(dir(response), "response")
33 |
34 | # aseertions
35 | # - json
36 | # - status
37 | assert response.json() != None
38 |
39 | assert len(response.json()) == 2
40 |
41 | assert response.status_code == 200
42 |
43 | def test_student_create_works(self):
44 | # data
45 |
46 | input_data = {
47 | "first_name": "Wangari",
48 | "last_name": "Maathai",
49 | "username": "",
50 | "admission_number": 9876,
51 | "is_qualified": True,
52 | "average_score": 100,
53 | }
54 |
55 | url = reverse("student_create_api")
56 |
57 | # call the url
58 | response = self.client.post(url, data=input_data)
59 |
60 | # assertions
61 | # - json
62 | # - status
63 |
64 | print(response.data)
65 | assert response.json() != None
66 | assert response.status_code == 201
67 | assert Student.objects.count() == 1
68 |
69 | def test_student_detail_works(self):
70 | # create a student
71 |
72 | student = mixer.blend(Student, pk=1, first_name="Geoffrey")
73 | print(Student.objects.last().pk, "qs")
74 | url = reverse("student_detail_api", kwargs={"pk": 1})
75 | response = self.client.get(url)
76 |
77 | student2 = mixer.blend(Student, pk=2, first_name="Naomi")
78 | url2 = reverse("student_detail_api", kwargs={"pk": 2})
79 | response2 = self.client.get(url2)
80 |
81 | # assertions
82 | # - json
83 | # - status
84 |
85 | print(response.json(), "response json")
86 |
87 | assert response.json() != None
88 | assert response.status_code == 200
89 | assert response.json()["first_name"] == "Geoffrey"
90 | assert response.json()["username"] == "geoffrey"
91 |
92 | assert response2.json()["first_name"] == "Naomi"
93 | assert response2.json()["username"] == "naomi"
94 |
95 | def test_student_delete_works(self):
96 | # create a student
97 |
98 | student = mixer.blend(Student, pk=1, first_name="Geoffrey")
99 | assert Student.objects.count() == 1
100 |
101 | url = reverse("student_delete_api", kwargs={"pk": 1})
102 | response = self.client.delete(url)
103 | # assertions
104 | # - json
105 | # - status
106 |
107 | print(dir(response.json), "response json")
108 | print((response.status_code), "response json")
109 |
110 | assert response.status_code == 204
111 |
112 | assert Student.objects.count() == 0
113 |
114 |
115 | class TestClassroomAPIViews(TestCase):
116 | def setUp(self):
117 | self.client = APIClient()
118 |
119 | print(self.client, "self.client")
120 |
121 | # method 1
122 |
123 | # from rest_framework.authtoken.models import Token
124 |
125 | # from django.contrib.auth import get_user_model
126 |
127 | # User = get_user_model()
128 |
129 | # self.our_user = User.objects.create(username="testuser", password="abcde")
130 |
131 | # self.token = Token.objects.create(user=self.our_user)
132 |
133 | # print(self.token.key, "token")
134 |
135 | # self.client.credentials(HTTP_AUTHORIZATION="Token " + self.token.key)
136 |
137 | # method 2 # normal
138 |
139 | from django.contrib.auth import get_user_model
140 |
141 | User = get_user_model()
142 |
143 | self.our_user = User.objects.create_user(username="testuser", password="abcde")
144 |
145 | self.token_url = "http://localhost:8000/api-token-auth/"
146 |
147 | user_data = {"username": "testuser", "password": "abcde"}
148 |
149 | response = self.client.post(self.token_url, data=user_data)
150 |
151 | # print(dir(response.), "reponse")
152 | print((response.data), "reponse")
153 | """
154 | {
155 | "token": "b89d0bab1b4f818c5af6682cec66f84b0bdb664c"
156 | }
157 | """
158 |
159 | self.client.credentials(HTTP_AUTHORIZATION="Token " + response.data["token"])
160 |
161 | def test_classroom_qs_works(self):
162 | classroom = mixer.blend(Classroom, student_capacity=20)
163 | classroom2 = mixer.blend(Classroom, student_capacity=27)
164 |
165 | url = reverse("class_qs_api", kwargs={"student_capacity": 15})
166 |
167 | response = self.client.get(url,)
168 |
169 | assert response.status_code == 202
170 | assert response.data["classroom_data"] != []
171 | assert response.data["number_of_classes"] == 2
172 |
--------------------------------------------------------------------------------
/classroom/api/urls.py:
--------------------------------------------------------------------------------
1 | from django.contrib import admin
2 | from django.urls import path
3 |
4 | from .views import (
5 | StudentListAPIView,
6 | StudentCreateAPIView,
7 | StudentDeleteAPIView,
8 | StudentDetailAPIView,
9 | ClassroomNumberAPIView,
10 | )
11 |
12 | urlpatterns = [
13 | path("student/list/", StudentListAPIView.as_view(), name="student_list_api"),
14 | path("student/create/", StudentCreateAPIView.as_view(), name="student_create_api"),
15 | path(
16 | "student//", StudentDetailAPIView.as_view(), name="student_detail_api"
17 | ),
18 | path(
19 | "student//delete/",
20 | StudentDeleteAPIView.as_view(),
21 | name="student_delete_api",
22 | ),
23 | path(
24 | "classroom//",
25 | ClassroomNumberAPIView.as_view(),
26 | name="class_qs_api",
27 | ),
28 | ]
29 |
--------------------------------------------------------------------------------
/classroom/api/views.py:
--------------------------------------------------------------------------------
1 | from rest_framework.generics import (
2 | ListAPIView,
3 | CreateAPIView,
4 | DestroyAPIView,
5 | RetrieveAPIView,
6 | )
7 |
8 | from rest_framework import status, permissions, authentication
9 |
10 | from rest_framework.views import APIView
11 |
12 | from rest_framework.response import Response
13 |
14 | from .serializers import StudentSerializer, ClassroomSerializer
15 | from classroom.models import Student, Classroom
16 |
17 |
18 | class StudentListAPIView(ListAPIView):
19 | serializer_class = StudentSerializer
20 | model = Student
21 | queryset = Student.objects.all()
22 |
23 |
24 | class StudentCreateAPIView(CreateAPIView):
25 | serializer_class = StudentSerializer
26 | model = Student
27 | queryset = Student.objects.all()
28 |
29 |
30 | class StudentDetailAPIView(RetrieveAPIView):
31 | serializer_class = StudentSerializer
32 | model = Student
33 | queryset = Student.objects.all()
34 |
35 |
36 | class StudentDeleteAPIView(DestroyAPIView):
37 | serializer_class = StudentSerializer
38 | model = Student
39 | queryset = Student.objects.all()
40 |
41 |
42 | class ClassroomNumberAPIView(APIView):
43 |
44 | serializer_class = ClassroomSerializer
45 | model = Classroom
46 | queryset = Student.objects.all()
47 |
48 | permission_classes = [permissions.IsAuthenticated]
49 | authentication_classes = [authentication.TokenAuthentication]
50 |
51 | def get(self, *args, **kwargs):
52 |
53 | url_number = self.kwargs.get("student_capacity")
54 | print(url_number, "student_capacity")
55 |
56 | classroom_qs = Classroom.objects.filter(student_capacity__gte=url_number)
57 | print(classroom_qs, "classroom_qs")
58 |
59 | number_of_classes = classroom_qs.count()
60 |
61 | serialized_data = ClassroomSerializer(classroom_qs, many=True)
62 | # print(serialized_data.data, "serialized_data")
63 |
64 | if serialized_data.is_valid:
65 | return Response(
66 | {
67 | "classroom_data": serialized_data.data,
68 | "number_of_classes": number_of_classes,
69 | },
70 | status=status.HTTP_202_ACCEPTED,
71 | )
72 | else:
73 | return Response(
74 | {"Error": "Could not serialize data"},
75 | status=status.HTTP_203_NON_AUTHORITATIVE_INFORMATION,
76 | )
77 |
--------------------------------------------------------------------------------
/classroom/apps.py:
--------------------------------------------------------------------------------
1 | from django.apps import AppConfig
2 |
3 |
4 | class ClassroomConfig(AppConfig):
5 | name = "classroom"
6 |
--------------------------------------------------------------------------------
/classroom/migrations/0001_initial.py:
--------------------------------------------------------------------------------
1 | # Generated by Django 3.0.2 on 2020-02-01 21:40
2 |
3 | from django.db import migrations, models
4 |
5 |
6 | class Migration(migrations.Migration):
7 |
8 | initial = True
9 |
10 | dependencies = []
11 |
12 | operations = [
13 | migrations.CreateModel(
14 | name="Student",
15 | fields=[
16 | (
17 | "id",
18 | models.AutoField(
19 | auto_created=True,
20 | primary_key=True,
21 | serialize=False,
22 | verbose_name="ID",
23 | ),
24 | ),
25 | ("first_name", models.CharField(max_length=50)),
26 | ("last_name", models.CharField(max_length=50)),
27 | ("admission_number", models.IntegerField(unique=True)),
28 | ("is_qualified", models.BooleanField(default=False)),
29 | ("average_score", models.FloatField(blank=True, null=True)),
30 | ],
31 | ),
32 | migrations.CreateModel(
33 | name="Classroom",
34 | fields=[
35 | (
36 | "id",
37 | models.AutoField(
38 | auto_created=True,
39 | primary_key=True,
40 | serialize=False,
41 | verbose_name="ID",
42 | ),
43 | ),
44 | ("name", models.CharField(max_length=120)),
45 | ("student_capacity", models.IntegerField()),
46 | ("students", models.ManyToManyField(to="classroom.Student")),
47 | ],
48 | ),
49 | ]
50 |
--------------------------------------------------------------------------------
/classroom/migrations/0002_student_username.py:
--------------------------------------------------------------------------------
1 | # Generated by Django 3.0.2 on 2020-02-20 11:28
2 |
3 | from django.db import migrations, models
4 |
5 |
6 | class Migration(migrations.Migration):
7 |
8 | dependencies = [
9 | ("classroom", "0001_initial"),
10 | ]
11 |
12 | operations = [
13 | migrations.AddField(
14 | model_name="student",
15 | name="username",
16 | field=models.SlugField(blank=True, null=True),
17 | ),
18 | ]
19 |
--------------------------------------------------------------------------------
/classroom/migrations/0003_auto_20200224_1921.py:
--------------------------------------------------------------------------------
1 | # Generated by Django 3.0.2 on 2020-02-24 19:21
2 |
3 | import classroom.models
4 | from django.db import migrations, models
5 |
6 |
7 | class Migration(migrations.Migration):
8 |
9 | dependencies = [
10 | ('classroom', '0002_student_username'),
11 | ]
12 |
13 | operations = [
14 | migrations.AlterField(
15 | model_name='student',
16 | name='average_score',
17 | field=models.FloatField(blank=True, null=True, validators=[classroom.models.validate_negative]),
18 | ),
19 | ]
20 |
--------------------------------------------------------------------------------
/classroom/migrations/__init__.py:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/geoffreynyaga/django-and-djangorestframework-pytest-tutorial/6d1f60ae642b83d475db8718ea3ee45d5177d5eb/classroom/migrations/__init__.py
--------------------------------------------------------------------------------
/classroom/models.py:
--------------------------------------------------------------------------------
1 | from django.db import models
2 | from django.utils.text import slugify
3 |
4 | from django.core.exceptions import ValidationError
5 | from django.utils.translation import gettext_lazy as _
6 |
7 |
8 | def validate_negative(value):
9 | if value < 0:
10 | raise ValidationError(
11 | _("%(value)s is not an posituve number"), params={"value": value},
12 | )
13 |
14 |
15 | class Student(models.Model):
16 | """Model definition for Student."""
17 |
18 | first_name = models.CharField(max_length=50)
19 | last_name = models.CharField(max_length=50)
20 |
21 | username = models.SlugField(blank=True, null=True)
22 |
23 | admission_number = models.IntegerField(unique=True)
24 |
25 | is_qualified = models.BooleanField(default=False)
26 |
27 | average_score = models.FloatField(
28 | blank=True, null=True, validators=[validate_negative]
29 | )
30 |
31 | def __str__(self):
32 | """Unicode representation of Student."""
33 | return self.first_name
34 |
35 | def get_grade(self):
36 | if 0 <= self.average_score < 40:
37 | return "Fail"
38 | elif 40 <= self.average_score < 70:
39 | return "Pass"
40 | elif 70 <= self.average_score < 100:
41 | return "Excellent"
42 | else:
43 | return "Error"
44 |
45 | def save(self, *args, **kwargs):
46 | self.username = slugify(self.first_name)
47 | super(Student, self).save(*args, **kwargs)
48 |
49 |
50 | class Classroom(models.Model):
51 | name = models.CharField(max_length=120)
52 | student_capacity = models.IntegerField()
53 | students = models.ManyToManyField("classroom.Student")
54 |
55 | def __str__(self):
56 | return self.name
57 |
--------------------------------------------------------------------------------
/classroom/tests.py:
--------------------------------------------------------------------------------
1 | # from django.test import TestCase
2 | from hypothesis.extra.django import TestCase
3 | import pytest
4 | from hypothesis import strategies as st, given
5 | from classroom.models import Student, Classroom
6 |
7 | from mixer.backend.django import mixer
8 |
9 | pytestmark = pytest.mark.django_db
10 |
11 |
12 | class TestStudentModel(TestCase):
13 | # def setUp(self):
14 |
15 | # self.student1 = Student.objects.create(
16 | # first_name="Tom", last_name="Mboya", admission_number=12345
17 | # )
18 |
19 | # setting up new users
20 | # getting access tokens / logged in users
21 | # setup up timers
22 |
23 | def test_add_a_plus_b(self):
24 | a = 1
25 | b = 2
26 | c = a + b
27 |
28 | assert c == 3
29 |
30 | def test_student_can_be_created(self):
31 |
32 | student1 = mixer.blend(Student, first_name="Tom")
33 |
34 | student_result = Student.objects.last() # getting the last student
35 |
36 | assert student_result.first_name == "Tom"
37 |
38 | def test_str_return(self):
39 |
40 | student1 = mixer.blend(Student, first_name="Tom")
41 |
42 | student_result = Student.objects.last() # getting the last student
43 |
44 | assert str(student_result) == "Tom"
45 |
46 | # @given(st.characters())
47 | # def test_slugify(self, name):
48 |
49 | # print(name, "name")
50 |
51 | # student1 = mixer.blend(Student, first_name=name)
52 | # student1.save()
53 |
54 | # student_result = Student.objects.last() # getting the last student
55 |
56 | # assert len(str(student_result.username)) == len(name)
57 |
58 | @given(st.floats(min_value=0, max_value=40))
59 | def test_grade_fail(self, fail_score):
60 |
61 | print(fail_score, "this is failscore")
62 |
63 | student1 = mixer.blend(Student, average_score=fail_score)
64 |
65 | student_result = Student.objects.last() # getting the last student
66 |
67 | assert student_result.get_grade() == "Fail"
68 |
69 | @given(st.floats(min_value=40, max_value=70))
70 | def test_grade_pass(self, pass_grade):
71 |
72 | student1 = mixer.blend(Student, average_score=pass_grade)
73 |
74 | student_result = Student.objects.last() # getting the last student
75 |
76 | assert student_result.get_grade() == "Pass"
77 |
78 | @given(st.floats(min_value=70, max_value=100))
79 | def test_grade_excellent(self, excellent_grade):
80 |
81 | student1 = mixer.blend(Student, average_score=excellent_grade)
82 |
83 | student_result = Student.objects.last() # getting the last student
84 |
85 | assert student_result.get_grade() == "Excellent"
86 |
87 | @given(st.floats(min_value=100))
88 | def test_grade_error(self, error_grade):
89 |
90 | student1 = mixer.blend(Student, average_score=error_grade)
91 |
92 | student_result = Student.objects.last() # getting the last student
93 |
94 | assert student_result.get_grade() == "Error"
95 |
96 |
97 | class TestClassroomModel:
98 | def test_classroom_create(self):
99 | classroom = mixer.blend(Classroom, name="Physics")
100 |
101 | classroom_result = Classroom.objects.last() # getting the last student
102 |
103 | assert str(classroom_result) == "Physics"
104 |
--------------------------------------------------------------------------------
/classroom/views.py:
--------------------------------------------------------------------------------
1 | from django.shortcuts import render
2 |
3 | # Create your views here.
4 |
--------------------------------------------------------------------------------
/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 | os.environ.setdefault("DJANGO_SETTINGS_MODULE", "School.settings")
9 | try:
10 | from django.core.management import execute_from_command_line
11 | except ImportError as exc:
12 | raise ImportError(
13 | "Couldn't import Django. Are you sure it's installed and "
14 | "available on your PYTHONPATH environment variable? Did you "
15 | "forget to activate a virtual environment?"
16 | ) from exc
17 | execute_from_command_line(sys.argv)
18 |
19 |
20 | if __name__ == "__main__":
21 | main()
22 |
--------------------------------------------------------------------------------
/pyproject.toml:
--------------------------------------------------------------------------------
1 | # Example configuration for Black.
2 |
3 | # NOTE: you have to use single-quoted strings in TOML for regular expressions.
4 | # It's the equivalent of r-strings in Python. Multiline strings are treated as
5 | # verbose regular expressions by Black. Use [ ] to denote a significant space
6 | # character.
7 |
8 | [tool.black]
9 | line-length = 88
10 | target-version = ['py36', 'py37', 'py38']
11 | include = '\.pyi?$'
12 | exclude = '(/(\.venv|migrations)|.*\/_settings\.py.*|tests/)'
13 |
--------------------------------------------------------------------------------
/pytest Django and DRF.code-workspace:
--------------------------------------------------------------------------------
1 | {
2 | "folders": [
3 | {
4 | "path": "."
5 | }
6 | ],
7 | "settings": {}
8 | }
9 |
--------------------------------------------------------------------------------
/pytest.ini:
--------------------------------------------------------------------------------
1 | [pytest]
2 | DJANGO_SETTINGS_MODULE = School.local_settings
3 |
4 | #optional but recommended
5 | python_files = tests.py test_*.py *_test.py
6 |
7 | addopts = -v --nomigrations --ignore=venv --cov=. --cov-report=html
8 |
--------------------------------------------------------------------------------
/requirements.txt:
--------------------------------------------------------------------------------
1 | apipkg==1.5
2 | appdirs==1.4.3
3 | asgiref==3.2.3
4 | attrs==19.3.0
5 | black==19.10b0
6 | Click==7.0
7 | coverage==5.0.3
8 | Django==3.0.3
9 | djangorestframework==3.11.0
10 | execnet==1.7.1
11 | Faker==0.9.1
12 | hypothesis==5.5.4
13 | importlib-metadata==1.5.0
14 | Markdown==3.2.1
15 | mixer==6.1.3
16 | more-itertools==8.2.0
17 | mypy==0.761
18 | mypy-extensions==0.4.3
19 | packaging==20.1
20 | pathspec==0.7.0
21 | pluggy==0.13.1
22 | psycopg2-binary==2.8.4
23 | py==1.8.1
24 | pyparsing==2.4.6
25 | pytest==5.3.5
26 | pytest-black==0.3.8
27 | pytest-cov==2.8.1
28 | pytest-django==3.8.0
29 | pytest-forked==1.1.3
30 | pytest-sugar==0.9.2
31 | pytest-xdist==1.31.0
32 | python-dateutil==2.8.1
33 | python-decouple==3.3
34 | pytz==2019.3
35 | regex==2020.1.8
36 | six==1.14.0
37 | sortedcontainers==2.1.0
38 | sqlparse==0.3.0
39 | termcolor==1.1.0
40 | text-unidecode==1.2
41 | toml==0.10.0
42 | typed-ast==1.4.1
43 | typing-extensions==3.7.4.1
44 | wcwidth==0.1.8
45 | zipp==2.2.0
46 |
--------------------------------------------------------------------------------