Pamela Fox
commited on
Commit
·
4dec878
1
Parent(s):
e6f8796
Porting to pytest-django
Browse files- .devcontainer/devcontainer.json +6 -2
- .devcontainer/docker-compose.yml +1 -1
- .github/workflows/check.yaml +6 -2
- .gitignore +2 -0
- .vscode/settings.json +11 -0
- .vscode/tasks.json +10 -1
- infra/main.bicep +5 -3
- pyproject.toml +6 -0
- requirements-dev.txt +2 -0
- src/manage.py +1 -11
- src/quizsite/production.py +0 -25
- src/quizsite/settings.py +43 -15
- src/quizsite/wsgi.py +1 -2
- src/quizzes/models.py +13 -8
- src/quizzes/templates/quizzes/partial.html +8 -0
- src/quizzes/tests.py +2 -2
- src/quizzes/views.py +3 -1
- src/requirements.txt +1 -0
.devcontainer/devcontainer.json
CHANGED
|
@@ -30,15 +30,19 @@
|
|
| 30 |
],
|
| 31 |
"python.defaultInterpreterPath": "/usr/local/bin/python",
|
| 32 |
"python.linting.enabled": true,
|
| 33 |
-
"python.testing.unittestEnabled": false,
|
| 34 |
"python.testing.pytestEnabled": true,
|
|
|
|
| 35 |
"[python]": {
|
| 36 |
"editor.formatOnSave": true,
|
| 37 |
"editor.codeActionsOnSave": {
|
| 38 |
"source.fixAll": true
|
| 39 |
}
|
| 40 |
},
|
| 41 |
-
"python.formatting.provider": "black"
|
|
|
|
|
|
|
|
|
|
|
|
|
| 42 |
},
|
| 43 |
|
| 44 |
// Add the IDs of extensions you want installed when the container is created.
|
|
|
|
| 30 |
],
|
| 31 |
"python.defaultInterpreterPath": "/usr/local/bin/python",
|
| 32 |
"python.linting.enabled": true,
|
|
|
|
| 33 |
"python.testing.pytestEnabled": true,
|
| 34 |
+
"python.testing.unittestEnabled": false,
|
| 35 |
"[python]": {
|
| 36 |
"editor.formatOnSave": true,
|
| 37 |
"editor.codeActionsOnSave": {
|
| 38 |
"source.fixAll": true
|
| 39 |
}
|
| 40 |
},
|
| 41 |
+
"python.formatting.provider": "black",
|
| 42 |
+
"files.exclude": {
|
| 43 |
+
"**/*.coverage": true,
|
| 44 |
+
".ruff_cache": true
|
| 45 |
+
}
|
| 46 |
},
|
| 47 |
|
| 48 |
// Add the IDs of extensions you want installed when the container is created.
|
.devcontainer/docker-compose.yml
CHANGED
|
@@ -7,7 +7,7 @@ services:
|
|
| 7 |
dockerfile: .devcontainer/Dockerfile
|
| 8 |
args:
|
| 9 |
# [Choice] Python version: 3, 3.8, 3.7, 3.6
|
| 10 |
-
IMAGE: python:3.
|
| 11 |
# On Linux, you may need to update USER_UID and USER_GID below if not your local UID is not 1000.
|
| 12 |
USER_UID: 1000
|
| 13 |
USER_GID: 1000
|
|
|
|
| 7 |
dockerfile: .devcontainer/Dockerfile
|
| 8 |
args:
|
| 9 |
# [Choice] Python version: 3, 3.8, 3.7, 3.6
|
| 10 |
+
IMAGE: python:3.11
|
| 11 |
# On Linux, you may need to update USER_UID and USER_GID below if not your local UID is not 1000.
|
| 12 |
USER_UID: 1000
|
| 13 |
USER_GID: 1000
|
.github/workflows/check.yaml
CHANGED
|
@@ -40,5 +40,9 @@ jobs:
|
|
| 40 |
SECRET_KEY: django-insecure-key-${{ github.run_id }}-${{ github.run_attempt }}
|
| 41 |
run: |
|
| 42 |
python src/manage.py collectstatic
|
| 43 |
-
|
| 44 |
-
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 40 |
SECRET_KEY: django-insecure-key-${{ github.run_id }}-${{ github.run_attempt }}
|
| 41 |
run: |
|
| 42 |
python src/manage.py collectstatic
|
| 43 |
+
python3 -m pytest | tee pytest-coverage.txt
|
| 44 |
+
- name: Pytest coverage comment
|
| 45 |
+
uses: MishaKav/pytest-coverage-comment@main
|
| 46 |
+
with:
|
| 47 |
+
pytest-coverage-path: ./pytest-coverage.txt
|
| 48 |
+
junitxml-path: ./pytest.xml
|
.gitignore
CHANGED
|
@@ -4,3 +4,5 @@ staticfiles/
|
|
| 4 |
.coverage
|
| 5 |
.env
|
| 6 |
.azure
|
|
|
|
|
|
|
|
|
| 4 |
.coverage
|
| 5 |
.env
|
| 6 |
.azure
|
| 7 |
+
pytest.xml
|
| 8 |
+
pytest-coverage.txt
|
.vscode/settings.json
ADDED
|
@@ -0,0 +1,11 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
{
|
| 2 |
+
"python.testing.unittestArgs": [
|
| 3 |
+
"-v",
|
| 4 |
+
"-s",
|
| 5 |
+
"./src",
|
| 6 |
+
"-p",
|
| 7 |
+
"test*.py"
|
| 8 |
+
],
|
| 9 |
+
"python.testing.pytestEnabled": false,
|
| 10 |
+
"python.testing.unittestEnabled": true
|
| 11 |
+
}
|
.vscode/tasks.json
CHANGED
|
@@ -37,7 +37,7 @@
|
|
| 37 |
"panel": "dedicated"
|
| 38 |
}
|
| 39 |
}, {
|
| 40 |
-
"label": "
|
| 41 |
"type": "shell",
|
| 42 |
"command": "./src/manage.py migrate",
|
| 43 |
"problemMatcher": [],
|
|
@@ -54,5 +54,14 @@
|
|
| 54 |
"reveal": "always",
|
| 55 |
"panel": "dedicated"
|
| 56 |
}
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 57 |
}]
|
| 58 |
}
|
|
|
|
| 37 |
"panel": "dedicated"
|
| 38 |
}
|
| 39 |
}, {
|
| 40 |
+
"label": "Apply migrations",
|
| 41 |
"type": "shell",
|
| 42 |
"command": "./src/manage.py migrate",
|
| 43 |
"problemMatcher": [],
|
|
|
|
| 54 |
"reveal": "always",
|
| 55 |
"panel": "dedicated"
|
| 56 |
}
|
| 57 |
+
}, {
|
| 58 |
+
"label": "Flush database",
|
| 59 |
+
"type": "shell",
|
| 60 |
+
"command": "./src/manage.py flush --noinput",
|
| 61 |
+
"problemMatcher": [],
|
| 62 |
+
"presentation": {
|
| 63 |
+
"reveal": "always",
|
| 64 |
+
"panel": "dedicated"
|
| 65 |
+
}
|
| 66 |
}]
|
| 67 |
}
|
infra/main.bicep
CHANGED
|
@@ -66,17 +66,19 @@ module web 'core/host/appservice.bicep' = {
|
|
| 66 |
tags: union(tags, { 'azd-service-name': 'web' })
|
| 67 |
appServicePlanId: appServicePlan.outputs.id
|
| 68 |
runtimeName: 'python'
|
| 69 |
-
runtimeVersion: '3.
|
| 70 |
scmDoBuildDuringDeployment: true
|
| 71 |
ftpsState: 'Disabled'
|
| 72 |
managedIdentity: true
|
| 73 |
appCommandLine: 'python manage.py migrate && gunicorn --workers 2 --threads 4 --timeout 60 --access-logfile \'-\' --error-logfile \'-\' --bind=0.0.0.0:8000 --chdir=/home/site/wwwroot quizsite.wsgi'
|
| 74 |
appSettings: {
|
| 75 |
ADMIN_URL: 'admin${uniqueString(appServicePlan.outputs.id)}'
|
| 76 |
-
|
|
|
|
| 77 |
DBNAME: postgresDatabaseName
|
| 78 |
DBUSER: '@Microsoft.KeyVault(VaultName=${keyVault.outputs.name};SecretName=postgresAdminUser)'
|
| 79 |
DBPASS: '@Microsoft.KeyVault(VaultName=${keyVault.outputs.name};SecretName=postgresAdminPassword)'
|
|
|
|
| 80 |
SECRET_KEY: '@Microsoft.KeyVault(VaultName=${keyVault.outputs.name};SecretName=djangoSecretKey)'
|
| 81 |
}
|
| 82 |
}
|
|
@@ -155,4 +157,4 @@ module logAnalyticsWorkspace 'core/monitor/loganalytics.bicep' = {
|
|
| 155 |
|
| 156 |
output WEB_URI string = 'https://${web.outputs.uri}'
|
| 157 |
output AZURE_LOCATION string = location
|
| 158 |
-
output AZURE_KEY_VAULT_NAME string = keyVault.outputs.name
|
|
|
|
| 66 |
tags: union(tags, { 'azd-service-name': 'web' })
|
| 67 |
appServicePlanId: appServicePlan.outputs.id
|
| 68 |
runtimeName: 'python'
|
| 69 |
+
runtimeVersion: '3.11'
|
| 70 |
scmDoBuildDuringDeployment: true
|
| 71 |
ftpsState: 'Disabled'
|
| 72 |
managedIdentity: true
|
| 73 |
appCommandLine: 'python manage.py migrate && gunicorn --workers 2 --threads 4 --timeout 60 --access-logfile \'-\' --error-logfile \'-\' --bind=0.0.0.0:8000 --chdir=/home/site/wwwroot quizsite.wsgi'
|
| 74 |
appSettings: {
|
| 75 |
ADMIN_URL: 'admin${uniqueString(appServicePlan.outputs.id)}'
|
| 76 |
+
DBENGINE: 'django.db.backends.postgresql'
|
| 77 |
+
DBHOST: '${postgresServerName}.postgres.database.azure.com'
|
| 78 |
DBNAME: postgresDatabaseName
|
| 79 |
DBUSER: '@Microsoft.KeyVault(VaultName=${keyVault.outputs.name};SecretName=postgresAdminUser)'
|
| 80 |
DBPASS: '@Microsoft.KeyVault(VaultName=${keyVault.outputs.name};SecretName=postgresAdminPassword)'
|
| 81 |
+
DBSSL: 'require'
|
| 82 |
SECRET_KEY: '@Microsoft.KeyVault(VaultName=${keyVault.outputs.name};SecretName=djangoSecretKey)'
|
| 83 |
}
|
| 84 |
}
|
|
|
|
| 157 |
|
| 158 |
output WEB_URI string = 'https://${web.outputs.uri}'
|
| 159 |
output AZURE_LOCATION string = location
|
| 160 |
+
output AZURE_KEY_VAULT_NAME string = keyVault.outputs.name
|
pyproject.toml
CHANGED
|
@@ -12,3 +12,9 @@ exclude = '''
|
|
| 12 |
| migrations
|
| 13 |
)/
|
| 14 |
'''
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 12 |
| migrations
|
| 13 |
)/
|
| 14 |
'''
|
| 15 |
+
|
| 16 |
+
[tool.pytest.ini_options]
|
| 17 |
+
addopts = "-ra --cov=src --cov-report=term-missing:skip-covered --junitxml=pytest.xml"
|
| 18 |
+
pythonpath = ["src"]
|
| 19 |
+
python_files = ["tests.py"]
|
| 20 |
+
DJANGO_SETTINGS_MODULE = "quizsite.settings"
|
requirements-dev.txt
CHANGED
|
@@ -3,3 +3,5 @@ black
|
|
| 3 |
pre-commit
|
| 4 |
ruff
|
| 5 |
coverage
|
|
|
|
|
|
|
|
|
| 3 |
pre-commit
|
| 4 |
ruff
|
| 5 |
coverage
|
| 6 |
+
pytest-django
|
| 7 |
+
pytest-cov
|
src/manage.py
CHANGED
|
@@ -3,21 +3,11 @@
|
|
| 3 |
import os
|
| 4 |
import sys
|
| 5 |
|
| 6 |
-
from dotenv import load_dotenv
|
| 7 |
-
|
| 8 |
|
| 9 |
def main():
|
| 10 |
"""Run administrative tasks."""
|
| 11 |
-
# If WEBSITE_HOSTNAME is defined as an environment variable, then we're running on Azure App Service
|
| 12 |
-
|
| 13 |
-
# Only for Local Development - Load environment variables from the .env file
|
| 14 |
-
if "WEBSITE_HOSTNAME" not in os.environ:
|
| 15 |
-
print("Loading environment variables for .env file")
|
| 16 |
-
load_dotenv("./.env")
|
| 17 |
|
| 18 |
-
|
| 19 |
-
settings_module = "quizsite.production" if "WEBSITE_HOSTNAME" in os.environ else "quizsite.settings"
|
| 20 |
-
os.environ.setdefault("DJANGO_SETTINGS_MODULE", settings_module)
|
| 21 |
|
| 22 |
try:
|
| 23 |
from django.core.management import execute_from_command_line
|
|
|
|
| 3 |
import os
|
| 4 |
import sys
|
| 5 |
|
|
|
|
|
|
|
| 6 |
|
| 7 |
def main():
|
| 8 |
"""Run administrative tasks."""
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 9 |
|
| 10 |
+
os.environ.setdefault("DJANGO_SETTINGS_MODULE", "quizsite.settings")
|
|
|
|
|
|
|
| 11 |
|
| 12 |
try:
|
| 13 |
from django.core.management import execute_from_command_line
|
src/quizsite/production.py
DELETED
|
@@ -1,25 +0,0 @@
|
|
| 1 |
-
from .settings import * # noqa
|
| 2 |
-
import os
|
| 3 |
-
|
| 4 |
-
# Configure the domain name using the environment variable
|
| 5 |
-
# that Azure automatically creates for us.
|
| 6 |
-
ALLOWED_HOSTS = [os.environ["WEBSITE_HOSTNAME"]] if "WEBSITE_HOSTNAME" in os.environ else []
|
| 7 |
-
CSRF_TRUSTED_ORIGINS = ["https://" + os.environ["WEBSITE_HOSTNAME"]] if "WEBSITE_HOSTNAME" in os.environ else []
|
| 8 |
-
DEBUG = False
|
| 9 |
-
ADMIN_URL = os.environ["ADMIN_URL"]
|
| 10 |
-
|
| 11 |
-
# DBHOST is only the server name, not the full URL
|
| 12 |
-
hostname = os.environ["DBHOST"]
|
| 13 |
-
|
| 14 |
-
# Configure Postgres database; the full username for PostgreSQL flexible server is
|
| 15 |
-
# username (not @sever-name).
|
| 16 |
-
DATABASES = {
|
| 17 |
-
"default": {
|
| 18 |
-
"ENGINE": "django.db.backends.postgresql",
|
| 19 |
-
"NAME": os.environ["DBNAME"],
|
| 20 |
-
"HOST": hostname + ".postgres.database.azure.com",
|
| 21 |
-
"USER": os.environ["DBUSER"],
|
| 22 |
-
"PASSWORD": os.environ["DBPASS"],
|
| 23 |
-
"OPTIONS": {"sslmode": "require"},
|
| 24 |
-
}
|
| 25 |
-
}
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
src/quizsite/settings.py
CHANGED
|
@@ -13,25 +13,47 @@ https://docs.djangoproject.com/en/4.1/ref/settings/
|
|
| 13 |
import os
|
| 14 |
from pathlib import Path
|
| 15 |
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 16 |
# Build paths inside the project like this: BASE_DIR / 'subdir'.
|
| 17 |
BASE_DIR = Path(__file__).resolve().parent.parent
|
| 18 |
|
|
|
|
|
|
|
| 19 |
|
| 20 |
# Quick-start development settings - unsuitable for production
|
| 21 |
# See https://docs.djangoproject.com/en/4.1/howto/deployment/checklist/
|
| 22 |
|
| 23 |
# SECURITY WARNING: keep the secret key used in production secret!
|
| 24 |
-
SECRET_KEY =
|
| 25 |
|
| 26 |
# SECURITY WARNING: don't run with debug turned on in production!
|
| 27 |
-
DEBUG =
|
| 28 |
-
|
| 29 |
-
|
| 30 |
-
|
| 31 |
-
|
| 32 |
-
"
|
| 33 |
-
|
| 34 |
-
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 35 |
|
| 36 |
# Application definition
|
| 37 |
|
|
@@ -83,14 +105,16 @@ WSGI_APPLICATION = "quizsite.wsgi.application"
|
|
| 83 |
|
| 84 |
DATABASES = {
|
| 85 |
"default": {
|
| 86 |
-
"ENGINE": "
|
| 87 |
-
"NAME":
|
| 88 |
-
"HOST":
|
| 89 |
-
"USER":
|
| 90 |
-
"PASSWORD":
|
|
|
|
| 91 |
}
|
| 92 |
}
|
| 93 |
|
|
|
|
| 94 |
# Password validation
|
| 95 |
# https://docs.djangoproject.com/en/4.1/ref/settings/#auth-password-validators
|
| 96 |
|
|
@@ -128,7 +152,11 @@ USE_TZ = True
|
|
| 128 |
STATIC_URL = "static/"
|
| 129 |
|
| 130 |
# https://whitenoise.evans.io/en/stable/django.html
|
| 131 |
-
|
|
|
|
|
|
|
|
|
|
|
|
|
| 132 |
STATIC_ROOT = os.path.join(BASE_DIR, "staticfiles")
|
| 133 |
|
| 134 |
# Default primary key field type
|
|
|
|
| 13 |
import os
|
| 14 |
from pathlib import Path
|
| 15 |
|
| 16 |
+
import environ
|
| 17 |
+
|
| 18 |
+
env = environ.Env(
|
| 19 |
+
# set casting, default value
|
| 20 |
+
DEBUG=(bool, False),
|
| 21 |
+
DBENGINE=(str, "django.db.backends.postgresql_psycopg2"),
|
| 22 |
+
DBSSL=(str, "disable"),
|
| 23 |
+
ADMIN_URL=(str, "admin/"),
|
| 24 |
+
)
|
| 25 |
+
|
| 26 |
# Build paths inside the project like this: BASE_DIR / 'subdir'.
|
| 27 |
BASE_DIR = Path(__file__).resolve().parent.parent
|
| 28 |
|
| 29 |
+
# Take environment variables from .env file
|
| 30 |
+
environ.Env.read_env(os.path.join(BASE_DIR.parent, ".env"))
|
| 31 |
|
| 32 |
# Quick-start development settings - unsuitable for production
|
| 33 |
# See https://docs.djangoproject.com/en/4.1/howto/deployment/checklist/
|
| 34 |
|
| 35 |
# SECURITY WARNING: keep the secret key used in production secret!
|
| 36 |
+
SECRET_KEY = env("SECRET_KEY")
|
| 37 |
|
| 38 |
# SECURITY WARNING: don't run with debug turned on in production!
|
| 39 |
+
DEBUG = env("DEBUG")
|
| 40 |
+
|
| 41 |
+
# Configure the domain name using the environment variable
|
| 42 |
+
# that Azure automatically creates for us.
|
| 43 |
+
if env.get_value("WEBSITE_HOSTNAME", default=None):
|
| 44 |
+
ALLOWED_HOSTS = [os.environ["WEBSITE_HOSTNAME"]]
|
| 45 |
+
CSRF_TRUSTED_ORIGINS = ["https://" + os.environ["WEBSITE_HOSTNAME"]]
|
| 46 |
+
else:
|
| 47 |
+
ALLOWED_HOSTS = []
|
| 48 |
+
CSRF_TRUSTED_ORIGINS = [
|
| 49 |
+
"http://localhost:8000",
|
| 50 |
+
]
|
| 51 |
+
if env.get_value("CODESPACE_NAME", default=None):
|
| 52 |
+
CSRF_TRUSTED_ORIGINS.append(
|
| 53 |
+
f"https://{env('CODESPACE_NAME')}-8000.{env('GITHUB_CODESPACES_PORT_FORWARDING_DOMAIN')}"
|
| 54 |
+
)
|
| 55 |
+
|
| 56 |
+
ADMIN_URL = env("ADMIN_URL")
|
| 57 |
|
| 58 |
# Application definition
|
| 59 |
|
|
|
|
| 105 |
|
| 106 |
DATABASES = {
|
| 107 |
"default": {
|
| 108 |
+
"ENGINE": env("DBENGINE"),
|
| 109 |
+
"NAME": env("DBNAME"),
|
| 110 |
+
"HOST": env("DBHOST"),
|
| 111 |
+
"USER": env("DBUSER"),
|
| 112 |
+
"PASSWORD": env("DBPASS"),
|
| 113 |
+
"OPTIONS": {"sslmode": env("DBSSL")},
|
| 114 |
}
|
| 115 |
}
|
| 116 |
|
| 117 |
+
|
| 118 |
# Password validation
|
| 119 |
# https://docs.djangoproject.com/en/4.1/ref/settings/#auth-password-validators
|
| 120 |
|
|
|
|
| 152 |
STATIC_URL = "static/"
|
| 153 |
|
| 154 |
# https://whitenoise.evans.io/en/stable/django.html
|
| 155 |
+
STORAGES = {
|
| 156 |
+
"staticfiles": {
|
| 157 |
+
"BACKEND": "whitenoise.storage.CompressedManifestStaticFilesStorage",
|
| 158 |
+
},
|
| 159 |
+
}
|
| 160 |
STATIC_ROOT = os.path.join(BASE_DIR, "staticfiles")
|
| 161 |
|
| 162 |
# Default primary key field type
|
src/quizsite/wsgi.py
CHANGED
|
@@ -11,7 +11,6 @@ import os
|
|
| 11 |
|
| 12 |
from django.core.wsgi import get_wsgi_application
|
| 13 |
|
| 14 |
-
|
| 15 |
-
os.environ.setdefault("DJANGO_SETTINGS_MODULE", settings_module)
|
| 16 |
|
| 17 |
application = get_wsgi_application()
|
|
|
|
| 11 |
|
| 12 |
from django.core.wsgi import get_wsgi_application
|
| 13 |
|
| 14 |
+
os.environ.setdefault("DJANGO_SETTINGS_MODULE", "quizsite.settings")
|
|
|
|
| 15 |
|
| 16 |
application = get_wsgi_application()
|
src/quizzes/models.py
CHANGED
|
@@ -1,3 +1,5 @@
|
|
|
|
|
|
|
|
| 1 |
from django.contrib.postgres import fields
|
| 2 |
from django.db import models
|
| 3 |
|
|
@@ -16,6 +18,9 @@ class Question(models.Model):
|
|
| 16 |
def __str__(self):
|
| 17 |
return self.prompt
|
| 18 |
|
|
|
|
|
|
|
|
|
|
| 19 |
|
| 20 |
class Answer(models.Model):
|
| 21 |
question = models.OneToOneField(Question, on_delete=models.CASCADE)
|
|
@@ -24,14 +29,17 @@ class Answer(models.Model):
|
|
| 24 |
class Meta:
|
| 25 |
abstract = True
|
| 26 |
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 27 |
|
| 28 |
class FreeTextAnswer(Answer):
|
| 29 |
case_sensitive = models.BooleanField(default=False)
|
| 30 |
|
| 31 |
-
def
|
| 32 |
-
return self.correct_answer
|
| 33 |
-
|
| 34 |
-
def is_correct(self, user_answer):
|
| 35 |
if not self.case_sensitive:
|
| 36 |
return user_answer.lower() == self.correct_answer.lower()
|
| 37 |
return user_answer == self.correct_answer
|
|
@@ -40,8 +48,5 @@ class FreeTextAnswer(Answer):
|
|
| 40 |
class MultipleChoiceAnswer(Answer):
|
| 41 |
choices = fields.ArrayField(models.CharField(max_length=200, blank=True))
|
| 42 |
|
| 43 |
-
def __str__(self):
|
| 44 |
return f"{self.correct_answer} from {self.choices}"
|
| 45 |
-
|
| 46 |
-
def is_correct(self, user_answer):
|
| 47 |
-
return user_answer == self.correct_answer
|
|
|
|
| 1 |
+
import typing
|
| 2 |
+
|
| 3 |
from django.contrib.postgres import fields
|
| 4 |
from django.db import models
|
| 5 |
|
|
|
|
| 18 |
def __str__(self):
|
| 19 |
return self.prompt
|
| 20 |
|
| 21 |
+
def get_answer(self) -> typing.Union["Answer", None]:
|
| 22 |
+
return getattr(self, "multiplechoiceanswer", None) or getattr(self, "freetextanswer", None)
|
| 23 |
+
|
| 24 |
|
| 25 |
class Answer(models.Model):
|
| 26 |
question = models.OneToOneField(Question, on_delete=models.CASCADE)
|
|
|
|
| 29 |
class Meta:
|
| 30 |
abstract = True
|
| 31 |
|
| 32 |
+
def __str__(self) -> str:
|
| 33 |
+
return self.correct_answer
|
| 34 |
+
|
| 35 |
+
def is_correct(self, user_answer) -> bool:
|
| 36 |
+
return user_answer == self.correct_answer
|
| 37 |
+
|
| 38 |
|
| 39 |
class FreeTextAnswer(Answer):
|
| 40 |
case_sensitive = models.BooleanField(default=False)
|
| 41 |
|
| 42 |
+
def is_correct(self, user_answer) -> bool:
|
|
|
|
|
|
|
|
|
|
| 43 |
if not self.case_sensitive:
|
| 44 |
return user_answer.lower() == self.correct_answer.lower()
|
| 45 |
return user_answer == self.correct_answer
|
|
|
|
| 48 |
class MultipleChoiceAnswer(Answer):
|
| 49 |
choices = fields.ArrayField(models.CharField(max_length=200, blank=True))
|
| 50 |
|
| 51 |
+
def __str__(self) -> str:
|
| 52 |
return f"{self.correct_answer} from {self.choices}"
|
|
|
|
|
|
|
|
|
src/quizzes/templates/quizzes/partial.html
CHANGED
|
@@ -1,5 +1,13 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
{% if is_correct %}
|
| 2 |
✅ You got it!
|
| 3 |
{% else %}
|
| 4 |
❌ Sorry! The correct answer is {{ correct_answer }}
|
| 5 |
{% endif %}
|
|
|
|
|
|
|
|
|
| 1 |
+
{% if error %}
|
| 2 |
+
<div class="alert alert-danger" role="alert">
|
| 3 |
+
{{ error }}
|
| 4 |
+
</div>
|
| 5 |
+
{% else %}
|
| 6 |
+
|
| 7 |
{% if is_correct %}
|
| 8 |
✅ You got it!
|
| 9 |
{% else %}
|
| 10 |
❌ Sorry! The correct answer is {{ correct_answer }}
|
| 11 |
{% endif %}
|
| 12 |
+
|
| 13 |
+
{% endif %}
|
src/quizzes/tests.py
CHANGED
|
@@ -53,12 +53,12 @@ class IndexViewTests(TestCase):
|
|
| 53 |
response = self.client.get(reverse("quizzes:index"))
|
| 54 |
self.assertEqual(response.status_code, 200)
|
| 55 |
self.assertContains(response, "No quizzes are available.")
|
| 56 |
-
self.
|
| 57 |
|
| 58 |
def test_one_quiz(self):
|
| 59 |
quiz, _, _ = create_quiz()
|
| 60 |
response = self.client.get(reverse("quizzes:index"))
|
| 61 |
-
self.
|
| 62 |
response.context["quiz_list"],
|
| 63 |
[quiz],
|
| 64 |
)
|
|
|
|
| 53 |
response = self.client.get(reverse("quizzes:index"))
|
| 54 |
self.assertEqual(response.status_code, 200)
|
| 55 |
self.assertContains(response, "No quizzes are available.")
|
| 56 |
+
self.assertQuerySetEqual(response.context["quiz_list"], [])
|
| 57 |
|
| 58 |
def test_one_quiz(self):
|
| 59 |
quiz, _, _ = create_quiz()
|
| 60 |
response = self.client.get(reverse("quizzes:index"))
|
| 61 |
+
self.assertQuerySetEqual(
|
| 62 |
response.context["quiz_list"],
|
| 63 |
[quiz],
|
| 64 |
)
|
src/quizzes/views.py
CHANGED
|
@@ -36,7 +36,9 @@ def display_question(request, quiz_id, question_id):
|
|
| 36 |
|
| 37 |
def grade_question(request, question_id):
|
| 38 |
question = get_object_or_404(Question, pk=question_id)
|
| 39 |
-
answer =
|
|
|
|
|
|
|
| 40 |
is_correct = answer.is_correct(request.POST.get("answer"))
|
| 41 |
return render(
|
| 42 |
request,
|
|
|
|
| 36 |
|
| 37 |
def grade_question(request, question_id):
|
| 38 |
question = get_object_or_404(Question, pk=question_id)
|
| 39 |
+
answer = question.get_answer()
|
| 40 |
+
if answer is None:
|
| 41 |
+
return render(request, "quizzes/partial.html", {"error": "Question must have an answer"}, status=422)
|
| 42 |
is_correct = answer.is_correct(request.POST.get("answer"))
|
| 43 |
return render(
|
| 44 |
request,
|
src/requirements.txt
CHANGED
|
@@ -2,3 +2,4 @@ Django==4.2
|
|
| 2 |
psycopg2==2.9.6
|
| 3 |
python-dotenv==1.0.0
|
| 4 |
whitenoise[brotli]==6.4.0
|
|
|
|
|
|
| 2 |
psycopg2==2.9.6
|
| 3 |
python-dotenv==1.0.0
|
| 4 |
whitenoise[brotli]==6.4.0
|
| 5 |
+
django-environ==0.10.0
|