The following text is a Git repository with code. The structure of the text are sections that begin with ----, followed by a single line containing the file path and file name, followed by a variable amount of lines containing the file contents. The text representing the Git repository ends when the symbols --END-- are encounted. Any further text beyond --END-- are meant to be interpreted as instructions using the aforementioned Git repository as context. ---- src/manage.py #!/usr/bin/env python """Django's command-line utility for administrative tasks.""" import os import sys def main(): """Run administrative tasks.""" os.environ.setdefault("DJANGO_SETTINGS_MODULE", "quizsite.settings") try: from django.core.management import execute_from_command_line except ImportError as exc: raise ImportError( "Couldn't import Django. Are you sure it's installed and " "available on your PYTHONPATH environment variable? Did you " "forget to activate a virtual environment?" ) from exc execute_from_command_line(sys.argv) if __name__ == "__main__": main() ---- src/quizsite/__init__.py ---- src/quizsite/asgi.py """ ASGI config for quizsite 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/4.1/howto/deployment/asgi/ """ import os from django.core.asgi import get_asgi_application os.environ.setdefault("DJANGO_SETTINGS_MODULE", "quizsite.settings") application = get_asgi_application() ---- src/quizsite/postgresql/__init__.py ---- src/quizsite/postgresql/base.py from azure.identity import DefaultAzureCredential from django.db.backends.postgresql import base class DatabaseWrapper(base.DatabaseWrapper): def get_connection_params(self): params = super().get_connection_params() if params.get("host", "").endswith(".database.azure.com"): azure_credential = DefaultAzureCredential() dbpass = azure_credential.get_token("https://ossrdbms-aad.database.windows.net/.default").token params["password"] = dbpass return params ---- src/quizsite/settings.py """ Django settings for quizsite project. Generated by 'django-admin startproject' using Django 4.1.1. For more information on this file, see https://docs.djangoproject.com/en/4.1/topics/settings/ For the full list of settings and their values, see https://docs.djangoproject.com/en/4.1/ref/settings/ """ import os from pathlib import Path import environ env = environ.Env( # set casting, default value DEBUG=(bool, False), DBENGINE=(str, "django.db.backends.postgresql_psycopg2"), DBSSL=(str, "disable"), ADMIN_URL=(str, "admin/"), STATIC_BACKEND=(str, "django.contrib.staticfiles.storage.StaticFilesStorage"), ) # Build paths inside the project like this: BASE_DIR / 'subdir'. BASE_DIR = Path(__file__).resolve().parent.parent # Take environment variables from .env file environ.Env.read_env(os.path.join(BASE_DIR.parent, ".env")) # Quick-start development settings - unsuitable for production # See https://docs.djangoproject.com/en/4.1/howto/deployment/checklist/ # SECURITY WARNING: keep the secret key used in production secret! SECRET_KEY = env("SECRET_KEY") # SECURITY WARNING: don't run with debug turned on in production! DEBUG = env("DEBUG") # This allows us to use the VS Code debugger to break on exceptions DEBUG_PROPAGATE_EXCEPTIONS = env("DEBUG") # Configure the domain name using the environment variable # that Azure automatically creates for us. if env.get_value("WEBSITE_HOSTNAME", default=None): ALLOWED_HOSTS = [os.environ["WEBSITE_HOSTNAME"]] CSRF_TRUSTED_ORIGINS = ["https://" + os.environ["WEBSITE_HOSTNAME"]] else: ALLOWED_HOSTS = [] CSRF_TRUSTED_ORIGINS = [ "http://localhost:8000", "https://mq-django-quiz-app.vercel.app", "https://mq-quiz.teddysc.me", ] if env.get_value("CODESPACE_NAME", default=None): CSRF_TRUSTED_ORIGINS.append( f"https://{env('CODESPACE_NAME')}-8000.{env('GITHUB_CODESPACES_PORT_FORWARDING_DOMAIN')}" ) ADMIN_URL = env("ADMIN_URL") # Application definition INSTALLED_APPS = [ "markdownify.apps.MarkdownifyConfig", "quizzes.apps.QuizzesConfig", "django.contrib.admin", "django.contrib.auth", "django.contrib.contenttypes", "django.contrib.sessions", "django.contrib.messages", "whitenoise.runserver_nostatic", "django.contrib.staticfiles", ] MIDDLEWARE = [ "django.middleware.security.SecurityMiddleware", "whitenoise.middleware.WhiteNoiseMiddleware", "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 = "quizsite.urls" TEMPLATES = [ { "BACKEND": "django.template.backends.django.DjangoTemplates", "DIRS": [], "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 = "quizsite.wsgi.application" # Database # https://docs.djangoproject.com/en/4.1/ref/settings/#databases DATABASES = { "default": { "ENGINE": "quizsite.postgresql", "NAME": env("DBNAME"), "HOST": env("DBHOST"), "USER": env("DBUSER"), "PASSWORD": env("DBPASS", default="PASSWORD_WILL_BE_SET_LATER"), "OPTIONS": {"sslmode": env("DBSSL")}, "CONN_MAX_AGE": 60 * 60 * 6, # 6 hours } } # Password validation # https://docs.djangoproject.com/en/4.1/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/4.1/topics/i18n/ LANGUAGE_CODE = "en-us" TIME_ZONE = "America/Los_Angeles" USE_I18N = True USE_TZ = True # Static files (CSS, JavaScript, Images) # https://docs.djangoproject.com/en/4.1/howto/static-files/ STATIC_URL = "static/" # https://whitenoise.evans.io/en/stable/django.html STORAGES = { "staticfiles": { "BACKEND": env("STATIC_BACKEND"), }, } STATIC_ROOT = os.path.join(BASE_DIR, "staticfiles") # Default primary key field type # https://docs.djangoproject.com/en/4.1/ref/settings/#default-auto-field DEFAULT_AUTO_FIELD = "django.db.models.BigAutoField" ---- src/quizsite/urls.py """quizsite URL Configuration The `urlpatterns` list routes URLs to views. For more information please see: https://docs.djangoproject.com/en/4.1/topics/http/urls/ Examples: Function views 1. Add an import: from my_app import views 2. Add a URL to urlpatterns: path('', views.home, name='home') Class-based views 1. Add an import: from other_app.views import Home 2. Add a URL to urlpatterns: path('', Home.as_view(), name='home') Including another URLconf 1. Import the include() function: from django.urls import include, path 2. Add a URL to urlpatterns: path('blog/', include('blog.urls')) """ from django.conf import settings from django.contrib import admin from django.urls import include, path urlpatterns = [ path("", include("quizzes.urls")), path(settings.ADMIN_URL, admin.site.urls), ] ---- src/quizsite/wsgi.py """ WSGI config for quizsite 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/4.1/howto/deployment/wsgi/ """ import os from django.core.wsgi import get_wsgi_application os.environ.setdefault("DJANGO_SETTINGS_MODULE", "quizsite.settings") application = get_wsgi_application() app = application ---- src/quizzes/__init__.py ---- src/quizzes/admin.py from django.contrib import admin from .models import FreeTextAnswer, MultipleChoiceAnswer, Question, Quiz, LLMGradedAnswer admin.site.register(Quiz) class FreeTextAnswerInline(admin.StackedInline): model = FreeTextAnswer class MultipleChoiceAnswerInline(admin.StackedInline): model = MultipleChoiceAnswer class LLMGradedAnswerInline(admin.StackedInline): model = LLMGradedAnswer class QuestionAdmin(admin.ModelAdmin): inlines = [FreeTextAnswerInline, MultipleChoiceAnswerInline, LLMGradedAnswerInline] admin.site.register(Question, QuestionAdmin) ---- src/quizzes/apps.py from django.apps import AppConfig class QuizzesConfig(AppConfig): default_auto_field = "django.db.models.BigAutoField" name = "quizzes" ---- src/quizzes/migrations/0001_initial.py # Generated by Django 4.1.1 on 2022-09-14 18:17 import django.contrib.postgres.fields import django.db.models.deletion from django.db import migrations, models class Migration(migrations.Migration): initial = True dependencies = [] operations = [ migrations.CreateModel( name="Quiz", fields=[ ( "id", models.BigAutoField( auto_created=True, primary_key=True, serialize=False, verbose_name="ID", ), ), ("name", models.CharField(max_length=100)), ], ), migrations.CreateModel( name="Question", fields=[ ( "id", models.BigAutoField( auto_created=True, primary_key=True, serialize=False, verbose_name="ID", ), ), ("prompt", models.CharField(max_length=200)), ( "answer_status", models.CharField(default="unanswered", max_length=16), ), ( "quiz", models.ForeignKey(on_delete=django.db.models.deletion.CASCADE, to="quizzes.quiz"), ), ], ), migrations.CreateModel( name="MultipleChoiceAnswer", fields=[ ( "id", models.BigAutoField( auto_created=True, primary_key=True, serialize=False, verbose_name="ID", ), ), ("correct_answer", models.CharField(max_length=200)), ( "choices", django.contrib.postgres.fields.ArrayField( base_field=models.CharField(blank=True, max_length=200), size=None, ), ), ( "question", models.ForeignKey( on_delete=django.db.models.deletion.CASCADE, to="quizzes.question", ), ), ], ), migrations.CreateModel( name="FreeTextAnswer", fields=[ ( "id", models.BigAutoField( auto_created=True, primary_key=True, serialize=False, verbose_name="ID", ), ), ("correct_answer", models.CharField(max_length=200)), ("case_sensitive", models.BooleanField(default=False)), ( "question", models.ForeignKey( on_delete=django.db.models.deletion.CASCADE, to="quizzes.question", ), ), ], ), ] ---- src/quizzes/migrations/0002_remove_question_answer_status_and_more.py # Generated by Django 4.1.1 on 2022-09-15 00:57 import django.db.models.deletion from django.db import migrations, models class Migration(migrations.Migration): dependencies = [ ("quizzes", "0001_initial"), ] operations = [ migrations.RemoveField( model_name="question", name="answer_status", ), migrations.AlterField( model_name="freetextanswer", name="question", field=models.OneToOneField(on_delete=django.db.models.deletion.CASCADE, to="quizzes.question"), ), migrations.AlterField( model_name="multiplechoiceanswer", name="question", field=models.OneToOneField(on_delete=django.db.models.deletion.CASCADE, to="quizzes.question"), ), ] ---- src/quizzes/migrations/0003_alter_freetextanswer_correct_answer_and_more.py # Generated by Django 4.2.7 on 2024-10-08 23:30 from django.db import migrations, models import django.db.models.deletion class Migration(migrations.Migration): dependencies = [ ("quizzes", "0002_remove_question_answer_status_and_more"), ] operations = [ migrations.AlterField( model_name="freetextanswer", name="correct_answer", field=models.CharField(default="", max_length=200), ), migrations.AlterField( model_name="multiplechoiceanswer", name="correct_answer", field=models.CharField(default="", max_length=200), ), migrations.CreateModel( name="LLMGradedAnswer", fields=[ ("id", models.BigAutoField(auto_created=True, primary_key=True, serialize=False, verbose_name="ID")), ( "rubrics", models.TextField( blank=True, null=True, verbose_name="Grading Rubrics - For LLM-graded questions only. You can leave this empty.", ), ), ("question", models.OneToOneField(on_delete=django.db.models.deletion.CASCADE, to="quizzes.question")), ], options={ "abstract": False, }, ), ] ---- src/quizzes/migrations/0004_remove_llmgradedanswer_rubrics_question_rubrics.py # Generated by Django 4.2.7 on 2024-10-08 23:46 from django.db import migrations, models class Migration(migrations.Migration): dependencies = [ ("quizzes", "0003_alter_freetextanswer_correct_answer_and_more"), ] operations = [ migrations.RemoveField( model_name="llmgradedanswer", name="rubrics", ), migrations.AddField( model_name="question", name="rubrics", field=models.TextField( blank=True, null=True, verbose_name="Grading Rubrics - For LLM-graded questions only. You can leave this empty.", ), ), ] ---- src/quizzes/migrations/__init__.py ---- src/quizzes/models.py import typing from django.contrib.postgres import fields from django.db import models class Quiz(models.Model): name = models.CharField(max_length=100) def __str__(self): return self.name class Question(models.Model): quiz = models.ForeignKey(Quiz, on_delete=models.CASCADE) prompt = models.CharField(max_length=200) rubrics = models.TextField( blank=True, null=True, verbose_name="Grading Rubrics - For LLM-graded questions only. You can leave this empty." ) def __str__(self): return self.prompt def get_answer(self) -> typing.Union["Answer", None]: return ( getattr(self, "multiplechoiceanswer", None) or getattr(self, "freetextanswer", None) or getattr(self, "llmgradedanswer", None) ) class Answer(models.Model): question = models.OneToOneField(Question, on_delete=models.CASCADE) class Meta: abstract = True def __str__(self) -> str: return ( getattr(self, "correct_answer", None) or getattr(self, "rubrics", None) or "No answer or rubrics provided" ) def is_correct(self, user_answer) -> bool: return user_answer == getattr(self, "correct_answer", None) class FreeTextAnswer(Answer): correct_answer = models.CharField(max_length=200, default="") case_sensitive = models.BooleanField(default=False) def is_correct(self, user_answer) -> bool: if not self.case_sensitive: return user_answer.lower() == self.correct_answer.lower() return user_answer == self.correct_answer class LLMGradedAnswer(Answer): def grade(self, user_answer, rubrics) -> dict: import requests """ Grades the user's answer by calling the grading API. Args: user_answer (str): The answer provided by the user. rubrics (str): The grading rubrics. Returns: bool: True if the user's answer is correct, False otherwise. """ api_url = "http://localhost/api/grade" payload = {"user_answer": user_answer, "rubrics": rubrics} try: response = requests.post(api_url, json=payload) response.raise_for_status() # Raise an error for bad status codes result = response.json() # Assuming the API returns a JSON object with a 'correct' field return result except requests.RequestException as e: # Handle any errors that occur during the request print(f"An error occurred: {e}") return {"result": "error", "message": str(e)} class MultipleChoiceAnswer(Answer): correct_answer = models.CharField(max_length=200, default="") choices = fields.ArrayField(models.CharField(max_length=200, blank=True)) def __str__(self) -> str: return f"{self.correct_answer} from {self.choices}" ---- src/quizzes/tests.py from django.test import TestCase from django.urls import reverse from .models import FreeTextAnswer, MultipleChoiceAnswer, Question, Quiz def create_quiz(): quiz = Quiz.objects.create(name="Butterflies") question = Question.objects.create(quiz=quiz, prompt="What plant do Swallowtail caterpillars eat?") answer = MultipleChoiceAnswer.objects.create( question=question, correct_answer="Dill", choices=["Thistle", "Milkweed", "Dill"] ) return quiz, question, answer class FreeTextAnswerModelTests(TestCase): def test_case_insensitive(self): ans = FreeTextAnswer(correct_answer="Milkweed", case_sensitive=False) self.assertTrue(ans.is_correct("Milkweed")) self.assertTrue(ans.is_correct("milkweed")) self.assertFalse(ans.is_correct("thistle")) def test_case_sensitive(self): ans = FreeTextAnswer(correct_answer="Armeria Maritima", case_sensitive=True) self.assertFalse(ans.is_correct("armeria maritima")) self.assertTrue(ans.is_correct("Armeria Maritima")) class MultipleChoiceAnswerModelTests(TestCase): def test_choices(self): ans = MultipleChoiceAnswer(correct_answer="Dill", choices=["Milkweed", "Dill", "Thistle"]) self.assertTrue(ans.is_correct("Dill")) self.assertFalse(ans.is_correct("dill")) self.assertFalse(ans.is_correct("Milkweed")) class QuizModelTests(TestCase): def test_quiz_relations(self): quiz = Quiz.objects.create(name="Butterflies") q1 = Question.objects.create(quiz=quiz, prompt="What plant do Swallowtail caterpillars eat?") a1 = MultipleChoiceAnswer.objects.create( question=q1, correct_answer="Dill", choices=["Thistle", "Milkweed", "Dill"] ) q2 = Question.objects.create(quiz=quiz, prompt="What plant do Monarch caterpillars eat?") a2 = FreeTextAnswer.objects.create(question=q2, correct_answer="Milkweed", case_sensitive=False) self.assertEqual(len(quiz.question_set.all()), 2) self.assertEqual(q1.multiplechoiceanswer, a1) self.assertEqual(q2.freetextanswer, a2) class IndexViewTests(TestCase): def test_no_quizzes(self): response = self.client.get(reverse("quizzes:index")) self.assertEqual(response.status_code, 200) self.assertContains(response, "No quizzes are available.") self.assertQuerySetEqual(response.context["quiz_list"], []) def test_one_quiz(self): quiz, _, _ = create_quiz() response = self.client.get(reverse("quizzes:index")) self.assertQuerySetEqual( response.context["quiz_list"], [quiz], ) class DisplayQuizViewTests(TestCase): def test_quiz_404(self): url = reverse("quizzes:display_quiz", args=(12,)) response = self.client.get(url) self.assertEqual(response.status_code, 404) def test_quiz_redirects(self): quiz, question, _ = create_quiz() url = reverse("quizzes:display_quiz", args=(quiz.pk,)) response = self.client.get(url) self.assertRedirects(response, reverse("quizzes:display_question", args=(quiz.pk, question.pk))) class DisplayQuestionViewTests(TestCase): def test_quiz_404(self): url = reverse("quizzes:display_question", args=(12, 1)) response = self.client.get(url) self.assertEqual(response.status_code, 404) def test_question_404(self): quiz, question, _ = create_quiz() url = reverse("quizzes:display_question", args=(quiz.pk, question.pk + 100)) response = self.client.get(url) self.assertContains(response, "that question doesn't exist") def test_quiz_question_exists(self): quiz, question, answer = create_quiz() url = reverse("quizzes:display_question", args=(quiz.pk, question.pk)) response = self.client.get(url) self.assertContains(response, quiz.name) self.assertContains(response, question.prompt) self.assertContains(response, answer.choices[0]) class GradeQuestionViewTests(TestCase): def test_question_404(self): url = reverse("quizzes:grade_question", args=(12,)) response = self.client.get(url) self.assertEqual(response.status_code, 404) def test_question_correct(self): _, question, answer = create_quiz() url = reverse("quizzes:grade_question", args=(question.pk,)) response = self.client.post(url, {"answer": answer.correct_answer}) self.assertTrue(response.context["is_correct"]) self.assertEqual(response.context["correct_answer"], answer.correct_answer) ---- src/quizzes/urls.py from django.urls import path from . import views app_name = "quizzes" urlpatterns = [ path("", views.IndexView.as_view(), name="index"), path("quizzes//", views.display_quiz, name="display_quiz"), path("quizzes//questions/", views.display_question, name="display_question"), path("questions//grade/", views.grade_question, name="grade_question"), ] ---- src/quizzes/views.py from django.shortcuts import get_object_or_404, redirect, render from django.urls import reverse from django.views import generic from .models import Question, Quiz class IndexView(generic.ListView): model = Quiz template_name = "quizzes/index.html" def display_quiz(request, quiz_id): quiz = get_object_or_404(Quiz, pk=quiz_id) question = quiz.question_set.first() return redirect(reverse("quizzes:display_question", kwargs={"quiz_id": quiz_id, "question_id": question.pk})) def display_question(request, quiz_id, question_id): quiz = get_object_or_404(Quiz, pk=quiz_id) # fetch ALL of the questions to find current and next question questions = quiz.question_set.all() current_question, next_question = None, None for ind, question in enumerate(questions): if question.pk == question_id: current_question = question if ind != len(questions) - 1: next_question = questions[ind + 1] return render( request, "quizzes/display.html", {"quiz": quiz, "question": current_question, "next_question": next_question}, ) def grade_question(request, question_id): question = get_object_or_404(Question, pk=question_id) answer = question.get_answer() if answer is None: return render(request, "quizzes/partial.html", {"error": "Question must have an answer"}, status=422) is_correct = answer.is_correct(request.POST.get("answer")) return render( request, "quizzes/partial.html", {"is_correct": is_correct, "correct_answer": answer.correct_answer}, ) ---- src/setup_postgres_azurerole.py import argparse import logging import psycopg2 from azure.identity import DefaultAzureCredential logger = logging.getLogger("scripts") def assign_role_for_webapp(postgres_host, postgres_username, app_identity_name): if not postgres_host.endswith(".database.azure.com"): logger.info("This script is intended to be used with Azure Database for PostgreSQL.") logger.info("Please set the environment variable DBHOST to the Azure Database for PostgreSQL server hostname.") return logger.info("Authenticating to Azure Database for PostgreSQL using Azure Identity...") azure_credential = DefaultAzureCredential() token = azure_credential.get_token("https://ossrdbms-aad.database.windows.net/.default") conn = psycopg2.connect( database="postgres", # You must connect to postgres database when assigning roles user=postgres_username, password=token.token, host=postgres_host, sslmode="require", ) conn.autocommit = True cur = conn.cursor() cur.execute(f"select * from pgaadauth_list_principals(false) WHERE rolname = '{app_identity_name}'") identities = cur.fetchall() if len(identities) > 0: logger.info(f"Found an existing PostgreSQL role for identity {app_identity_name}") else: logger.info(f"Creating a PostgreSQL role for identity {app_identity_name}") cur.execute(f"SELECT * FROM pgaadauth_create_principal('{app_identity_name}', false, false)") logger.info(f"Granting permissions to {app_identity_name}") # set role to azure_pg_admin cur.execute(f'GRANT USAGE ON SCHEMA public TO "{app_identity_name}"') cur.execute(f'GRANT CREATE ON SCHEMA public TO "{app_identity_name}"') cur.execute(f'GRANT ALL PRIVILEGES ON ALL TABLES IN SCHEMA public TO "{app_identity_name}"') cur.execute( f"ALTER DEFAULT PRIVILEGES IN SCHEMA public " f'GRANT SELECT, UPDATE, INSERT, DELETE ON TABLES TO "{app_identity_name}"' ) cur.close() if __name__ == "__main__": logging.basicConfig(level=logging.WARNING) logger.setLevel(logging.INFO) parser = argparse.ArgumentParser(description="Create database schema") parser.add_argument("--host", type=str, help="Postgres host") parser.add_argument("--username", type=str, help="Postgres username") parser.add_argument("--app-identity-name", type=str, help="Azure App Service identity name") args = parser.parse_args() if not args.host.endswith(".database.azure.com"): logger.info("This script is intended to be used with Azure Database for PostgreSQL, not local PostgreSQL.") exit(1) assign_role_for_webapp(args.host, args.username, args.app_identity_name) logger.info("Role created successfully.") --END-- reply 'ok' and only 'ok' if you read.