diff --git a/anatomy/filters.py b/anatomy/filters.py index 0778d15f..0db1e28a 100644 --- a/anatomy/filters.py +++ b/anatomy/filters.py @@ -51,7 +51,7 @@ class AnatomyUserAnswerFilter(django_filters.FilterSet): model= CidUserAnswer fields = ( "cid", - #"user", + "user", "question", "answer", #"answer_compare", diff --git a/anatomy/migrations/0063_ciduseranswer_user.py b/anatomy/migrations/0063_ciduseranswer_user.py new file mode 100644 index 00000000..80838968 --- /dev/null +++ b/anatomy/migrations/0063_ciduseranswer_user.py @@ -0,0 +1,21 @@ +# Generated by Django 3.2.13 on 2022-05-20 19:51 + +from django.conf import settings +from django.db import migrations, models +import django.db.models.deletion + + +class Migration(migrations.Migration): + + dependencies = [ + migrations.swappable_dependency(settings.AUTH_USER_MODEL), + ('anatomy', '0062_exam_valid_user_users'), + ] + + operations = [ + migrations.AddField( + model_name='ciduseranswer', + name='user', + field=models.ForeignKey(blank=True, null=True, on_delete=django.db.models.deletion.CASCADE, related_name='user_anatomy_user_answers', to=settings.AUTH_USER_MODEL), + ), + ] diff --git a/anatomy/migrations/0064_auto_20220521_1020.py b/anatomy/migrations/0064_auto_20220521_1020.py new file mode 100644 index 00000000..3bd07906 --- /dev/null +++ b/anatomy/migrations/0064_auto_20220521_1020.py @@ -0,0 +1,24 @@ +# Generated by Django 3.2.13 on 2022-05-21 09:20 + +from django.db import migrations, models + + +class Migration(migrations.Migration): + + dependencies = [ + ('generic', '0032_userprofile_registration_number'), + ('anatomy', '0063_ciduseranswer_user'), + ] + + operations = [ + migrations.AddField( + model_name='exam', + name='cid_user_groups', + field=models.ManyToManyField(blank=True, help_text='These groups define which candidates are able to be added to the exams/collection.', related_name='anatomy_cid_user_groups', to='generic.CidUserGroup'), + ), + migrations.AddField( + model_name='exam', + name='user_user_groups', + field=models.ManyToManyField(blank=True, help_text='These groups define which candidates are able to be added to the exams/collection.', related_name='anatomy_user_user_groups', to='generic.UserUserGroup'), + ), + ] diff --git a/anatomy/models.py b/anatomy/models.py index b4427e07..3448d031 100644 --- a/anatomy/models.py +++ b/anatomy/models.py @@ -15,7 +15,7 @@ from sortedm2m.fields import SortedManyToManyField import string -from generic.models import CidUser, Examination, ExamBase, QuestionNote +from generic.models import CidUser, CidUserGroup, Examination, ExamBase, QuestionNote, UserUserGroup from collections import defaultdict from helpers.images import image_as_base64 @@ -391,6 +391,20 @@ class Exam(ExamBase): settings.AUTH_USER_MODEL, blank=True, related_name="user_anatomy_exams" ) + cid_user_groups = models.ManyToManyField( + CidUserGroup, + blank=True, + help_text="These groups define which candidates are able to be added to the exams/collection.", + related_name="anatomy_cid_user_groups" + ) + + user_user_groups = models.ManyToManyField( + UserUserGroup, + blank=True, + help_text="These groups define which candidates are able to be added to the exams/collection.", + related_name="anatomy_user_user_groups" + ) + def get_exam_json(self, based=True): questions = self.exam_questions.all() @@ -452,9 +466,9 @@ class CidUserAnswer(models.Model): help_text="Candidate ID (limitied by BigIntegerField size)", ) - #user = models.ForeignKey( - # settings.AUTH_USER_MODEL, on_delete=models.CASCADE, blank=True, null=True#, related_name="user_anatomy_user_answers" - #) + user = models.ForeignKey( + settings.AUTH_USER_MODEL, on_delete=models.CASCADE, blank=True, null=True, related_name="user_anatomy_user_answers" + ) # Each user answer is associated with a particular exam exam = models.ForeignKey( diff --git a/anatomy/tables.py b/anatomy/tables.py index 8d369fe6..de2922a7 100644 --- a/anatomy/tables.py +++ b/anatomy/tables.py @@ -69,7 +69,7 @@ class AnatomyUserAnswerTable(tables.Table): template_name = "django_tables2/bootstrap4.html" fields = ( "cid", - #"user", + "user", "question", "answer", #"answer_compare", diff --git a/anatomy/views.py b/anatomy/views.py index 20a0841b..7d2bad65 100644 --- a/anatomy/views.py +++ b/anatomy/views.py @@ -250,11 +250,6 @@ def loadJsonAnswer(answer): {"success": False, "error": "invalid cid / passcode"} ) - # if not exam.active: - # return False, JsonResponse( - # {"success": False, "error": "No active exam: {}".format(eid)} - # ) - exiting_answers = CidUserAnswer.objects.filter( question__id=answer["qid"], exam__id=eid, cid=cid ) diff --git a/generic/views.py b/generic/views.py index d638b467..ff8bc420 100644 --- a/generic/views.py +++ b/generic/views.py @@ -1039,7 +1039,38 @@ class ExamViews(View, LoginRequiredMixin): n = 0 for answer in json.loads(request.POST.get("answers")): - ret_status, ret = self.loadJsonAnswer(answer, user=request.user) + + eid = int(answer["eid"].split("/")[1]) + uid = False + + try: + cid = int(answer["cid"]) + except ValueError: + if answer["cid"].startswith("u-"): + cid = False + uid = int(answer["cid"][2:]) + + if not uid == request.user.id: + return JsonResponse( + {"success": False, "error": "user / uid mismatch"} + ) + + else: + return JsonResponse( + {"success": False, "error": "cid or eid or answers not defined"} + ) + + if not uid: + # As access is not restricted make sure the data appears valid + if (not isinstance(cid, int)) or ( + not isinstance(eid, int) or (not isinstance(answer["ans"], str)) + ): + return JsonResponse( + {"success": False, "error": "cid or eid or answers not defined"} + ) + + + ret_status, ret = self.loadJsonAnswer(answer, cid=cid, eid=eid, uid=uid) # try: # ret_status, ret = self.loadJsonAnswer(answer) # except: diff --git a/pytest.ini b/pytest.ini new file mode 100644 index 00000000..fde73665 --- /dev/null +++ b/pytest.ini @@ -0,0 +1,3 @@ +[pytest] +DJANGO_SETTINGS_MODULE=rad.settings +python_files = tests.py test_*.py *_tests.py \ No newline at end of file diff --git a/rapids/migrations/0057_alter_ciduseranswer_user.py b/rapids/migrations/0057_alter_ciduseranswer_user.py new file mode 100644 index 00000000..84ee24c6 --- /dev/null +++ b/rapids/migrations/0057_alter_ciduseranswer_user.py @@ -0,0 +1,21 @@ +# Generated by Django 3.2.13 on 2022-05-20 19:51 + +from django.conf import settings +from django.db import migrations, models +import django.db.models.deletion + + +class Migration(migrations.Migration): + + dependencies = [ + migrations.swappable_dependency(settings.AUTH_USER_MODEL), + ('rapids', '0056_exam_user_user_groups'), + ] + + operations = [ + migrations.AlterField( + model_name='ciduseranswer', + name='user', + field=models.ForeignKey(blank=True, null=True, on_delete=django.db.models.deletion.CASCADE, related_name='user_rapid_user_answers', to=settings.AUTH_USER_MODEL), + ), + ] diff --git a/rapids/migrations/0058_auto_20220521_1020.py b/rapids/migrations/0058_auto_20220521_1020.py new file mode 100644 index 00000000..5034f384 --- /dev/null +++ b/rapids/migrations/0058_auto_20220521_1020.py @@ -0,0 +1,24 @@ +# Generated by Django 3.2.13 on 2022-05-21 09:20 + +from django.db import migrations, models + + +class Migration(migrations.Migration): + + dependencies = [ + ('generic', '0032_userprofile_registration_number'), + ('rapids', '0057_alter_ciduseranswer_user'), + ] + + operations = [ + migrations.AlterField( + model_name='exam', + name='cid_user_groups', + field=models.ManyToManyField(blank=True, help_text='These groups define which candidates are able to be added to the exams/collection.', related_name='rapid_cid_user_groups', to='generic.CidUserGroup'), + ), + migrations.AlterField( + model_name='exam', + name='user_user_groups', + field=models.ManyToManyField(blank=True, help_text='These groups define which candidates are able to be added to the exams/collection.', related_name='rapid_user_user_groups', to='generic.UserUserGroup'), + ), + ] diff --git a/rapids/models.py b/rapids/models.py index 314879ef..fce04c10 100644 --- a/rapids/models.py +++ b/rapids/models.py @@ -1,4 +1,5 @@ import os +import re import dicognito from rad.settings import REMOTE_URL from django.db import models @@ -597,7 +598,7 @@ class Exam(ExamBase): time_limit = models.IntegerField( help_text="Exam time limit (in seconds). Default is 2100 secondse (35 minutes)", - default=2100, + default=35 * 60, ) author = models.ManyToManyField( @@ -619,12 +620,15 @@ class Exam(ExamBase): CidUserGroup, blank=True, help_text="These groups define which candidates are able to be added to the exams/collection.", + related_name="rapid_cid_user_groups" + ) user_user_groups = models.ManyToManyField( UserUserGroup, blank=True, help_text="These groups define which candidates are able to be added to the exams/collection.", + related_name="rapid_user_user_groups" ) def get_normal_abnormal_breakdown(self): @@ -740,7 +744,8 @@ class CidUserAnswer(models.Model): settings.AUTH_USER_MODEL, on_delete=models.CASCADE, blank=True, - null=True, # , related_name="user_rapid_user_answers" + null=True, + related_name="user_rapid_user_answers", ) # Each user answer is associated with a particular exam diff --git a/rapids/tests.py b/rapids/tests.py deleted file mode 100755 index 7ce503c2..00000000 --- a/rapids/tests.py +++ /dev/null @@ -1,3 +0,0 @@ -from django.test import TestCase - -# Create your tests here. diff --git a/rapids/tests/test_models.py b/rapids/tests/test_models.py new file mode 100644 index 00000000..aaf8a200 --- /dev/null +++ b/rapids/tests/test_models.py @@ -0,0 +1,20 @@ + +import pytest +from rapids.models import Abnormality, Exam + +#@pytest.mark.django_db +def test_create_abnormality(db): + abnormality = Abnormality.objects.create(name="Abnorm") + assert abnormality.name == "Abnorm" + +@pytest.fixture +def create_exam(db): + return Exam.objects.create(name="test exam") + +def test_exam_creation(create_exam): + exams = Exam.objects.filter(name="test exam") + assert exams.exists() + + exam = exams.first() + + assert exam.time_limit == 35 * 60 diff --git a/rapids/tests/test_urls.py b/rapids/tests/test_urls.py new file mode 100644 index 00000000..ecd6e105 --- /dev/null +++ b/rapids/tests/test_urls.py @@ -0,0 +1,12 @@ +import pytest +#from django.contrib.auth.models import User +from django.urls import reverse + +@pytest.fixture +def create_superuser(db, django_user_model): + return django_user_model.objects.create_superuser("admin", "ross@xkjq.uk", "adminpassword") + +def test_index(client, create_superuser): + client.login(username="admin", password="adminpassword") + response = client.get(reverse('rapids:index')) + assert response.status_code == 200 \ No newline at end of file diff --git a/rapids/tests/tests.py b/rapids/tests/tests.py new file mode 100755 index 00000000..3c549e79 --- /dev/null +++ b/rapids/tests/tests.py @@ -0,0 +1,16 @@ +from django.test import TestCase, Client + +from django.urls import reverse +from django.contrib.auth.models import User + +# Create your tests here. + + +#class TestUrls(TestCase): +# def setUp(self): +# User.objects.create_superuser("admin", "ross@xkjq.uk", "adminpassword") +# +# def test_index(self): +# self.client.login(username="admin", password="adminpassword") +# response = self.client.get(reverse('rapids:index')) +# self.assertEqual(response.status_code, 200) \ No newline at end of file diff --git a/rapids/views.py b/rapids/views.py index 57a4fd35..3457045c 100755 --- a/rapids/views.py +++ b/rapids/views.py @@ -509,44 +509,7 @@ class RapidView(LoginRequiredMixin, SingleTableMixin, FilterView): filterset_class = RapidFilter -def loadJsonAnswer(answer, user=None): - eid = int(answer["eid"].split("/")[1]) - uid = False - - try: - cid = int(answer["cid"]) - except ValueError: - if answer["cid"].startswith("u-"): - cid = False - uid = int(answer["cid"][2:]) - - print(f"{uid=}") - print(f"{uid=}") - if not uid == user.id: - return False, JsonResponse( - {"success": False, "error": "user / uid mismatch"} - ) - - else: - return False, JsonResponse( - {"success": False, "error": "cid or eid or answers not defined"} - ) - - #uid = int(answer["uid"]) - - if not uid: - # As access is not restricted make sure the data appears valid - if (not isinstance(cid, int)) or ( - not isinstance(eid, int) or (not isinstance(answer["ans"], str)) - ): - return False, JsonResponse( - {"success": False, "error": "cid or eid or answers not defined"} - ) - - ## The model should catch invalid data but this should be less intensive - #max_int = 999999999999999999999 - #if cid > max_int or eid > max_int: - # return False, JsonResponse({"success": False, "error": "invalid cid"}) +def loadJsonAnswer(answer, cid, eid, uid): exam = get_object_or_404(Exam, pk=eid) @@ -555,11 +518,6 @@ def loadJsonAnswer(answer, user=None): {"success": False, "error": "invalid cid / passcode"} ) - # if not exam.active: - # return False, JsonResponse( - # {"success": False, "error": "No active exam: {}".format(eid)} - # ) - if uid: exiting_answers = CidUserAnswer.objects.filter( question__id=answer["qid"], exam__id=eid, user__id=uid diff --git a/requirements.txt b/requirements.txt index 5336eb24..e2066674 100644 --- a/requirements.txt +++ b/requirements.txt @@ -25,4 +25,5 @@ django-autocomplete-light django-querysetsequence dicognito django_unused_media +pytest-django gunicorn \ No newline at end of file diff --git a/templates/profile.html b/templates/profile.html index d2971100..d43a62ed 100644 --- a/templates/profile.html +++ b/templates/profile.html @@ -15,7 +15,7 @@
Supervisor: {{ user.userprofile.supervisor_name }}
- Email: {{ user.userprofile.supervisor_email }}
+ Supervisor Email: {{ user.userprofile.supervisor_email }}
Registration number: {{ user.userprofile.registration_number }}