diff --git a/.gitignore b/.gitignore
index 56a04c3..aac87cd 100644
--- a/.gitignore
+++ b/.gitignore
@@ -134,4 +134,6 @@ GitHub.sublime-settings
!.vscode/tasks.json
!.vscode/launch.json
!.vscode/extensions.json
-.history
\ No newline at end of file
+.history
+
+secrets.json
\ No newline at end of file
diff --git a/.idea/.gitignore b/.idea/.gitignore
deleted file mode 100644
index 73f69e0..0000000
--- a/.idea/.gitignore
+++ /dev/null
@@ -1,8 +0,0 @@
-# Default ignored files
-/shelf/
-/workspace.xml
-# Datasource local storage ignored files
-/dataSources/
-/dataSources.local.xml
-# Editor-based HTTP Client requests
-/httpRequests/
diff --git a/.idea/SOITA.iml b/.idea/SOITA.iml
deleted file mode 100644
index 66ca8ca..0000000
--- a/.idea/SOITA.iml
+++ /dev/null
@@ -1,30 +0,0 @@
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
\ No newline at end of file
diff --git a/.idea/encodings.xml b/.idea/encodings.xml
deleted file mode 100644
index 15a15b2..0000000
--- a/.idea/encodings.xml
+++ /dev/null
@@ -1,4 +0,0 @@
-
-
-
-
\ No newline at end of file
diff --git a/.idea/inspectionProfiles/profiles_settings.xml b/.idea/inspectionProfiles/profiles_settings.xml
deleted file mode 100644
index 105ce2d..0000000
--- a/.idea/inspectionProfiles/profiles_settings.xml
+++ /dev/null
@@ -1,6 +0,0 @@
-
-
-
-
-
-
\ No newline at end of file
diff --git a/.idea/misc.xml b/.idea/misc.xml
deleted file mode 100644
index be60604..0000000
--- a/.idea/misc.xml
+++ /dev/null
@@ -1,7 +0,0 @@
-
-
-
-
-
-
-
\ No newline at end of file
diff --git a/.idea/modules.xml b/.idea/modules.xml
deleted file mode 100644
index 19e9fd9..0000000
--- a/.idea/modules.xml
+++ /dev/null
@@ -1,8 +0,0 @@
-
-
-
-
-
-
-
-
\ No newline at end of file
diff --git a/.idea/vcs.xml b/.idea/vcs.xml
deleted file mode 100644
index 94a25f7..0000000
--- a/.idea/vcs.xml
+++ /dev/null
@@ -1,6 +0,0 @@
-
-
-
-
-
-
\ No newline at end of file
diff --git a/SOITA/settings.py b/SOITA/settings.py
deleted file mode 100644
index 07017d9..0000000
--- a/SOITA/settings.py
+++ /dev/null
@@ -1,126 +0,0 @@
-"""
-Django settings for SOITA project.
-
-Generated by 'django-admin startproject' using Django 3.2.9.
-
-For more information on this file, see
-https://docs.djangoproject.com/en/3.2/topics/settings/
-
-For the full list of settings and their values, see
-https://docs.djangoproject.com/en/3.2/ref/settings/
-"""
-
-from pathlib import Path
-
-# Build paths inside the project like this: BASE_DIR / 'subdir'.
-BASE_DIR = Path(__file__).resolve().parent.parent
-
-
-# Quick-start development settings - unsuitable for production
-# See https://docs.djangoproject.com/en/3.2/howto/deployment/checklist/
-
-# SECURITY WARNING: keep the secret key used in production secret!
-SECRET_KEY = 'django-insecure-&bro7o3+h4mu-stibpc_$pen0+b^5t)!thrlj61u9v8_y6ec1)'
-
-# SECURITY WARNING: don't run with debug turned on in production!
-DEBUG = True
-
-ALLOWED_HOSTS = []
-
-
-# Application definition
-
-INSTALLED_APPS = [
- 'django.contrib.admin',
- 'django.contrib.auth',
- 'django.contrib.contenttypes',
- 'django.contrib.sessions',
- 'django.contrib.messages',
- 'django.contrib.staticfiles',
-]
-
-MIDDLEWARE = [
- 'django.middleware.security.SecurityMiddleware',
- 'django.contrib.sessions.middleware.SessionMiddleware',
- 'django.middleware.common.CommonMiddleware',
- 'django.middleware.csrf.CsrfViewMiddleware',
- 'django.contrib.auth.middleware.AuthenticationMiddleware',
- 'django.contrib.messages.middleware.MessageMiddleware',
- 'django.middleware.clickjacking.XFrameOptionsMiddleware',
-]
-
-ROOT_URLCONF = 'SOITA.urls'
-
-TEMPLATES = [
- {
- 'BACKEND': 'django.template.backends.django.DjangoTemplates',
- 'DIRS': [os.path.join(BASE_DIR, 'templates')]
- ,
- 'APP_DIRS': True,
- 'OPTIONS': {
- 'context_processors': [
- 'django.template.context_processors.debug',
- 'django.template.context_processors.request',
- 'django.contrib.auth.context_processors.auth',
- 'django.contrib.messages.context_processors.messages',
- ],
- },
- },
-]
-
-WSGI_APPLICATION = 'SOITA.wsgi.application'
-
-
-# Database
-# https://docs.djangoproject.com/en/3.2/ref/settings/#databases
-
-DATABASES = {
- 'default': {
- 'ENGINE': 'django.db.backends.sqlite3',
- 'NAME': BASE_DIR / 'db.sqlite3',
- }
-}
-
-
-# Password validation
-# https://docs.djangoproject.com/en/3.2/ref/settings/#auth-password-validators
-
-AUTH_PASSWORD_VALIDATORS = [
- {
- 'NAME': 'django.contrib.auth.password_validation.UserAttributeSimilarityValidator',
- },
- {
- 'NAME': 'django.contrib.auth.password_validation.MinimumLengthValidator',
- },
- {
- 'NAME': 'django.contrib.auth.password_validation.CommonPasswordValidator',
- },
- {
- 'NAME': 'django.contrib.auth.password_validation.NumericPasswordValidator',
- },
-]
-
-
-# Internationalization
-# https://docs.djangoproject.com/en/3.2/topics/i18n/
-
-LANGUAGE_CODE = 'en-us'
-
-TIME_ZONE = 'UTC'
-
-USE_I18N = True
-
-USE_L10N = True
-
-USE_TZ = True
-
-
-# Static files (CSS, JavaScript, Images)
-# https://docs.djangoproject.com/en/3.2/howto/static-files/
-
-STATIC_URL = '/static/'
-
-# Default primary key field type
-# https://docs.djangoproject.com/en/3.2/ref/settings/#default-auto-field
-
-DEFAULT_AUTO_FIELD = 'django.db.models.BigAutoField'
diff --git a/SOITA/__init__.py b/answers/__init__.py
similarity index 100%
rename from SOITA/__init__.py
rename to answers/__init__.py
diff --git a/answers/migrations/0001_initial.py b/answers/migrations/0001_initial.py
new file mode 100644
index 0000000..0461f63
--- /dev/null
+++ b/answers/migrations/0001_initial.py
@@ -0,0 +1,29 @@
+# Generated by Django 3.1.1 on 2021-12-01 18:52
+
+from django.db import migrations, models
+import django.db.models.deletion
+
+
+class Migration(migrations.Migration):
+
+ initial = True
+
+ dependencies = [
+ ('questions', '0001_initial'),
+ ]
+
+ operations = [
+ migrations.CreateModel(
+ name='Answer',
+ fields=[
+ ('id', models.AutoField(auto_created=True, primary_key=True, serialize=False, verbose_name='ID')),
+ ('description', models.TextField()),
+ ('is_correct', models.BooleanField(default=False)),
+ ('question', models.ForeignKey(on_delete=django.db.models.deletion.CASCADE, related_name='answers', to='questions.question')),
+ ],
+ ),
+ migrations.AddConstraint(
+ model_name='answer',
+ constraint=models.UniqueConstraint(condition=models.Q(is_correct=True), fields=('question',), name='only_one_correct_answer'),
+ ),
+ ]
diff --git a/README.md b/answers/migrations/__init__.py
similarity index 100%
rename from README.md
rename to answers/migrations/__init__.py
diff --git a/answers/models.py b/answers/models.py
new file mode 100644
index 0000000..e8e1e12
--- /dev/null
+++ b/answers/models.py
@@ -0,0 +1,27 @@
+from django.db import models
+from django.db.models import Q
+from django.db.models import UniqueConstraint
+
+
+class Answer(models.Model):
+ question = models.ForeignKey(
+ "questions.Question",
+ on_delete=models.CASCADE,
+ null=False,
+ related_name="answers"
+ )
+ description = models.TextField()
+ is_correct = models.BooleanField(default=False)
+
+ def get_secret_answer(self):
+ return {
+ "id": self.id,
+ "description": self.description,
+ }
+
+ class Meta:
+ constraints = [UniqueConstraint(
+ fields=["question"],
+ condition=Q(is_correct=True),
+ name="only_one_correct_answer"
+ )]
diff --git a/answers/serializers.py b/answers/serializers.py
new file mode 100644
index 0000000..131ac22
--- /dev/null
+++ b/answers/serializers.py
@@ -0,0 +1,14 @@
+from rest_framework import serializers
+
+from answers.models import Answer
+
+
+class AnswerSerializer(serializers.ModelSerializer):
+ class Meta:
+ model = Answer
+ fields = (
+ "id",
+ "description",
+ "is_correct",
+ "question"
+ )
diff --git a/answers/urls.py b/answers/urls.py
new file mode 100644
index 0000000..e6f4956
--- /dev/null
+++ b/answers/urls.py
@@ -0,0 +1,8 @@
+from rest_framework.routers import DefaultRouter
+
+from answers.views import AnswerModelViewSet
+
+router = DefaultRouter(trailing_slash=False)
+router.register("items", AnswerModelViewSet)
+
+urlpatterns = router.urls
diff --git a/answers/views.py b/answers/views.py
new file mode 100644
index 0000000..c14ee50
--- /dev/null
+++ b/answers/views.py
@@ -0,0 +1,9 @@
+from rest_framework import viewsets
+
+from answers.models import Answer
+from answers.serializers import AnswerSerializer
+
+
+class AnswerModelViewSet(viewsets.ModelViewSet):
+ serializer_class = AnswerSerializer
+ queryset = Answer.objects.all()
diff --git a/config/__init__.py b/config/__init__.py
new file mode 100644
index 0000000..e69de29
diff --git a/SOITA/asgi.py b/config/asgi.py
similarity index 58%
rename from SOITA/asgi.py
rename to config/asgi.py
index 2ace8f4..82ae9c0 100644
--- a/SOITA/asgi.py
+++ b/config/asgi.py
@@ -1,16 +1,16 @@
"""
-ASGI config for SOITA project.
+ASGI config for testMe project.
It exposes the ASGI callable as a module-level variable named ``application``.
For more information on this file, see
-https://docs.djangoproject.com/en/3.2/howto/deployment/asgi/
+https://docs.djangoproject.com/en/3.1/howto/deployment/asgi/
"""
import os
from django.core.asgi import get_asgi_application
-os.environ.setdefault('DJANGO_SETTINGS_MODULE', 'SOITA.settings')
+os.environ.setdefault('DJANGO_SETTINGS_MODULE', 'config.settings')
application = get_asgi_application()
diff --git a/config/settings.py b/config/settings.py
new file mode 100644
index 0000000..b6a07f7
--- /dev/null
+++ b/config/settings.py
@@ -0,0 +1,190 @@
+import json
+import os
+"""
+Django settings for config project.
+
+Generated by 'django-admin startproject' using Django 3.1.1.
+
+For more information on this file, see
+https://docs.djangoproject.com/en/3.1/topics/settings/
+
+For the full list of settings and their values, see
+https://docs.djangoproject.com/en/3.1/ref/settings/
+"""
+
+from pathlib import Path
+
+BASE_DIR = os.path.dirname(os.path.dirname(os.path.abspath(__file__)))
+
+secrets = BASE_DIR + "/secrets.json"
+
+with open(secrets) as f:
+ keys = json.loads(f.read())
+
+
+def get_secret(setting, keys=keys):
+ return keys[setting]
+
+# Quick-start development settings - unsuitable for production
+# See https://docs.djangoproject.com/en/3.1/howto/deployment/checklist/
+
+# SECURITY WARNING: keep the secret key used in production secret!
+SECRET_KEY = get_secret("SECRET_KEY")
+
+# SECURITY WARNING: don't run with debug turned on in production!
+DEBUG = get_secret("DEBUG")
+
+ALLOWED_HOSTS = get_secret("ALLOWED_HOSTS")
+
+
+# Application definition
+
+INSTALLED_APPS = [
+ 'django.contrib.auth',
+ 'django.contrib.contenttypes',
+ 'django.contrib.staticfiles',
+ "django.contrib.gis",
+ "rest_framework",
+ "rest_framework_simplejwt",
+ "django_extensions",
+
+ "users",
+ "trials",
+ "answers",
+ "questions"
+]
+
+MIDDLEWARE = [
+ 'django.middleware.security.SecurityMiddleware',
+ "corsheaders.middleware.CorsMiddleware",
+ # "`debug_toolbar.middleware.DebugToolbarMiddleware`",
+ 'django.middleware.common.CommonMiddleware',
+ 'django.middleware.clickjacking.XFrameOptionsMiddleware',
+]
+
+ROOT_URLCONF = get_secret("ROOT_URLCONF")
+
+TEMPLATES = [
+ {
+ 'BACKEND': 'django.template.backends.django.DjangoTemplates',
+ 'DIRS': os.path.join(BASE_DIR, "templates"),
+ 'APP_DIRS': True,
+ 'OPTIONS': {
+ 'context_processors': [
+ 'django.template.context_processors.debug',
+ 'django.template.context_processors.request',
+ 'django.contrib.auth.context_processors.auth',
+ 'django.contrib.messages.context_processors.messages',
+ ],
+ },
+ },
+]
+
+WSGI_APPLICATION = 'config.wsgi.application'
+
+# User model
+AUTH_USER_MODEL = "users.User"
+
+# Database
+# https://docs.djangoproject.com/en/3.1/ref/settings/#databases
+
+DATABASES = {
+ 'default': {
+ "ENGINE": "django.contrib.gis.db.backends.postgis",
+ "NAME": get_secret("DB_NAME"),
+ "USER": get_secret("DB_USER"),
+ "PASSWORD": get_secret("DB_PASSWORD"),
+ "HOST": get_secret("DB_HOST"),
+ "PORT": get_secret("DB_PORT"),
+ }
+}
+
+REST_FRAMEWORK = {
+ "DEFAULT_PERMISSION_CLASSES": (
+ "rest_framework.permissions.IsAuthenticated",
+ ),
+ "DEFAULT_AUTHENTICATION_CLASSES": (
+ "rest_framework_simplejwt.authentication.JWTAuthentication",
+ ),
+ "DEFAULT_PAGINATION_CLASS": "rest_framework.pagination.LimitOffsetPagination",
+ "DEFAULT_FILTER_BACKENDS": (
+ "django_filters.rest_framework.DjangoFilterBackend",
+ "rest_framework.filters.SearchFilter",
+ "rest_framework.filters.OrderingFilter",
+ ),
+ "NON_FIELD_ERRORS_KEY": "message",
+ 'DEFAULT_THROTTLE_CLASSES': [
+ 'rest_framework.throttling.AnonRateThrottle',
+ 'rest_framework.throttling.UserRateThrottle'
+ ],
+ 'DEFAULT_THROTTLE_RATES': {
+ 'anon': '100/day',
+ 'user': '10000/day'
+ },
+ "DEFAULT_SCHEMA_CLASS": "drf_spectacular.openapi.AutoSchema",
+}
+
+# Password validation
+# https://docs.djangoproject.com/en/3.1/ref/settings/#auth-password-validators
+
+DEVELOPMENT = get_secret("DEVELOPMENT")
+
+if not DEVELOPMENT:
+ REST_FRAMEWORK.update({
+ "DEFAULT_RENDERER_CLASSES": (
+ "djangorestframework_camel_case.render.CamelCaseJSONRenderer",
+ "djangorestframework_camel_case.render.CamelCaseBrowsableAPIRenderer",
+ ),
+ "DEFAULT_PARSER_CLASSES": (
+ "djangorestframework_camel_case.parser.CamelCaseJSONParser",
+ "djangorestframework_camel_case.parser.CamelCaseMultiPartParser",
+ "djangorestframework_camel_case.parser.CamelCaseFormParser",
+ ),
+ "JSON_UNDERSCOREIZE": {
+ "no_underscore_before_number": True,
+ },
+ })
+
+AUTH_PASSWORD_VALIDATORS = [
+ {
+ 'NAME': 'django.contrib.auth.password_validation.UserAttributeSimilarityValidator',
+ },
+ {
+ 'NAME': 'django.contrib.auth.password_validation.MinimumLengthValidator',
+ },
+ {
+ 'NAME': 'django.contrib.auth.password_validation.CommonPasswordValidator',
+ },
+ {
+ 'NAME': 'django.contrib.auth.password_validation.NumericPasswordValidator',
+ },
+]
+
+
+# Internationalization
+# https://docs.djangoproject.com/en/3.1/topics/i18n/
+
+LANGUAGE_CODE = "pl"
+
+TIME_ZONE = "Europe/Warsaw"
+
+USE_I18N = True
+
+USE_L10N = True
+
+USE_TZ = True
+
+
+# Static files (CSS, JavaScript, Images)
+# https://docs.djangoproject.com/en/3.1/howto/static-files/
+
+STATIC_URL = "/static/"
+MEDIA_ROOT = "/media/"
+STATICFILES_DIRS = (os.path.join(BASE_DIR, "static"),)
+STATIC_ROOT = os.path.join(BASE_DIR, "../static")
+CORS_ORIGIN_ALLOW_ALL = True
+
+CORS_ALLOW_HEADERS = ('content-disposition', 'accept-encoding',
+ 'content-type', 'accept', 'origin', 'authorization')
+
+MEDIA_URL = "/media/"
diff --git a/SOITA/urls.py b/config/urls.py
similarity index 67%
rename from SOITA/urls.py
rename to config/urls.py
index 3bf6d9b..a40f86d 100644
--- a/SOITA/urls.py
+++ b/config/urls.py
@@ -1,7 +1,7 @@
-"""SOITA URL Configuration
+"""testMe URL Configuration
The `urlpatterns` list routes URLs to views. For more information please see:
- https://docs.djangoproject.com/en/3.2/topics/http/urls/
+ https://docs.djangoproject.com/en/3.1/topics/http/urls/
Examples:
Function views
1. Add an import: from my_app import views
@@ -14,8 +14,12 @@ Including another URLconf
2. Add a URL to urlpatterns: path('blog/', include('blog.urls'))
"""
from django.contrib import admin
+from django.urls import include
from django.urls import path
urlpatterns = [
- path('admin/', admin.site.urls),
+ path('users/', include("users.urls")),
+ path('questions/', include("questions.urls")),
+ path('answers/', include("answers.urls")),
+ path('tests/', include("trials.urls")),
]
diff --git a/SOITA/wsgi.py b/config/wsgi.py
similarity index 58%
rename from SOITA/wsgi.py
rename to config/wsgi.py
index f0c2d7c..7781ac0 100644
--- a/SOITA/wsgi.py
+++ b/config/wsgi.py
@@ -1,16 +1,16 @@
"""
-WSGI config for SOITA project.
+WSGI config for testMe project.
It exposes the WSGI callable as a module-level variable named ``application``.
For more information on this file, see
-https://docs.djangoproject.com/en/3.2/howto/deployment/wsgi/
+https://docs.djangoproject.com/en/3.1/howto/deployment/wsgi/
"""
import os
from django.core.wsgi import get_wsgi_application
-os.environ.setdefault('DJANGO_SETTINGS_MODULE', 'SOITA.settings')
+os.environ.setdefault('DJANGO_SETTINGS_MODULE', 'config.settings')
application = get_wsgi_application()
diff --git a/manage.py b/manage.py
old mode 100644
new mode 100755
index 74258f0..8e7ac79
--- a/manage.py
+++ b/manage.py
@@ -6,7 +6,7 @@ import sys
def main():
"""Run administrative tasks."""
- os.environ.setdefault('DJANGO_SETTINGS_MODULE', 'SOITA.settings')
+ os.environ.setdefault('DJANGO_SETTINGS_MODULE', 'config.settings')
try:
from django.core.management import execute_from_command_line
except ImportError as exc:
diff --git a/questions/__init__.py b/questions/__init__.py
new file mode 100644
index 0000000..e69de29
diff --git a/questions/apps.py b/questions/apps.py
new file mode 100644
index 0000000..e69de29
diff --git a/questions/migrations/0001_initial.py b/questions/migrations/0001_initial.py
new file mode 100644
index 0000000..710c384
--- /dev/null
+++ b/questions/migrations/0001_initial.py
@@ -0,0 +1,25 @@
+# Generated by Django 3.1.1 on 2021-12-01 18:52
+
+from django.db import migrations, models
+import django.db.models.deletion
+
+
+class Migration(migrations.Migration):
+
+ initial = True
+
+ dependencies = [
+ ('trials', '0001_initial'),
+ ]
+
+ operations = [
+ migrations.CreateModel(
+ name='Question',
+ fields=[
+ ('id', models.AutoField(auto_created=True, primary_key=True, serialize=False, verbose_name='ID')),
+ ('name', models.CharField(default='', max_length=200)),
+ ('description', models.TextField()),
+ ('test', models.ForeignKey(null=True, on_delete=django.db.models.deletion.SET_NULL, related_name='questions', to='trials.test')),
+ ],
+ ),
+ ]
diff --git a/questions/migrations/0002_question_points.py b/questions/migrations/0002_question_points.py
new file mode 100644
index 0000000..350d1cb
--- /dev/null
+++ b/questions/migrations/0002_question_points.py
@@ -0,0 +1,18 @@
+# Generated by Django 3.2.9 on 2021-12-05 13:46
+
+from django.db import migrations, models
+
+
+class Migration(migrations.Migration):
+
+ dependencies = [
+ ('questions', '0001_initial'),
+ ]
+
+ operations = [
+ migrations.AddField(
+ model_name='question',
+ name='points',
+ field=models.PositiveSmallIntegerField(default=1),
+ ),
+ ]
diff --git a/questions/migrations/__init__.py b/questions/migrations/__init__.py
new file mode 100644
index 0000000..e69de29
diff --git a/questions/models.py b/questions/models.py
new file mode 100644
index 0000000..c839628
--- /dev/null
+++ b/questions/models.py
@@ -0,0 +1,19 @@
+from django.db import models
+
+
+class Question(models.Model):
+ test = models.ForeignKey(
+ "trials.Test",
+ on_delete=models.SET_NULL,
+ null=True,
+ related_name="questions"
+ )
+ name = models.CharField(max_length=200, default="")
+ description = models.TextField()
+ points = models.PositiveSmallIntegerField(default=1)
+
+ def get_answers_secret(self):
+ return [
+ answer.get_secret_answer()
+ for answer in self.answers.all()
+ ]
diff --git a/questions/serializers.py b/questions/serializers.py
new file mode 100644
index 0000000..fa42a9a
--- /dev/null
+++ b/questions/serializers.py
@@ -0,0 +1,21 @@
+from rest_framework import serializers
+
+from questions.models import Question
+
+
+class QuestionSerializer(serializers.ModelSerializer):
+
+ answers = serializers.SerializerMethodField()
+
+ def get_answers(self, instance: Question):
+ return instance.get_answers_secret()
+
+ class Meta:
+ model = Question
+ fields = (
+ "id",
+ "name",
+ "description",
+ "answers",
+ "test"
+ )
diff --git a/questions/urls.py b/questions/urls.py
new file mode 100644
index 0000000..7e61cf7
--- /dev/null
+++ b/questions/urls.py
@@ -0,0 +1,8 @@
+from rest_framework.routers import DefaultRouter
+
+from questions.views import QuestionModelViewSet
+
+router = DefaultRouter()
+router.register("items", QuestionModelViewSet)
+
+urlpatterns = router.urls
diff --git a/questions/views.py b/questions/views.py
new file mode 100644
index 0000000..33c4537
--- /dev/null
+++ b/questions/views.py
@@ -0,0 +1,9 @@
+from rest_framework import viewsets
+
+from questions.models import Question
+from questions.serializers import QuestionSerializer
+
+
+class QuestionModelViewSet(viewsets.ModelViewSet):
+ queryset = Question.objects.all()
+ serializer_class = QuestionSerializer
diff --git a/requirements.txt b/requirements.txt
new file mode 100644
index 0000000..12c912c
--- /dev/null
+++ b/requirements.txt
@@ -0,0 +1,110 @@
+alabaster==0.7.12
+appdirs==1.4.4
+appnope==0.1.2
+argon2-cffi==21.1.0
+asgiref==3.4.1
+attrs==21.2.0
+Babel==2.9.1
+backcall==0.2.0
+bleach==4.1.0
+botocore==1.21.15
+certifi==2021.10.8
+cffi==1.15.0
+charset-normalizer==2.0.9
+debugpy==1.5.1
+decorator==5.1.0
+defusedxml==0.7.1
+distlib==0.3.2
+Django==3.2.9
+django-cors-headers==3.10.0
+django-debug-toolbar==3.2.2
+django-extensions==3.1.5
+django-filter==21.1
+django-shell-plus==1.1.7
+djangorestframework==3.12.4
+djangorestframework-simplejwt==5.0.0
+docutils==0.17.1
+drf-spectacular==0.21.0
+entrypoints==0.3
+filelock==3.0.12
+GDAL==3.3.3
+git-remote-codecommit==1.15.1
+idna==3.3
+imagesize==1.3.0
+importlib-metadata==4.8.2
+inflection==0.5.1
+ipykernel==6.6.0
+ipyparallel==8.0.0
+ipython==7.30.1
+ipython-genutils==0.2.0
+ipywidgets==7.6.5
+jedi==0.18.1
+Jinja2==3.0.3
+jmespath==0.10.0
+jsonschema==4.2.1
+jupyter-client==7.1.0
+jupyter-core==4.9.1
+jupyterlab-pygments==0.1.2
+jupyterlab-widgets==1.0.2
+Markdown==3.3.6
+MarkupSafe==2.0.1
+matplotlib-inline==0.1.3
+mistune==0.8.4
+nbclient==0.5.9
+nbconvert==6.3.0
+nbformat==5.1.3
+nest-asyncio==1.5.4
+nose==1.3.7
+notebook==6.4.6
+numpy==1.21.4
+packaging==21.3
+pandocfilters==1.5.0
+parso==0.8.3
+pbr==5.6.0
+pexpect==4.8.0
+pickleshare==0.7.5
+Pillow==8.4.0
+prometheus-client==0.12.0
+prompt-toolkit==3.0.23
+protobuf==3.17.3
+psutil==5.8.0
+psycopg2-binary==2.9.2
+ptyprocess==0.7.0
+pycparser==2.21
+Pygments==2.10.0
+PyJWT==2.3.0
+pyparsing==3.0.6
+pyrsistent==0.18.0
+python-dateutil==2.8.2
+pytz==2021.3
+PyYAML==6.0
+pyzmq==22.3.0
+qtconsole==5.2.1
+QtPy==1.11.3
+requests==2.26.0
+Send2Trash==1.8.0
+six==1.16.0
+snowballstemmer==2.2.0
+Sphinx==4.3.1
+sphinxcontrib-applehelp==1.0.2
+sphinxcontrib-devhelp==1.0.2
+sphinxcontrib-htmlhelp==2.0.0
+sphinxcontrib-jsmath==1.0.1
+sphinxcontrib-qthelp==1.0.3
+sphinxcontrib-serializinghtml==1.1.5
+sqlparse==0.4.2
+stevedore==3.3.0
+terminado==0.12.1
+testpath==0.5.0
+tornado==6.1
+tqdm==4.62.3
+traitlets==5.1.1
+uritemplate==4.1.1
+urllib3==1.26.6
+virtualenv==20.4.7
+virtualenv-clone==0.5.4
+virtualenvwrapper==4.8.4
+wcwidth==0.2.5
+webencodings==0.5.1
+widgetsnbextension==3.5.2
+zipp==3.6.0
diff --git a/templates/generic_test.html b/templates/generic_test.html
new file mode 100644
index 0000000..0a815ae
--- /dev/null
+++ b/templates/generic_test.html
@@ -0,0 +1,48 @@
+
+
+
+
+ {{ test.name }}
+
+
+
+
+
+
+
+{% for question in test.questions.all %}
+
+
+ {{ question.description }}
+
+
+
+ {% for answer in question.answers.all %}
+
+ {% endfor %}
+
+
+{% endfor %}
+
+
+
+
+
diff --git a/trials/__init__.py b/trials/__init__.py
new file mode 100644
index 0000000..e69de29
diff --git a/trials/apps.py b/trials/apps.py
new file mode 100644
index 0000000..e69de29
diff --git a/trials/migrations/0001_initial.py b/trials/migrations/0001_initial.py
new file mode 100644
index 0000000..c7c1c39
--- /dev/null
+++ b/trials/migrations/0001_initial.py
@@ -0,0 +1,21 @@
+# Generated by Django 3.1.1 on 2021-12-01 18:52
+
+from django.db import migrations, models
+
+
+class Migration(migrations.Migration):
+
+ initial = True
+
+ dependencies = [
+ ]
+
+ operations = [
+ migrations.CreateModel(
+ name='Test',
+ fields=[
+ ('id', models.AutoField(auto_created=True, primary_key=True, serialize=False, verbose_name='ID')),
+ ('name', models.CharField(max_length=100)),
+ ],
+ ),
+ ]
diff --git a/trials/migrations/0002_test_passing_score.py b/trials/migrations/0002_test_passing_score.py
new file mode 100644
index 0000000..ae4fdf4
--- /dev/null
+++ b/trials/migrations/0002_test_passing_score.py
@@ -0,0 +1,18 @@
+# Generated by Django 3.2.9 on 2021-12-05 13:46
+
+from django.db import migrations, models
+
+
+class Migration(migrations.Migration):
+
+ dependencies = [
+ ('trials', '0001_initial'),
+ ]
+
+ operations = [
+ migrations.AddField(
+ model_name='test',
+ name='passing_score',
+ field=models.PositiveSmallIntegerField(default=0),
+ ),
+ ]
diff --git a/trials/migrations/__init__.py b/trials/migrations/__init__.py
new file mode 100644
index 0000000..e69de29
diff --git a/trials/models.py b/trials/models.py
new file mode 100644
index 0000000..014c00c
--- /dev/null
+++ b/trials/models.py
@@ -0,0 +1,26 @@
+from django.db import models
+
+
+class Test(models.Model):
+ name = models.CharField(max_length=100)
+ passing_score = models.PositiveSmallIntegerField(default=0)
+
+ def get_score(self, answers):
+ """
+ [
+ {
+ "question": 1,
+ "answer": 1
+ },
+ {
+ "question": 2,
+ "answer": 1
+ }
+ ]
+ """
+ points = 0
+ for answer in answers:
+ question = self.questions.get(id=answer["question"])
+ if question.answers.get(id=answer["answer"]).is_correct:
+ points += question.points
+ return points
diff --git a/trials/serializers.py b/trials/serializers.py
new file mode 100644
index 0000000..52fce74
--- /dev/null
+++ b/trials/serializers.py
@@ -0,0 +1,18 @@
+from rest_framework import serializers
+
+from questions.serializers import QuestionSerializer
+from trials.models import Test
+
+
+class TestSerializer(serializers.ModelSerializer):
+
+ questions = QuestionSerializer(many=True, required=False)
+
+ class Meta:
+ model = Test
+ fields = (
+ "id",
+ "name",
+ "passing_score",
+ "questions"
+ )
diff --git a/trials/urls.py b/trials/urls.py
new file mode 100644
index 0000000..3ebf009
--- /dev/null
+++ b/trials/urls.py
@@ -0,0 +1,16 @@
+from django.urls import path
+from rest_framework.routers import DefaultRouter
+
+from trials.views import TestModelViewSet
+from trials.views import TestTemplateView
+from trials.views import TestValidateAPIView
+
+router = DefaultRouter()
+router.register("items", TestModelViewSet)
+
+urlpatterns = [
+ path('/show/', TestTemplateView.as_view()),
+ path('/mark/', TestValidateAPIView.as_view())
+]
+
+urlpatterns += router.urls
diff --git a/trials/views.py b/trials/views.py
new file mode 100644
index 0000000..12dcd13
--- /dev/null
+++ b/trials/views.py
@@ -0,0 +1,50 @@
+from django.views.generic import TemplateView
+from rest_framework import views
+from rest_framework import viewsets
+from rest_framework.response import Response
+
+from config.settings import BASE_DIR
+from trials.models import Test
+from trials.serializers import TestSerializer
+
+
+class TestModelViewSet(viewsets.ModelViewSet):
+ queryset = Test.objects.all()
+ serializer_class = TestSerializer
+
+
+class TestTemplateView(TemplateView):
+
+ permission_classes = []
+ template_name = BASE_DIR + f"/templates/generic_test.html"
+
+ def get_queryset(self):
+ return Test.objects.all()
+
+ def get_context_data(self, test_id, **kwargs):
+ context = super().get_context_data(**kwargs)
+ context["test"] = self.get_queryset().filter(id=test_id).prefetch_related("questions__answers").first()
+ return context
+
+
+class TestValidateAPIView(views.APIView):
+ PASSED = "passed"
+ FAILED = "failed"
+ UNKNOWN = "unknown"
+
+ PASSED = {
+ True: PASSED,
+ False: FAILED
+ }
+
+ def get_score(self, test: Test, answers):
+ return test.get_score(answers)
+
+ def post(self, request, test_id, **kwargs):
+ test = Test.objects.get(id=test_id)
+ score = self.get_score(test, request.data["answers"])
+ status = score >= test.passing_score
+ return Response({
+ "status": self.PASSED.get(status, self.UNKNOWN),
+ "points": score
+ })
diff --git a/users/.DS_Store b/users/.DS_Store
new file mode 100644
index 0000000..d94f02d
Binary files /dev/null and b/users/.DS_Store differ
diff --git a/users/__init__.py b/users/__init__.py
new file mode 100644
index 0000000..e69de29
diff --git a/users/managers.py b/users/managers.py
new file mode 100644
index 0000000..d4dece3
--- /dev/null
+++ b/users/managers.py
@@ -0,0 +1,21 @@
+from django.contrib.auth.base_user import BaseUserManager
+
+from .querysets import UserQuerySet
+
+
+class UserManager(BaseUserManager):
+
+ def get_queryset(self):
+ return UserQuerySet(self.model, using=self._db)
+
+ def create(self, email, password=None, **kwargs):
+
+ if password is None:
+ message = "User must have valid password"
+ raise ValueError(message)
+
+ user = self.model(email=email, **kwargs)
+ user.set_password(password)
+ user.save()
+
+ return user
diff --git a/users/migrations/.DS_Store b/users/migrations/.DS_Store
new file mode 100644
index 0000000..4f9740e
Binary files /dev/null and b/users/migrations/.DS_Store differ
diff --git a/users/migrations/0001_initial.py b/users/migrations/0001_initial.py
new file mode 100644
index 0000000..c77fb1e
--- /dev/null
+++ b/users/migrations/0001_initial.py
@@ -0,0 +1,32 @@
+# Generated by Django 3.1.1 on 2021-12-01 18:52
+
+from django.db import migrations, models
+
+
+class Migration(migrations.Migration):
+
+ initial = True
+
+ dependencies = [
+ ]
+
+ operations = [
+ migrations.CreateModel(
+ name='User',
+ fields=[
+ ('id', models.AutoField(auto_created=True, primary_key=True, serialize=False, verbose_name='ID')),
+ ('password', models.CharField(max_length=128, verbose_name='password')),
+ ('last_login', models.DateTimeField(blank=True, null=True, verbose_name='last login')),
+ ('first_name', models.CharField(max_length=100)),
+ ('last_name', models.CharField(max_length=100)),
+ ('email', models.EmailField(db_index=True, max_length=50, unique=True)),
+ ('is_active', models.BooleanField(default=False)),
+ ('confirmation_number', models.CharField(max_length=100)),
+ ('reset_code', models.CharField(max_length=100)),
+ ('avatar', models.ImageField(null=True, upload_to='avatars/')),
+ ],
+ options={
+ 'ordering': ('id',),
+ },
+ ),
+ ]
diff --git a/users/migrations/__init__.py b/users/migrations/__init__.py
new file mode 100644
index 0000000..e69de29
diff --git a/users/models.py b/users/models.py
new file mode 100644
index 0000000..886360f
--- /dev/null
+++ b/users/models.py
@@ -0,0 +1,30 @@
+from django.contrib.auth.base_user import AbstractBaseUser
+from django.db import models
+
+from .managers import UserManager
+
+
+class User(AbstractBaseUser):
+ first_name = models.CharField(max_length=100)
+ last_name = models.CharField(max_length=100)
+
+ email = models.EmailField(db_index=True, unique=True, max_length=50)
+ is_active = models.BooleanField(default=False)
+
+ confirmation_number = models.CharField(max_length=100)
+ reset_code = models.CharField(max_length=100)
+ avatar = models.ImageField(upload_to="avatars/", null=True)
+
+ USERNAME_FIELD = "email"
+
+ objects = UserManager()
+
+ class Meta:
+ ordering = ("id", )
+
+ def get_details(self):
+ return {
+ "first_name": self.first_name,
+ "last_name": self.last_name,
+ "email": self.email
+ }
diff --git a/users/querysets.py b/users/querysets.py
new file mode 100644
index 0000000..d4323d9
--- /dev/null
+++ b/users/querysets.py
@@ -0,0 +1,7 @@
+from django.db.models import QuerySet
+
+
+class UserQuerySet(QuerySet):
+
+ def activated(self, **kwargs):
+ return self.filter(is_activated=True)
\ No newline at end of file
diff --git a/users/serializers.py b/users/serializers.py
new file mode 100644
index 0000000..2209870
--- /dev/null
+++ b/users/serializers.py
@@ -0,0 +1,26 @@
+from rest_framework import serializers
+
+from users.models import User
+
+
+class UserSerializer(serializers.ModelSerializer):
+
+ password = serializers.CharField(
+ write_only=True,
+ required=False,
+ min_length=8,
+ style={"input_type": "password"},
+ )
+ #todo
+ # avatar = serializers.ImageField(allow_empty_file=True, source="profile.avatar", read_only=True)
+
+ class Meta:
+ model = User
+ fields = (
+ "id",
+ "email",
+ "first_name",
+ "last_name",
+ "is_active",
+ "password"
+ )
diff --git a/users/urls.py b/users/urls.py
new file mode 100644
index 0000000..cf7ca52
--- /dev/null
+++ b/users/urls.py
@@ -0,0 +1,17 @@
+from rest_framework.routers import DefaultRouter
+from django.urls import include
+from django.urls import path
+from users.views import UserModelViewSet
+from rest_framework_simplejwt.views import TokenObtainPairView
+from rest_framework_simplejwt.views import TokenRefreshView
+
+
+router = DefaultRouter(trailing_slash=False)
+router.register("items", UserModelViewSet)
+
+urlpatterns = [
+ path("", include(router.urls)),
+ path('api/token', TokenObtainPairView.as_view(), name='token_obtain_pair'),
+ path('api/token/refresh', TokenRefreshView.as_view(), name='token_refresh'),
+]
+
diff --git a/users/views.py b/users/views.py
new file mode 100644
index 0000000..a441865
--- /dev/null
+++ b/users/views.py
@@ -0,0 +1,9 @@
+from rest_framework import viewsets
+
+from users.models import User
+from users.serializers import UserSerializer
+
+
+class UserModelViewSet(viewsets.ModelViewSet):
+ queryset = User.objects.all()
+ serializer_class = UserSerializer