From bf77a1654dff9bca7c378e6d13ed8c312ea1e93a Mon Sep 17 00:00:00 2001 From: Michael Nef Date: Tue, 5 Jul 2022 14:56:45 +0800 Subject: [PATCH 01/89] basic body card --- frontend/components/utils/BodyCard.tsx | 19 +++++++++++++++++++ frontend/pages/reviewer.tsx | 23 +++++++++++++++++++++++ 2 files changed, 42 insertions(+) create mode 100644 frontend/components/utils/BodyCard.tsx create mode 100644 frontend/pages/reviewer.tsx diff --git a/frontend/components/utils/BodyCard.tsx b/frontend/components/utils/BodyCard.tsx new file mode 100644 index 0000000..30b0284 --- /dev/null +++ b/frontend/components/utils/BodyCard.tsx @@ -0,0 +1,19 @@ +import * as React from 'react'; + +import { Card, Container } from '@mui/material' + +type BodyCardProps = { + children?: React.ReactNode; +} + +const BodyCard = ({children}: BodyCardProps): JSX.Element => { + return ( + + + {children} + + + ); +} + +export default BodyCard; diff --git a/frontend/pages/reviewer.tsx b/frontend/pages/reviewer.tsx new file mode 100644 index 0000000..76c8b4c --- /dev/null +++ b/frontend/pages/reviewer.tsx @@ -0,0 +1,23 @@ +import type { NextPage } from 'next' +import Head from 'next/head' +import Image from 'next/image' +import Link from 'next/link' +import styles from '../styles/Home.module.css' + +import BodyCard from '../components/utils/BodyCard' + +import { Card } from '@mui/material'; + +const Reviewer: NextPage = () => { + return ( +
+ +
+ swag +
+
+
+ ) +} + +export default Reviewer; From 08e6f1607ee63b993bc8dee598b4a9d4b1ec7673 Mon Sep 17 00:00:00 2001 From: Michael Nef Date: Tue, 5 Jul 2022 16:14:20 +0800 Subject: [PATCH 02/89] Created general card comopnent --- frontend/components/utils/BodyCard.tsx | 36 +++++++++++++++++++++++--- frontend/pages/reviewer.tsx | 2 +- 2 files changed, 33 insertions(+), 5 deletions(-) diff --git a/frontend/components/utils/BodyCard.tsx b/frontend/components/utils/BodyCard.tsx index 30b0284..25e40a9 100644 --- a/frontend/components/utils/BodyCard.tsx +++ b/frontend/components/utils/BodyCard.tsx @@ -4,13 +4,41 @@ import { Card, Container } from '@mui/material' type BodyCardProps = { children?: React.ReactNode; + header?: String; } -const BodyCard = ({children}: BodyCardProps): JSX.Element => { - return ( +const BodyCard = ({children, header}: BodyCardProps): JSX.Element => { + const headerComponent = header ? ( + + {header} + + ) : null; + return ( - - {children} + {headerComponent} + + +
+ {children} +
+
); diff --git a/frontend/pages/reviewer.tsx b/frontend/pages/reviewer.tsx index 76c8b4c..a9337b4 100644 --- a/frontend/pages/reviewer.tsx +++ b/frontend/pages/reviewer.tsx @@ -11,7 +11,7 @@ import { Card } from '@mui/material'; const Reviewer: NextPage = () => { return (
- +
swag
From 95a92c940fc41edb9b41d6096ab891551e036bb2 Mon Sep 17 00:00:00 2001 From: Michael Nef Date: Fri, 8 Jul 2022 11:07:55 +0800 Subject: [PATCH 03/89] reviewer page general structure --- frontend/pages/reviewer.tsx | 13 ++++++++++--- 1 file changed, 10 insertions(+), 3 deletions(-) diff --git a/frontend/pages/reviewer.tsx b/frontend/pages/reviewer.tsx index a9337b4..f035c02 100644 --- a/frontend/pages/reviewer.tsx +++ b/frontend/pages/reviewer.tsx @@ -1,4 +1,7 @@ +import { useState } from 'react' + import type { NextPage } from 'next' + import Head from 'next/head' import Image from 'next/image' import Link from 'next/link' @@ -6,14 +9,18 @@ import styles from '../styles/Home.module.css' import BodyCard from '../components/utils/BodyCard' -import { Card } from '@mui/material'; +import { Card, Button } from '@mui/material'; const Reviewer: NextPage = () => { + + const showArchived = useState(false); + return (
- +
- swag + +
From a3d5fe9609f4e740612894c98050de8397b49356 Mon Sep 17 00:00:00 2001 From: Michael Nef Date: Fri, 8 Jul 2022 11:17:12 +0800 Subject: [PATCH 04/89] Added archive button toggle --- frontend/pages/reviewer.tsx | 9 +++++++-- 1 file changed, 7 insertions(+), 2 deletions(-) diff --git a/frontend/pages/reviewer.tsx b/frontend/pages/reviewer.tsx index f035c02..550cf25 100644 --- a/frontend/pages/reviewer.tsx +++ b/frontend/pages/reviewer.tsx @@ -11,15 +11,20 @@ import BodyCard from '../components/utils/BodyCard' import { Card, Button } from '@mui/material'; +import OngoingReviewsList from '../components/Reviewer/OngoingReviewsList.tsx' + + const Reviewer: NextPage = () => { - const showArchived = useState(false); + const [showArchived, setShowArchived] = useState(false); + const archivedButtonText = showArchived ? "Hide Archived" : "Show Archvied"; + const toggleArchived = (e) => setShowArchived(!showArchived); return (
- +
From dd10698e8d0c0221bdfba81ff642234bdc7a9972 Mon Sep 17 00:00:00 2001 From: Michael Nef Date: Mon, 18 Jul 2022 13:16:58 +0800 Subject: [PATCH 05/89] course evaluation tests --- .../course_evaluations/tests/tests_crud.py | 112 ------------------ .../tests/tests_permissions.py | 75 ------------ 2 files changed, 187 deletions(-) delete mode 100644 backend/course_evaluations/tests/tests_crud.py delete mode 100644 backend/course_evaluations/tests/tests_permissions.py diff --git a/backend/course_evaluations/tests/tests_crud.py b/backend/course_evaluations/tests/tests_crud.py deleted file mode 100644 index 8cbe3c7..0000000 --- a/backend/course_evaluations/tests/tests_crud.py +++ /dev/null @@ -1,112 +0,0 @@ -""" -This test file is focused on the crud functionality of the course evaluations. - -Note: Permission testing is not the focus of this test file. -""" -from django.urls import reverse -from rest_framework import status - -from course_evaluations.models import CourseEvaluation, EOCSet - - -def test_list_view_course_evaluation(api_client_with_credentials_return_user, make_course_evaluation): - """ - GIVEN: There are multiple course evaluation assigned to a user - WHEN: The endpoint for the course evaluation is called - THEN: The course evaluation data is returned successfully with only a couple of information (eg. EOC info is not returned) - """ - api_client, user = api_client_with_credentials_return_user() - course_evaluation_1 = make_course_evaluation(coordinators=[user]) - course_evaluation_2 = make_course_evaluation(coordinators=[user]) - - # This user should not be able to see this - make_course_evaluation(coordinators=[]) - - url = reverse("api-v1:course_evaluations:course-evaluations-list") - response = api_client.get(url) - - assert response.status_code == status.HTTP_200_OK - data = response.data - - # Check that we can find the two course evaluations - assert len(data) == 2 - assert data[0]["id"] == str(course_evaluation_1.id) - assert data[1]["id"] == str(course_evaluation_2.id) - - # Check the content of one of the course_evaluation - course_evaluation_from_endpoint = data[0] - assert course_evaluation_from_endpoint["id"] == str(course_evaluation_1.id) - assert course_evaluation_from_endpoint["unit_code"] == course_evaluation_1.unit_code - assert course_evaluation_from_endpoint["description"] == course_evaluation_1.description - - # Check the coordinator - assert len(course_evaluation_from_endpoint["coordinators"]) == 1 - coordinator = course_evaluation_from_endpoint["coordinators"][0] - - assert coordinator["id"] == user.id - assert coordinator["username"] == user.username - - # Check that there are certain fields that does not exist - assert "eoc_set" not in course_evaluation_from_endpoint - assert "eoc_set_id" not in course_evaluation_from_endpoint - - -def test_create_view_course_evaluation(setup_indeaa, api_client_with_credentials_return_user): - """ - GIVEN: As a user of the system - WHEN: I create a course evaluation - THEN: The course evaluation is created successfully - """ - api_client, user = api_client_with_credentials_return_user() - - url = reverse("api-v1:course_evaluations:course-evaluations-list") - data = { - "eoc_set_id": str(EOCSet.objects.first().id), - "unit_code": "TEST1002", - "description": "Test Creation of CourseEvaluation", - } - response = api_client.post(url, data) - - assert response.status_code == status.HTTP_201_CREATED - - # Check that the course evaluation is created - course_evaluation = CourseEvaluation.objects.first() - assert course_evaluation.unit_code == data["unit_code"] - assert course_evaluation.description == data["description"] - assert course_evaluation.eoc_set == EOCSet.objects.first() - - -def test_update_view_course_evaluation(api_client_with_credentials_return_user, make_course_evaluation): - """ - GIVEN: A course evaluation is created - WHEN: I update the course evaluation - THEN: The course evaluation is updated successfully - """ - api_client, user = api_client_with_credentials_return_user() - course_evaluation_1 = make_course_evaluation(coordinators=[user]) - - url = reverse("api-v1:course_evaluations:course-evaluations-detail", kwargs={"pk": course_evaluation_1.id}) - data = { - "description": "Test Update of CourseEvaluation", - } - response = api_client.patch(url, data) - - assert response.status_code == status.HTTP_200_OK - assert response.data["unit_code"] == course_evaluation_1.unit_code - assert response.data["description"] == data["description"] - - -def test_delete_view_course_evaluation(api_client_with_credentials_return_user, make_course_evaluation): - """ - GIVEN: A course evaluation is created - WHEN: I delete the course evaluation - THEN: The course evaluation cannot be deleted - """ - api_client, user = api_client_with_credentials_return_user() - course_evaluation_1 = make_course_evaluation(coordinators=[user]) - - url = reverse("api-v1:course_evaluations:course-evaluations-detail", kwargs={"pk": course_evaluation_1.id}) - response = api_client.delete(url) - - assert response.status_code == status.HTTP_405_METHOD_NOT_ALLOWED - assert CourseEvaluation.objects.count() == 1 diff --git a/backend/course_evaluations/tests/tests_permissions.py b/backend/course_evaluations/tests/tests_permissions.py deleted file mode 100644 index cfdb3fa..0000000 --- a/backend/course_evaluations/tests/tests_permissions.py +++ /dev/null @@ -1,75 +0,0 @@ -""" -This test file is focused on permission testing. This is to ensure that unauthorised access cannot use the API -""" -from django.urls import reverse -from rest_framework import status - -from course_evaluations.models import CourseEvaluation - - -def test_list_view_course_evaluation_anonymous(api_client_no_auth): - """ - GIVEN: The user is not authenticated - WHEN: the user tries to use the endpoint - THEN: The user is not authorised to use the endpoint - """ - url = reverse("api-v1:course_evaluations:course-evaluations-list") - response = api_client_no_auth.get(url) - - assert response.status_code == status.HTTP_401_UNAUTHORIZED - - -def test_create_view_course_evaluation_anonymous(api_client_no_auth): - """ - GIVEN: The user is not authenticated - WHEN: I create a course evaluation - THEN: The user is not authorised to use the endpoint - """ - url = reverse("api-v1:course_evaluations:course-evaluations-list") - data = { - "unit_code": "TEST", - "description": "Test Course Evaluation", - } - response = api_client_no_auth.post(url, data) - - assert response.status_code == status.HTTP_401_UNAUTHORIZED - assert CourseEvaluation.objects.count() == 0 - - -def test_update_view_course_evaluation_anonymous(api_client_no_auth, create_user, make_course_evaluation): - """ - GIVEN: The user is not authenticated - WHEN: I update a course evaluation - THEN: The user is not authorised to use the endpoint - """ - user = create_user() - course_evaluation = make_course_evaluation(coordinators=[user]) - - url = reverse( - "api-v1:course_evaluations:course-evaluation-detail", - kwargs={"pk": course_evaluation.id}, - ) - data = {"unit_code": "TEST"} - response = api_client_no_auth.put(url, data) - - assert response.status_code == status.HTTP_401_UNAUTHORIZED - assert CourseEvaluation.objects.count() == 1 - - -def test_delete_view_course_evaluation_anonymous(api_client_no_auth, create_user, make_course_evaluation): - """ - GIVEN: The user is not authenticated - WHEN: I delete a course evaluation - THEN: The user is not authorised to use the endpoint - """ - user = create_user() - course_evaluation = make_course_evaluation(coordinators=[user]) - - url = reverse( - "api-v1:course_evaluations:course-evaluation-detail", - kwargs={"pk": course_evaluation.id}, - ) - response = api_client_no_auth.delete(url) - - assert response.status_code == status.HTTP_401_UNAUTHORIZED - assert CourseEvaluation.objects.count() == 1 From 095244c7ac7ddfb04c2fd9b3cbbde942bb98372f Mon Sep 17 00:00:00 2001 From: Michael Nef Date: Tue, 19 Jul 2022 09:58:31 +0800 Subject: [PATCH 06/89] Revert "Added archive button toggle" This reverts commit a3d5fe9609f4e740612894c98050de8397b49356. --- frontend/pages/reviewer.tsx | 9 ++------- 1 file changed, 2 insertions(+), 7 deletions(-) diff --git a/frontend/pages/reviewer.tsx b/frontend/pages/reviewer.tsx index 550cf25..f035c02 100644 --- a/frontend/pages/reviewer.tsx +++ b/frontend/pages/reviewer.tsx @@ -11,20 +11,15 @@ import BodyCard from '../components/utils/BodyCard' import { Card, Button } from '@mui/material'; -import OngoingReviewsList from '../components/Reviewer/OngoingReviewsList.tsx' - - const Reviewer: NextPage = () => { - const [showArchived, setShowArchived] = useState(false); - const archivedButtonText = showArchived ? "Hide Archived" : "Show Archvied"; - const toggleArchived = (e) => setShowArchived(!showArchived); + const showArchived = useState(false); return (
- +
From ec5cca62eb57709ad214f92f084554b1d0dfc381 Mon Sep 17 00:00:00 2001 From: Michael Nef Date: Tue, 19 Jul 2022 09:58:55 +0800 Subject: [PATCH 07/89] Revert "Revert "Added archive button toggle"" This reverts commit 095244c7ac7ddfb04c2fd9b3cbbde942bb98372f. --- frontend/pages/reviewer.tsx | 9 +++++++-- 1 file changed, 7 insertions(+), 2 deletions(-) diff --git a/frontend/pages/reviewer.tsx b/frontend/pages/reviewer.tsx index f035c02..550cf25 100644 --- a/frontend/pages/reviewer.tsx +++ b/frontend/pages/reviewer.tsx @@ -11,15 +11,20 @@ import BodyCard from '../components/utils/BodyCard' import { Card, Button } from '@mui/material'; +import OngoingReviewsList from '../components/Reviewer/OngoingReviewsList.tsx' + + const Reviewer: NextPage = () => { - const showArchived = useState(false); + const [showArchived, setShowArchived] = useState(false); + const archivedButtonText = showArchived ? "Hide Archived" : "Show Archvied"; + const toggleArchived = (e) => setShowArchived(!showArchived); return (
- +
From c20f1a310aa75b81fa53b3563088b8d1cd4e0c1d Mon Sep 17 00:00:00 2001 From: Michael Nef Date: Tue, 19 Jul 2022 09:59:09 +0800 Subject: [PATCH 08/89] Revert "course evaluation tests" This reverts commit dd10698e8d0c0221bdfba81ff642234bdc7a9972. --- .../course_evaluations/tests/tests_crud.py | 112 ++++++++++++++++++ .../tests/tests_permissions.py | 75 ++++++++++++ 2 files changed, 187 insertions(+) create mode 100644 backend/course_evaluations/tests/tests_crud.py create mode 100644 backend/course_evaluations/tests/tests_permissions.py diff --git a/backend/course_evaluations/tests/tests_crud.py b/backend/course_evaluations/tests/tests_crud.py new file mode 100644 index 0000000..8cbe3c7 --- /dev/null +++ b/backend/course_evaluations/tests/tests_crud.py @@ -0,0 +1,112 @@ +""" +This test file is focused on the crud functionality of the course evaluations. + +Note: Permission testing is not the focus of this test file. +""" +from django.urls import reverse +from rest_framework import status + +from course_evaluations.models import CourseEvaluation, EOCSet + + +def test_list_view_course_evaluation(api_client_with_credentials_return_user, make_course_evaluation): + """ + GIVEN: There are multiple course evaluation assigned to a user + WHEN: The endpoint for the course evaluation is called + THEN: The course evaluation data is returned successfully with only a couple of information (eg. EOC info is not returned) + """ + api_client, user = api_client_with_credentials_return_user() + course_evaluation_1 = make_course_evaluation(coordinators=[user]) + course_evaluation_2 = make_course_evaluation(coordinators=[user]) + + # This user should not be able to see this + make_course_evaluation(coordinators=[]) + + url = reverse("api-v1:course_evaluations:course-evaluations-list") + response = api_client.get(url) + + assert response.status_code == status.HTTP_200_OK + data = response.data + + # Check that we can find the two course evaluations + assert len(data) == 2 + assert data[0]["id"] == str(course_evaluation_1.id) + assert data[1]["id"] == str(course_evaluation_2.id) + + # Check the content of one of the course_evaluation + course_evaluation_from_endpoint = data[0] + assert course_evaluation_from_endpoint["id"] == str(course_evaluation_1.id) + assert course_evaluation_from_endpoint["unit_code"] == course_evaluation_1.unit_code + assert course_evaluation_from_endpoint["description"] == course_evaluation_1.description + + # Check the coordinator + assert len(course_evaluation_from_endpoint["coordinators"]) == 1 + coordinator = course_evaluation_from_endpoint["coordinators"][0] + + assert coordinator["id"] == user.id + assert coordinator["username"] == user.username + + # Check that there are certain fields that does not exist + assert "eoc_set" not in course_evaluation_from_endpoint + assert "eoc_set_id" not in course_evaluation_from_endpoint + + +def test_create_view_course_evaluation(setup_indeaa, api_client_with_credentials_return_user): + """ + GIVEN: As a user of the system + WHEN: I create a course evaluation + THEN: The course evaluation is created successfully + """ + api_client, user = api_client_with_credentials_return_user() + + url = reverse("api-v1:course_evaluations:course-evaluations-list") + data = { + "eoc_set_id": str(EOCSet.objects.first().id), + "unit_code": "TEST1002", + "description": "Test Creation of CourseEvaluation", + } + response = api_client.post(url, data) + + assert response.status_code == status.HTTP_201_CREATED + + # Check that the course evaluation is created + course_evaluation = CourseEvaluation.objects.first() + assert course_evaluation.unit_code == data["unit_code"] + assert course_evaluation.description == data["description"] + assert course_evaluation.eoc_set == EOCSet.objects.first() + + +def test_update_view_course_evaluation(api_client_with_credentials_return_user, make_course_evaluation): + """ + GIVEN: A course evaluation is created + WHEN: I update the course evaluation + THEN: The course evaluation is updated successfully + """ + api_client, user = api_client_with_credentials_return_user() + course_evaluation_1 = make_course_evaluation(coordinators=[user]) + + url = reverse("api-v1:course_evaluations:course-evaluations-detail", kwargs={"pk": course_evaluation_1.id}) + data = { + "description": "Test Update of CourseEvaluation", + } + response = api_client.patch(url, data) + + assert response.status_code == status.HTTP_200_OK + assert response.data["unit_code"] == course_evaluation_1.unit_code + assert response.data["description"] == data["description"] + + +def test_delete_view_course_evaluation(api_client_with_credentials_return_user, make_course_evaluation): + """ + GIVEN: A course evaluation is created + WHEN: I delete the course evaluation + THEN: The course evaluation cannot be deleted + """ + api_client, user = api_client_with_credentials_return_user() + course_evaluation_1 = make_course_evaluation(coordinators=[user]) + + url = reverse("api-v1:course_evaluations:course-evaluations-detail", kwargs={"pk": course_evaluation_1.id}) + response = api_client.delete(url) + + assert response.status_code == status.HTTP_405_METHOD_NOT_ALLOWED + assert CourseEvaluation.objects.count() == 1 diff --git a/backend/course_evaluations/tests/tests_permissions.py b/backend/course_evaluations/tests/tests_permissions.py new file mode 100644 index 0000000..cfdb3fa --- /dev/null +++ b/backend/course_evaluations/tests/tests_permissions.py @@ -0,0 +1,75 @@ +""" +This test file is focused on permission testing. This is to ensure that unauthorised access cannot use the API +""" +from django.urls import reverse +from rest_framework import status + +from course_evaluations.models import CourseEvaluation + + +def test_list_view_course_evaluation_anonymous(api_client_no_auth): + """ + GIVEN: The user is not authenticated + WHEN: the user tries to use the endpoint + THEN: The user is not authorised to use the endpoint + """ + url = reverse("api-v1:course_evaluations:course-evaluations-list") + response = api_client_no_auth.get(url) + + assert response.status_code == status.HTTP_401_UNAUTHORIZED + + +def test_create_view_course_evaluation_anonymous(api_client_no_auth): + """ + GIVEN: The user is not authenticated + WHEN: I create a course evaluation + THEN: The user is not authorised to use the endpoint + """ + url = reverse("api-v1:course_evaluations:course-evaluations-list") + data = { + "unit_code": "TEST", + "description": "Test Course Evaluation", + } + response = api_client_no_auth.post(url, data) + + assert response.status_code == status.HTTP_401_UNAUTHORIZED + assert CourseEvaluation.objects.count() == 0 + + +def test_update_view_course_evaluation_anonymous(api_client_no_auth, create_user, make_course_evaluation): + """ + GIVEN: The user is not authenticated + WHEN: I update a course evaluation + THEN: The user is not authorised to use the endpoint + """ + user = create_user() + course_evaluation = make_course_evaluation(coordinators=[user]) + + url = reverse( + "api-v1:course_evaluations:course-evaluation-detail", + kwargs={"pk": course_evaluation.id}, + ) + data = {"unit_code": "TEST"} + response = api_client_no_auth.put(url, data) + + assert response.status_code == status.HTTP_401_UNAUTHORIZED + assert CourseEvaluation.objects.count() == 1 + + +def test_delete_view_course_evaluation_anonymous(api_client_no_auth, create_user, make_course_evaluation): + """ + GIVEN: The user is not authenticated + WHEN: I delete a course evaluation + THEN: The user is not authorised to use the endpoint + """ + user = create_user() + course_evaluation = make_course_evaluation(coordinators=[user]) + + url = reverse( + "api-v1:course_evaluations:course-evaluation-detail", + kwargs={"pk": course_evaluation.id}, + ) + response = api_client_no_auth.delete(url) + + assert response.status_code == status.HTTP_401_UNAUTHORIZED + assert CourseEvaluation.objects.count() == 1 From 09fd8502618bd976021fdb110991ebc0c96d3b32 Mon Sep 17 00:00:00 2001 From: Michael Nef Date: Tue, 19 Jul 2022 10:01:42 +0800 Subject: [PATCH 09/89] fixed evaluation tests --- backend/course_evaluations/tests/tests_permissions.py | 9 +++++++-- backend/pytest.ini | 4 ++-- 2 files changed, 9 insertions(+), 4 deletions(-) diff --git a/backend/course_evaluations/tests/tests_permissions.py b/backend/course_evaluations/tests/tests_permissions.py index cfdb3fa..c16bc4e 100644 --- a/backend/course_evaluations/tests/tests_permissions.py +++ b/backend/course_evaluations/tests/tests_permissions.py @@ -5,8 +5,10 @@ from rest_framework import status from course_evaluations.models import CourseEvaluation +import pytest +@pytest.mark.django_db def test_list_view_course_evaluation_anonymous(api_client_no_auth): """ GIVEN: The user is not authenticated @@ -19,6 +21,7 @@ def test_list_view_course_evaluation_anonymous(api_client_no_auth): assert response.status_code == status.HTTP_401_UNAUTHORIZED +@pytest.mark.django_db def test_create_view_course_evaluation_anonymous(api_client_no_auth): """ GIVEN: The user is not authenticated @@ -36,6 +39,7 @@ def test_create_view_course_evaluation_anonymous(api_client_no_auth): assert CourseEvaluation.objects.count() == 0 +@pytest.mark.django_db def test_update_view_course_evaluation_anonymous(api_client_no_auth, create_user, make_course_evaluation): """ GIVEN: The user is not authenticated @@ -46,7 +50,7 @@ def test_update_view_course_evaluation_anonymous(api_client_no_auth, create_user course_evaluation = make_course_evaluation(coordinators=[user]) url = reverse( - "api-v1:course_evaluations:course-evaluation-detail", + "api-v1:course_evaluations:course-evaluations-detail", kwargs={"pk": course_evaluation.id}, ) data = {"unit_code": "TEST"} @@ -56,6 +60,7 @@ def test_update_view_course_evaluation_anonymous(api_client_no_auth, create_user assert CourseEvaluation.objects.count() == 1 +@pytest.mark.django_db def test_delete_view_course_evaluation_anonymous(api_client_no_auth, create_user, make_course_evaluation): """ GIVEN: The user is not authenticated @@ -66,7 +71,7 @@ def test_delete_view_course_evaluation_anonymous(api_client_no_auth, create_user course_evaluation = make_course_evaluation(coordinators=[user]) url = reverse( - "api-v1:course_evaluations:course-evaluation-detail", + "api-v1:course_evaluations:course-evaluations-detail", kwargs={"pk": course_evaluation.id}, ) response = api_client_no_auth.delete(url) diff --git a/backend/pytest.ini b/backend/pytest.ini index 3a83a6f..4eb0bf6 100644 --- a/backend/pytest.ini +++ b/backend/pytest.ini @@ -1,7 +1,7 @@ [pytest] DJANGO_SETTINGS_MODULE=config.settings.ci -python_files = tests.py test_*.py *_tests.py +python_files = tests.py test_*.py *_tests.py tests_*.py ; Automatic Process distribution of Test Files to CPU cores addopts = --strict-markers markers = - v1_0_0: All tests introduced in version 1.0.0 and previous versions \ No newline at end of file + v1_0_0: All tests introduced in version 1.0.0 and previous versions From e23e9ae7a988d10d55c96ca02683d9ffa164ac5a Mon Sep 17 00:00:00 2001 From: Michael Nef Date: Tue, 19 Jul 2022 11:24:12 +0800 Subject: [PATCH 10/89] created reviews app --- backend/reviews/__init__.py | 0 backend/reviews/admin.py | 3 +++ backend/reviews/apps.py | 6 ++++++ backend/reviews/migrations/__init__.py | 0 backend/reviews/models.py | 3 +++ backend/reviews/tests.py | 3 +++ backend/reviews/views.py | 3 +++ 7 files changed, 18 insertions(+) create mode 100644 backend/reviews/__init__.py create mode 100644 backend/reviews/admin.py create mode 100644 backend/reviews/apps.py create mode 100644 backend/reviews/migrations/__init__.py create mode 100644 backend/reviews/models.py create mode 100644 backend/reviews/tests.py create mode 100644 backend/reviews/views.py diff --git a/backend/reviews/__init__.py b/backend/reviews/__init__.py new file mode 100644 index 0000000..e69de29 diff --git a/backend/reviews/admin.py b/backend/reviews/admin.py new file mode 100644 index 0000000..8c38f3f --- /dev/null +++ b/backend/reviews/admin.py @@ -0,0 +1,3 @@ +from django.contrib import admin + +# Register your models here. diff --git a/backend/reviews/apps.py b/backend/reviews/apps.py new file mode 100644 index 0000000..80e0186 --- /dev/null +++ b/backend/reviews/apps.py @@ -0,0 +1,6 @@ +from django.apps import AppConfig + + +class ReviewsConfig(AppConfig): + default_auto_field = 'django.db.models.BigAutoField' + name = 'reviews' diff --git a/backend/reviews/migrations/__init__.py b/backend/reviews/migrations/__init__.py new file mode 100644 index 0000000..e69de29 diff --git a/backend/reviews/models.py b/backend/reviews/models.py new file mode 100644 index 0000000..71a8362 --- /dev/null +++ b/backend/reviews/models.py @@ -0,0 +1,3 @@ +from django.db import models + +# Create your models here. diff --git a/backend/reviews/tests.py b/backend/reviews/tests.py new file mode 100644 index 0000000..7ce503c --- /dev/null +++ b/backend/reviews/tests.py @@ -0,0 +1,3 @@ +from django.test import TestCase + +# Create your tests here. diff --git a/backend/reviews/views.py b/backend/reviews/views.py new file mode 100644 index 0000000..91ea44a --- /dev/null +++ b/backend/reviews/views.py @@ -0,0 +1,3 @@ +from django.shortcuts import render + +# Create your views here. From a7d544c68dca3f4816e9a84fa0361b11c9707749 Mon Sep 17 00:00:00 2001 From: Michael Nef Date: Tue, 19 Jul 2022 12:08:31 +0800 Subject: [PATCH 11/89] moved reviews model to new app --- backend/config/settings/base.py | 1 + backend/config/urls.py | 7 ++++++ backend/course_evaluations/models.py | 1 + backend/course_evaluations/serializers.py | 1 + backend/reviews/__init__.py | 0 backend/reviews/admin.py | 5 +++++ backend/reviews/apps.py | 0 backend/reviews/models.py | 26 ++++++++++++++++++++++- backend/reviews/serializers.py | 8 +++++++ backend/reviews/tests.py | 0 backend/reviews/urls.py | 13 ++++++++++++ backend/reviews/views.py | 16 +++++++++++++- 12 files changed, 76 insertions(+), 2 deletions(-) mode change 100644 => 100755 backend/reviews/__init__.py mode change 100644 => 100755 backend/reviews/admin.py mode change 100644 => 100755 backend/reviews/apps.py mode change 100644 => 100755 backend/reviews/models.py create mode 100644 backend/reviews/serializers.py mode change 100644 => 100755 backend/reviews/tests.py create mode 100644 backend/reviews/urls.py mode change 100644 => 100755 backend/reviews/views.py diff --git a/backend/config/settings/base.py b/backend/config/settings/base.py index 0ebd6b8..8c272e6 100644 --- a/backend/config/settings/base.py +++ b/backend/config/settings/base.py @@ -69,6 +69,7 @@ "django_filters", "commands", "course_evaluations", + "reviews", ] # Refer to https://dj-rest-auth.readthedocs.io/en/latest/installation.html#registration-optional diff --git a/backend/config/urls.py b/backend/config/urls.py index 50a5b34..9a7376f 100644 --- a/backend/config/urls.py +++ b/backend/config/urls.py @@ -11,6 +11,13 @@ namespace="course_evaluations", # use this namespace for url reversal ), ), + path( + "reviews/", + include( + ("reviews.urls", "reviews"), + namespace="reviews", + ), + ), path("authentication/", include("dj_rest_auth.urls")), ], "api", diff --git a/backend/course_evaluations/models.py b/backend/course_evaluations/models.py index 9c4b1df..f9b88fc 100644 --- a/backend/course_evaluations/models.py +++ b/backend/course_evaluations/models.py @@ -80,3 +80,4 @@ class CourseEvaluation(models.Model): def __str__(self): return f"{self.eoc_set.name} - {self.unit_code} ({self.created_at})" + diff --git a/backend/course_evaluations/serializers.py b/backend/course_evaluations/serializers.py index a2386df..48a131d 100644 --- a/backend/course_evaluations/serializers.py +++ b/backend/course_evaluations/serializers.py @@ -54,3 +54,4 @@ class CourseEvaluationDetailSerializer(serializers.ModelSerializer): class Meta: model = CourseEvaluation fields = "__all__" + diff --git a/backend/reviews/__init__.py b/backend/reviews/__init__.py old mode 100644 new mode 100755 diff --git a/backend/reviews/admin.py b/backend/reviews/admin.py old mode 100644 new mode 100755 index 8c38f3f..ec81bac --- a/backend/reviews/admin.py +++ b/backend/reviews/admin.py @@ -1,3 +1,8 @@ from django.contrib import admin +from reviews.models import Review # Register your models here. +@admin.register(Review) +class ReviewAdmin(admin.ModelAdmin): + list_display = ("id", "course_evaluation", "final_comment", "date_submitted") + ordering = ("created_at",) diff --git a/backend/reviews/apps.py b/backend/reviews/apps.py old mode 100644 new mode 100755 diff --git a/backend/reviews/models.py b/backend/reviews/models.py old mode 100644 new mode 100755 index 71a8362..e94a6eb --- a/backend/reviews/models.py +++ b/backend/reviews/models.py @@ -1,3 +1,27 @@ -from django.db import models +import uuid # Create your models here. +from django.db import models +from course_evaluations.models import CourseEvaluation + + +class Review(models.Model): + """ + A review is what Reviewers make. There are many reviews per + course evaluation, one per reviewer. + """ + + # UUID for the review + id = models.UUIDField(primary_key=True, default=uuid.uuid4, editable=False) + + # One-to-many Relationship with Course Evaluations + course_evaluation = models.ForeignKey(CourseEvaluation, on_delete=models.CASCADE) + + coordinators = models.ForeignKey("auth.User", related_name="reviews", on_delete=models.CASCADE) + + final_comment = models.TextField(null=False, blank=True) + + date_submitted = models.DateTimeField() + + created_at = models.DateTimeField(auto_now_add=True) + updated_at = models.DateTimeField(auto_now=True) diff --git a/backend/reviews/serializers.py b/backend/reviews/serializers.py new file mode 100644 index 0000000..20ec6dc --- /dev/null +++ b/backend/reviews/serializers.py @@ -0,0 +1,8 @@ +from rest_framework import serializers +from reviews.models import Review + +class ReviewSerializer(serializers.ModelSerializer): + class Meta: + model = Review + fields = "__all__" + diff --git a/backend/reviews/tests.py b/backend/reviews/tests.py old mode 100644 new mode 100755 diff --git a/backend/reviews/urls.py b/backend/reviews/urls.py new file mode 100644 index 0000000..d2c4d70 --- /dev/null +++ b/backend/reviews/urls.py @@ -0,0 +1,13 @@ +from django.urls import include, path +from rest_framework.routers import DefaultRouter + +from reviews.views import ReviewsViewSet + +# Create a router and register our viewsets with it. +router = DefaultRouter() +router.register(r"", ReviewsViewSet, basename="reviews") + +# The API URLs are now determined automatically by the router. +urlpatterns = [ + path("", include(router.urls)), +] diff --git a/backend/reviews/views.py b/backend/reviews/views.py old mode 100644 new mode 100755 index 91ea44a..6cab2f3 --- a/backend/reviews/views.py +++ b/backend/reviews/views.py @@ -1,3 +1,17 @@ -from django.shortcuts import render +from rest_framework import permissions, status, viewsets +from rest_framework.exceptions import ValidationError +from rest_framework.response import Response + +from reviews.models import Review +from reviews.serializers import ReviewSerializer # Create your views here. + +class ReviewsViewSet(viewsets.ModelViewSet): + """ + Viewset that handles the following + """ + + queryset = Review.objects.all() + def get_serializer(self, *args, **kwargs): + return ReviewSerializer(*args, **kwargs) From 59fc0467915f5c9f7add3ded79393d92a498626e Mon Sep 17 00:00:00 2001 From: Michael Nef Date: Tue, 19 Jul 2022 12:12:26 +0800 Subject: [PATCH 12/89] created documents app --- backend/documents/__init__.py | 0 backend/documents/admin.py | 3 +++ backend/documents/apps.py | 6 ++++++ backend/documents/migrations/__init__.py | 0 backend/documents/models.py | 3 +++ backend/documents/tests.py | 3 +++ backend/documents/views.py | 3 +++ 7 files changed, 18 insertions(+) create mode 100644 backend/documents/__init__.py create mode 100644 backend/documents/admin.py create mode 100644 backend/documents/apps.py create mode 100644 backend/documents/migrations/__init__.py create mode 100644 backend/documents/models.py create mode 100644 backend/documents/tests.py create mode 100644 backend/documents/views.py diff --git a/backend/documents/__init__.py b/backend/documents/__init__.py new file mode 100644 index 0000000..e69de29 diff --git a/backend/documents/admin.py b/backend/documents/admin.py new file mode 100644 index 0000000..8c38f3f --- /dev/null +++ b/backend/documents/admin.py @@ -0,0 +1,3 @@ +from django.contrib import admin + +# Register your models here. diff --git a/backend/documents/apps.py b/backend/documents/apps.py new file mode 100644 index 0000000..ab8a093 --- /dev/null +++ b/backend/documents/apps.py @@ -0,0 +1,6 @@ +from django.apps import AppConfig + + +class DocumentsConfig(AppConfig): + default_auto_field = 'django.db.models.BigAutoField' + name = 'documents' diff --git a/backend/documents/migrations/__init__.py b/backend/documents/migrations/__init__.py new file mode 100644 index 0000000..e69de29 diff --git a/backend/documents/models.py b/backend/documents/models.py new file mode 100644 index 0000000..71a8362 --- /dev/null +++ b/backend/documents/models.py @@ -0,0 +1,3 @@ +from django.db import models + +# Create your models here. diff --git a/backend/documents/tests.py b/backend/documents/tests.py new file mode 100644 index 0000000..7ce503c --- /dev/null +++ b/backend/documents/tests.py @@ -0,0 +1,3 @@ +from django.test import TestCase + +# Create your tests here. diff --git a/backend/documents/views.py b/backend/documents/views.py new file mode 100644 index 0000000..91ea44a --- /dev/null +++ b/backend/documents/views.py @@ -0,0 +1,3 @@ +from django.shortcuts import render + +# Create your views here. From e951949f07ac535d97cb9b699a4aacacb145b3f9 Mon Sep 17 00:00:00 2001 From: Michael Nef Date: Tue, 19 Jul 2022 15:47:56 +0800 Subject: [PATCH 13/89] added documents app --- backend/config/settings/base.py | 1 + backend/config/urls.py | 7 +++++++ backend/course_evaluations/admin.py | 1 - backend/documents/admin.py | 5 +++++ backend/documents/models.py | 22 ++++++++++++++++++++++ backend/documents/serializers.py | 10 ++++++++++ backend/documents/urls.py | 14 ++++++++++++++ backend/documents/views.py | 19 ++++++++++++++++++- 8 files changed, 77 insertions(+), 2 deletions(-) create mode 100644 backend/documents/serializers.py create mode 100644 backend/documents/urls.py diff --git a/backend/config/settings/base.py b/backend/config/settings/base.py index 8c272e6..972bc86 100644 --- a/backend/config/settings/base.py +++ b/backend/config/settings/base.py @@ -70,6 +70,7 @@ "commands", "course_evaluations", "reviews", + "documents", ] # Refer to https://dj-rest-auth.readthedocs.io/en/latest/installation.html#registration-optional diff --git a/backend/config/urls.py b/backend/config/urls.py index 9a7376f..fbd4fc4 100644 --- a/backend/config/urls.py +++ b/backend/config/urls.py @@ -18,6 +18,13 @@ namespace="reviews", ), ), + path( + "documents/", + include( + ("documents.urls", "documents"), + namespace="documents", + ) + ), path("authentication/", include("dj_rest_auth.urls")), ], "api", diff --git a/backend/course_evaluations/admin.py b/backend/course_evaluations/admin.py index 34f5a75..37a0394 100644 --- a/backend/course_evaluations/admin.py +++ b/backend/course_evaluations/admin.py @@ -1,5 +1,4 @@ from django.contrib import admin - from course_evaluations.models import CourseEvaluation, EOCGeneral, EOCSet, EOCSpecific diff --git a/backend/documents/admin.py b/backend/documents/admin.py index 8c38f3f..8df77a0 100644 --- a/backend/documents/admin.py +++ b/backend/documents/admin.py @@ -1,3 +1,8 @@ from django.contrib import admin +from documents.models import Document # Register your models here. + +@admin.register(Document) +class DocumentAdmin(admin.ModelAdmin): + list_display = ("id", "url", "is_introduction") diff --git a/backend/documents/models.py b/backend/documents/models.py index 71a8362..f44017c 100644 --- a/backend/documents/models.py +++ b/backend/documents/models.py @@ -1,3 +1,25 @@ +import uuid from django.db import models # Create your models here. +from course_evaluations.models import EOCSpecific, EOCGeneral + +class Document(models.Model): + """ + A document is a url to a resource that a reviewer will look at + to judge the quality of a course + """ + + # UUID for the review + id = models.UUIDField(primary_key=True, default=uuid.uuid4, editable=False) + + is_introduction = models.BooleanField() + + # The actual URL to the resource + url = models.URLField() + + eoc_generals = models.ManyToManyField(EOCGeneral) + eoc_specifics = models.ManyToManyField(EOCSpecific) + + created_at = models.DateTimeField(auto_now_add=True) + updated_at = models.DateTimeField(auto_now=True) diff --git a/backend/documents/serializers.py b/backend/documents/serializers.py new file mode 100644 index 0000000..ada6729 --- /dev/null +++ b/backend/documents/serializers.py @@ -0,0 +1,10 @@ +from rest_framework import serializers + +from documents.models import Document + +class DocumentSerializer(serializers.ModelSerializer): + class Meta: + model = Document + fields = "__all__" + + diff --git a/backend/documents/urls.py b/backend/documents/urls.py new file mode 100644 index 0000000..b8b86f1 --- /dev/null +++ b/backend/documents/urls.py @@ -0,0 +1,14 @@ +from django.urls import include, path +from rest_framework.routers import DefaultRouter + +from documents.views import DocumentsViewSet + +# Create a router and register our viewsets with it. +router = DefaultRouter() +router.register(r"", DocumentsViewSet, basename="documents") + +# The API URLs are now determined automatically by the router. +urlpatterns = [ + path("", include(router.urls)), +] + diff --git a/backend/documents/views.py b/backend/documents/views.py index 91ea44a..aac27ac 100644 --- a/backend/documents/views.py +++ b/backend/documents/views.py @@ -1,3 +1,20 @@ -from django.shortcuts import render +from rest_framework import permissions, status, viewsets +from rest_framework.exceptions import ValidationError +from rest_framework.response import Response +from documents.models import Document +from documents.serializers import DocumentSerializer # Create your views here. + + +class DocumentsViewSet(viewsets.ModelViewSet): + """ + Viewset that handles documents + """ + + queryset = Document.objects.all() + def get_serializer(self, *args, **kwargs): + """ + + """ + return DocumentSerializer(*args, **kwargs) \ No newline at end of file From 7fd77f21eccca4e769f40940c9b6a7c8a788f0ca Mon Sep 17 00:00:00 2001 From: Michael Nef Date: Tue, 19 Jul 2022 15:49:12 +0800 Subject: [PATCH 14/89] added documents migration --- backend/documents/migrations/0001_initial.py | 28 ++++++++++++++++++++ 1 file changed, 28 insertions(+) create mode 100644 backend/documents/migrations/0001_initial.py diff --git a/backend/documents/migrations/0001_initial.py b/backend/documents/migrations/0001_initial.py new file mode 100644 index 0000000..6a316e5 --- /dev/null +++ b/backend/documents/migrations/0001_initial.py @@ -0,0 +1,28 @@ +# Generated by Django 3.2.13 on 2022-07-19 07:45 + +from django.db import migrations, models +import uuid + + +class Migration(migrations.Migration): + + initial = True + + dependencies = [ + ('course_evaluations', '0001_initial'), + ] + + operations = [ + migrations.CreateModel( + name='Document', + fields=[ + ('id', models.UUIDField(default=uuid.uuid4, editable=False, primary_key=True, serialize=False)), + ('is_introduction', models.BooleanField()), + ('url', models.URLField()), + ('created_at', models.DateTimeField(auto_now_add=True)), + ('updated_at', models.DateTimeField(auto_now=True)), + ('eoc_generals', models.ManyToManyField(to='course_evaluations.EOCGeneral')), + ('eoc_specifics', models.ManyToManyField(to='course_evaluations.EOCSpecific')), + ], + ), + ] From 18a0aabddea5a0414b2f61450098287d7a5e6542 Mon Sep 17 00:00:00 2001 From: Michael Nef Date: Tue, 19 Jul 2022 15:49:36 +0800 Subject: [PATCH 15/89] added model for revieweocspecific --- backend/reviews/migrations/0001_initial.py | 40 ++++++++++++++++++++++ backend/reviews/models.py | 17 ++++++++- 2 files changed, 56 insertions(+), 1 deletion(-) create mode 100644 backend/reviews/migrations/0001_initial.py diff --git a/backend/reviews/migrations/0001_initial.py b/backend/reviews/migrations/0001_initial.py new file mode 100644 index 0000000..e56e852 --- /dev/null +++ b/backend/reviews/migrations/0001_initial.py @@ -0,0 +1,40 @@ +# Generated by Django 3.2.13 on 2022-07-19 07:45 + +from django.conf import settings +from django.db import migrations, models +import django.db.models.deletion +import uuid + + +class Migration(migrations.Migration): + + initial = True + + dependencies = [ + ('course_evaluations', '0001_initial'), + migrations.swappable_dependency(settings.AUTH_USER_MODEL), + ] + + operations = [ + migrations.CreateModel( + name='ReviewEocSpecific', + fields=[ + ('id', models.UUIDField(default=uuid.uuid4, editable=False, primary_key=True, serialize=False)), + ('suggestion', models.TextField(blank=True)), + ('justification', models.TextField(blank=True)), + ('eoc_specific', models.ForeignKey(on_delete=django.db.models.deletion.CASCADE, to='course_evaluations.eocspecific')), + ], + ), + migrations.CreateModel( + name='Review', + fields=[ + ('id', models.UUIDField(default=uuid.uuid4, editable=False, primary_key=True, serialize=False)), + ('final_comment', models.TextField(blank=True)), + ('date_submitted', models.DateTimeField()), + ('created_at', models.DateTimeField(auto_now_add=True)), + ('updated_at', models.DateTimeField(auto_now=True)), + ('coordinators', models.ForeignKey(on_delete=django.db.models.deletion.CASCADE, related_name='reviews', to=settings.AUTH_USER_MODEL)), + ('course_evaluation', models.ForeignKey(on_delete=django.db.models.deletion.CASCADE, to='course_evaluations.courseevaluation')), + ], + ), + ] diff --git a/backend/reviews/models.py b/backend/reviews/models.py index e94a6eb..7947ce4 100755 --- a/backend/reviews/models.py +++ b/backend/reviews/models.py @@ -2,7 +2,7 @@ # Create your models here. from django.db import models -from course_evaluations.models import CourseEvaluation +from course_evaluations.models import CourseEvaluation, EOCSpecific class Review(models.Model): @@ -25,3 +25,18 @@ class Review(models.Model): created_at = models.DateTimeField(auto_now_add=True) updated_at = models.DateTimeField(auto_now=True) + + +class ReviewEocSpecific(models.Model): + """ + For each eoc in a review, they each need specific infomration filled out. + """ + + id = models.UUIDField(primary_key=True, default=uuid.uuid4, editable=False) + + eoc_specific = models.ForeignKey(EOCSpecific, on_delete=models.CASCADE) + + development_level = models.IntegerChoices("DevelopmentLevel", "1 2 3 4 5") + + suggestion = models.TextField(null=False, blank=True) + justification = models.TextField(null=False, blank=True) From a585a1fc4dab67cad8ab9be2a3d671174a756c3e Mon Sep 17 00:00:00 2001 From: Michael Nef Date: Tue, 19 Jul 2022 16:23:37 +0800 Subject: [PATCH 16/89] added document reviews and eoc specific reviews --- backend/reviews/admin.py | 15 ++++++++++++++- backend/reviews/models.py | 24 +++++++++++++++++++++++- backend/reviews/serializers.py | 13 ++++++++++++- backend/reviews/views.py | 25 +++++++++++++++++++++++-- 4 files changed, 72 insertions(+), 5 deletions(-) diff --git a/backend/reviews/admin.py b/backend/reviews/admin.py index ec81bac..e53148a 100755 --- a/backend/reviews/admin.py +++ b/backend/reviews/admin.py @@ -1,8 +1,21 @@ from django.contrib import admin -from reviews.models import Review +from reviews.models import Review, ReviewDocument, ReviewEocSpecific # Register your models here. @admin.register(Review) class ReviewAdmin(admin.ModelAdmin): list_display = ("id", "course_evaluation", "final_comment", "date_submitted") ordering = ("created_at",) + + +@admin.register(ReviewDocument) +class ReviewDocumentAdmin(admin.ModelAdmin): + list_display = ("id", "review", "document", "is_viewed", "comment") + ordering = ("created_at",) + + +@admin.register(ReviewEocSpecific) +class ReviewEocSpecificAdmin(admin.ModelAdmin): + list_display = ("id", "review", "eoc_specific", "development_level", "suggestion", "justification") + ordering = ("created_at",) + diff --git a/backend/reviews/models.py b/backend/reviews/models.py index 7947ce4..50994d3 100755 --- a/backend/reviews/models.py +++ b/backend/reviews/models.py @@ -1,9 +1,11 @@ +from re import I import uuid # Create your models here. from django.db import models from course_evaluations.models import CourseEvaluation, EOCSpecific +from documents.models import Document class Review(models.Model): """ @@ -33,10 +35,30 @@ class ReviewEocSpecific(models.Model): """ id = models.UUIDField(primary_key=True, default=uuid.uuid4, editable=False) - + review = models.ForeignKey(Review, null=True, related_name="eoc_specific_reviews", on_delete=models.CASCADE) eoc_specific = models.ForeignKey(EOCSpecific, on_delete=models.CASCADE) development_level = models.IntegerChoices("DevelopmentLevel", "1 2 3 4 5") suggestion = models.TextField(null=False, blank=True) justification = models.TextField(null=False, blank=True) + + created_at = models.DateTimeField(auto_now_add=True) + updated_at = models.DateTimeField(auto_now=True) + + + +class ReviewDocument(models.Model): + """ + For each review, the reviewer may supply comments on different documents. + ReviewDocument contains information regarding these comments. + """ + id = models.UUIDField(primary_key=True, default=uuid.uuid4, editable=False) + review = models.ForeignKey(Review, related_name="documents", on_delete=models.CASCADE) + document = models.ForeignKey(Document, related_name="review_messages", on_delete=models.CASCADE) + is_viewed = models.BooleanField() + comment = models.TextField() + + created_at = models.DateTimeField(auto_now_add=True) + updated_at = models.DateTimeField(auto_now=True) + diff --git a/backend/reviews/serializers.py b/backend/reviews/serializers.py index 20ec6dc..13e044c 100644 --- a/backend/reviews/serializers.py +++ b/backend/reviews/serializers.py @@ -1,8 +1,19 @@ from rest_framework import serializers -from reviews.models import Review +from reviews.models import Review, ReviewEocSpecific, ReviewDocument class ReviewSerializer(serializers.ModelSerializer): class Meta: model = Review fields = "__all__" + +class ReviewEOCSpecificSerializer(serializers.ModelSerializer): + class Meta: + model = ReviewEocSpecific + fields = "__all__" + + +class ReviewDocumentSerializer(serializers.ModelSerializer): + class Meta: + model = ReviewDocument + fields = "__all__" \ No newline at end of file diff --git a/backend/reviews/views.py b/backend/reviews/views.py index 6cab2f3..64e5b46 100755 --- a/backend/reviews/views.py +++ b/backend/reviews/views.py @@ -2,8 +2,8 @@ from rest_framework.exceptions import ValidationError from rest_framework.response import Response -from reviews.models import Review -from reviews.serializers import ReviewSerializer +from reviews.models import Review, ReviewDocument, ReviewEocSpecific +from reviews.serializers import ReviewSerializer, ReviewDocumentSerializer, ReviewEOCSpecificSerializer # Create your views here. @@ -15,3 +15,24 @@ class ReviewsViewSet(viewsets.ModelViewSet): queryset = Review.objects.all() def get_serializer(self, *args, **kwargs): return ReviewSerializer(*args, **kwargs) + + +class ReviewDocumentViewSet(viewsets.ModelViewSet): + """ + Viewset that handles the following + """ + + queryset = ReviewDocument.objects.all() + def get_serializer(self, *args, **kwargs): + return ReviewDocumentSerializer(*args, **kwargs) + + +class ReviewEocSpecificViewSet(viewsets.ModelViewSet): + """ + Viewset that handles the following + """ + + queryset = ReviewEocSpecific.objects.all() + def get_serializer(self, *args, **kwargs): + return ReviewEOCSpecificSerializer(*args, **kwargs) + From f9303dbd73fcf3d6bf09ba581873728c9cb842e3 Mon Sep 17 00:00:00 2001 From: Michael Nef Date: Tue, 19 Jul 2022 16:23:57 +0800 Subject: [PATCH 17/89] document and eocspecific review migrations --- .../migrations/0002_auto_20220719_1622.py | 45 +++++++++++++++++++ 1 file changed, 45 insertions(+) create mode 100644 backend/reviews/migrations/0002_auto_20220719_1622.py diff --git a/backend/reviews/migrations/0002_auto_20220719_1622.py b/backend/reviews/migrations/0002_auto_20220719_1622.py new file mode 100644 index 0000000..907fbae --- /dev/null +++ b/backend/reviews/migrations/0002_auto_20220719_1622.py @@ -0,0 +1,45 @@ +# Generated by Django 3.2.13 on 2022-07-19 08:22 + +from django.db import migrations, models +import django.db.models.deletion +import django.utils.timezone +import uuid + + +class Migration(migrations.Migration): + + dependencies = [ + ('documents', '0001_initial'), + ('reviews', '0001_initial'), + ] + + operations = [ + migrations.AddField( + model_name='revieweocspecific', + name='created_at', + field=models.DateTimeField(auto_now_add=True, default=django.utils.timezone.now), + preserve_default=False, + ), + migrations.AddField( + model_name='revieweocspecific', + name='review', + field=models.ForeignKey(null=True, on_delete=django.db.models.deletion.CASCADE, related_name='eoc_specific_reviews', to='reviews.review'), + ), + migrations.AddField( + model_name='revieweocspecific', + name='updated_at', + field=models.DateTimeField(auto_now=True), + ), + migrations.CreateModel( + name='ReviewDocument', + fields=[ + ('id', models.UUIDField(default=uuid.uuid4, editable=False, primary_key=True, serialize=False)), + ('is_viewed', models.BooleanField()), + ('comment', models.TextField()), + ('created_at', models.DateTimeField(auto_now_add=True)), + ('updated_at', models.DateTimeField(auto_now=True)), + ('document', models.ForeignKey(on_delete=django.db.models.deletion.CASCADE, related_name='review_messages', to='documents.document')), + ('review', models.ForeignKey(on_delete=django.db.models.deletion.CASCADE, related_name='documents', to='reviews.review')), + ], + ), + ] From c68f4988fd49656871758f3e90298b7ee79506ad Mon Sep 17 00:00:00 2001 From: Michael Nef Date: Tue, 5 Jul 2022 14:56:45 +0800 Subject: [PATCH 18/89] basic body card --- frontend/components/utils/BodyCard.tsx | 19 +++++++++++++++++++ frontend/pages/reviewer.tsx | 23 +++++++++++++++++++++++ 2 files changed, 42 insertions(+) create mode 100644 frontend/components/utils/BodyCard.tsx create mode 100644 frontend/pages/reviewer.tsx diff --git a/frontend/components/utils/BodyCard.tsx b/frontend/components/utils/BodyCard.tsx new file mode 100644 index 0000000..30b0284 --- /dev/null +++ b/frontend/components/utils/BodyCard.tsx @@ -0,0 +1,19 @@ +import * as React from 'react'; + +import { Card, Container } from '@mui/material' + +type BodyCardProps = { + children?: React.ReactNode; +} + +const BodyCard = ({children}: BodyCardProps): JSX.Element => { + return ( + + + {children} + + + ); +} + +export default BodyCard; diff --git a/frontend/pages/reviewer.tsx b/frontend/pages/reviewer.tsx new file mode 100644 index 0000000..76c8b4c --- /dev/null +++ b/frontend/pages/reviewer.tsx @@ -0,0 +1,23 @@ +import type { NextPage } from 'next' +import Head from 'next/head' +import Image from 'next/image' +import Link from 'next/link' +import styles from '../styles/Home.module.css' + +import BodyCard from '../components/utils/BodyCard' + +import { Card } from '@mui/material'; + +const Reviewer: NextPage = () => { + return ( +
+ +
+ swag +
+
+
+ ) +} + +export default Reviewer; From f5b33a9414e2696c7c868d49e274c1310fd6651a Mon Sep 17 00:00:00 2001 From: Michael Nef Date: Tue, 5 Jul 2022 16:14:20 +0800 Subject: [PATCH 19/89] Created general card comopnent --- frontend/components/utils/BodyCard.tsx | 36 +++++++++++++++++++++++--- frontend/pages/reviewer.tsx | 2 +- 2 files changed, 33 insertions(+), 5 deletions(-) diff --git a/frontend/components/utils/BodyCard.tsx b/frontend/components/utils/BodyCard.tsx index 30b0284..25e40a9 100644 --- a/frontend/components/utils/BodyCard.tsx +++ b/frontend/components/utils/BodyCard.tsx @@ -4,13 +4,41 @@ import { Card, Container } from '@mui/material' type BodyCardProps = { children?: React.ReactNode; + header?: String; } -const BodyCard = ({children}: BodyCardProps): JSX.Element => { - return ( +const BodyCard = ({children, header}: BodyCardProps): JSX.Element => { + const headerComponent = header ? ( + + {header} + + ) : null; + return ( - - {children} + {headerComponent} + + +
+ {children} +
+
); diff --git a/frontend/pages/reviewer.tsx b/frontend/pages/reviewer.tsx index 76c8b4c..a9337b4 100644 --- a/frontend/pages/reviewer.tsx +++ b/frontend/pages/reviewer.tsx @@ -11,7 +11,7 @@ import { Card } from '@mui/material'; const Reviewer: NextPage = () => { return (
- +
swag
From cf71f7784abe7f9f3a8038256a1738c9ad060151 Mon Sep 17 00:00:00 2001 From: Michael Nef Date: Fri, 8 Jul 2022 11:07:55 +0800 Subject: [PATCH 20/89] reviewer page general structure --- frontend/pages/reviewer.tsx | 13 ++++++++++--- 1 file changed, 10 insertions(+), 3 deletions(-) diff --git a/frontend/pages/reviewer.tsx b/frontend/pages/reviewer.tsx index a9337b4..f035c02 100644 --- a/frontend/pages/reviewer.tsx +++ b/frontend/pages/reviewer.tsx @@ -1,4 +1,7 @@ +import { useState } from 'react' + import type { NextPage } from 'next' + import Head from 'next/head' import Image from 'next/image' import Link from 'next/link' @@ -6,14 +9,18 @@ import styles from '../styles/Home.module.css' import BodyCard from '../components/utils/BodyCard' -import { Card } from '@mui/material'; +import { Card, Button } from '@mui/material'; const Reviewer: NextPage = () => { + + const showArchived = useState(false); + return (
- +
- swag + +
From 926de72fe26f79b5caee54b96c057620fe3920e9 Mon Sep 17 00:00:00 2001 From: Michael Nef Date: Fri, 8 Jul 2022 11:17:12 +0800 Subject: [PATCH 21/89] Added archive button toggle --- frontend/pages/reviewer.tsx | 9 +++++++-- 1 file changed, 7 insertions(+), 2 deletions(-) diff --git a/frontend/pages/reviewer.tsx b/frontend/pages/reviewer.tsx index f035c02..550cf25 100644 --- a/frontend/pages/reviewer.tsx +++ b/frontend/pages/reviewer.tsx @@ -11,15 +11,20 @@ import BodyCard from '../components/utils/BodyCard' import { Card, Button } from '@mui/material'; +import OngoingReviewsList from '../components/Reviewer/OngoingReviewsList.tsx' + + const Reviewer: NextPage = () => { - const showArchived = useState(false); + const [showArchived, setShowArchived] = useState(false); + const archivedButtonText = showArchived ? "Hide Archived" : "Show Archvied"; + const toggleArchived = (e) => setShowArchived(!showArchived); return (
- +
From 012462ca801ff8b42f24f25da189fd0b3f5945c6 Mon Sep 17 00:00:00 2001 From: Michael Nef Date: Mon, 18 Jul 2022 13:16:58 +0800 Subject: [PATCH 22/89] course evaluation tests --- .../course_evaluations/tests/tests_crud.py | 112 ------------------ .../tests/tests_permissions.py | 75 ------------ 2 files changed, 187 deletions(-) delete mode 100644 backend/course_evaluations/tests/tests_crud.py delete mode 100644 backend/course_evaluations/tests/tests_permissions.py diff --git a/backend/course_evaluations/tests/tests_crud.py b/backend/course_evaluations/tests/tests_crud.py deleted file mode 100644 index 8cbe3c7..0000000 --- a/backend/course_evaluations/tests/tests_crud.py +++ /dev/null @@ -1,112 +0,0 @@ -""" -This test file is focused on the crud functionality of the course evaluations. - -Note: Permission testing is not the focus of this test file. -""" -from django.urls import reverse -from rest_framework import status - -from course_evaluations.models import CourseEvaluation, EOCSet - - -def test_list_view_course_evaluation(api_client_with_credentials_return_user, make_course_evaluation): - """ - GIVEN: There are multiple course evaluation assigned to a user - WHEN: The endpoint for the course evaluation is called - THEN: The course evaluation data is returned successfully with only a couple of information (eg. EOC info is not returned) - """ - api_client, user = api_client_with_credentials_return_user() - course_evaluation_1 = make_course_evaluation(coordinators=[user]) - course_evaluation_2 = make_course_evaluation(coordinators=[user]) - - # This user should not be able to see this - make_course_evaluation(coordinators=[]) - - url = reverse("api-v1:course_evaluations:course-evaluations-list") - response = api_client.get(url) - - assert response.status_code == status.HTTP_200_OK - data = response.data - - # Check that we can find the two course evaluations - assert len(data) == 2 - assert data[0]["id"] == str(course_evaluation_1.id) - assert data[1]["id"] == str(course_evaluation_2.id) - - # Check the content of one of the course_evaluation - course_evaluation_from_endpoint = data[0] - assert course_evaluation_from_endpoint["id"] == str(course_evaluation_1.id) - assert course_evaluation_from_endpoint["unit_code"] == course_evaluation_1.unit_code - assert course_evaluation_from_endpoint["description"] == course_evaluation_1.description - - # Check the coordinator - assert len(course_evaluation_from_endpoint["coordinators"]) == 1 - coordinator = course_evaluation_from_endpoint["coordinators"][0] - - assert coordinator["id"] == user.id - assert coordinator["username"] == user.username - - # Check that there are certain fields that does not exist - assert "eoc_set" not in course_evaluation_from_endpoint - assert "eoc_set_id" not in course_evaluation_from_endpoint - - -def test_create_view_course_evaluation(setup_indeaa, api_client_with_credentials_return_user): - """ - GIVEN: As a user of the system - WHEN: I create a course evaluation - THEN: The course evaluation is created successfully - """ - api_client, user = api_client_with_credentials_return_user() - - url = reverse("api-v1:course_evaluations:course-evaluations-list") - data = { - "eoc_set_id": str(EOCSet.objects.first().id), - "unit_code": "TEST1002", - "description": "Test Creation of CourseEvaluation", - } - response = api_client.post(url, data) - - assert response.status_code == status.HTTP_201_CREATED - - # Check that the course evaluation is created - course_evaluation = CourseEvaluation.objects.first() - assert course_evaluation.unit_code == data["unit_code"] - assert course_evaluation.description == data["description"] - assert course_evaluation.eoc_set == EOCSet.objects.first() - - -def test_update_view_course_evaluation(api_client_with_credentials_return_user, make_course_evaluation): - """ - GIVEN: A course evaluation is created - WHEN: I update the course evaluation - THEN: The course evaluation is updated successfully - """ - api_client, user = api_client_with_credentials_return_user() - course_evaluation_1 = make_course_evaluation(coordinators=[user]) - - url = reverse("api-v1:course_evaluations:course-evaluations-detail", kwargs={"pk": course_evaluation_1.id}) - data = { - "description": "Test Update of CourseEvaluation", - } - response = api_client.patch(url, data) - - assert response.status_code == status.HTTP_200_OK - assert response.data["unit_code"] == course_evaluation_1.unit_code - assert response.data["description"] == data["description"] - - -def test_delete_view_course_evaluation(api_client_with_credentials_return_user, make_course_evaluation): - """ - GIVEN: A course evaluation is created - WHEN: I delete the course evaluation - THEN: The course evaluation cannot be deleted - """ - api_client, user = api_client_with_credentials_return_user() - course_evaluation_1 = make_course_evaluation(coordinators=[user]) - - url = reverse("api-v1:course_evaluations:course-evaluations-detail", kwargs={"pk": course_evaluation_1.id}) - response = api_client.delete(url) - - assert response.status_code == status.HTTP_405_METHOD_NOT_ALLOWED - assert CourseEvaluation.objects.count() == 1 diff --git a/backend/course_evaluations/tests/tests_permissions.py b/backend/course_evaluations/tests/tests_permissions.py deleted file mode 100644 index cfdb3fa..0000000 --- a/backend/course_evaluations/tests/tests_permissions.py +++ /dev/null @@ -1,75 +0,0 @@ -""" -This test file is focused on permission testing. This is to ensure that unauthorised access cannot use the API -""" -from django.urls import reverse -from rest_framework import status - -from course_evaluations.models import CourseEvaluation - - -def test_list_view_course_evaluation_anonymous(api_client_no_auth): - """ - GIVEN: The user is not authenticated - WHEN: the user tries to use the endpoint - THEN: The user is not authorised to use the endpoint - """ - url = reverse("api-v1:course_evaluations:course-evaluations-list") - response = api_client_no_auth.get(url) - - assert response.status_code == status.HTTP_401_UNAUTHORIZED - - -def test_create_view_course_evaluation_anonymous(api_client_no_auth): - """ - GIVEN: The user is not authenticated - WHEN: I create a course evaluation - THEN: The user is not authorised to use the endpoint - """ - url = reverse("api-v1:course_evaluations:course-evaluations-list") - data = { - "unit_code": "TEST", - "description": "Test Course Evaluation", - } - response = api_client_no_auth.post(url, data) - - assert response.status_code == status.HTTP_401_UNAUTHORIZED - assert CourseEvaluation.objects.count() == 0 - - -def test_update_view_course_evaluation_anonymous(api_client_no_auth, create_user, make_course_evaluation): - """ - GIVEN: The user is not authenticated - WHEN: I update a course evaluation - THEN: The user is not authorised to use the endpoint - """ - user = create_user() - course_evaluation = make_course_evaluation(coordinators=[user]) - - url = reverse( - "api-v1:course_evaluations:course-evaluation-detail", - kwargs={"pk": course_evaluation.id}, - ) - data = {"unit_code": "TEST"} - response = api_client_no_auth.put(url, data) - - assert response.status_code == status.HTTP_401_UNAUTHORIZED - assert CourseEvaluation.objects.count() == 1 - - -def test_delete_view_course_evaluation_anonymous(api_client_no_auth, create_user, make_course_evaluation): - """ - GIVEN: The user is not authenticated - WHEN: I delete a course evaluation - THEN: The user is not authorised to use the endpoint - """ - user = create_user() - course_evaluation = make_course_evaluation(coordinators=[user]) - - url = reverse( - "api-v1:course_evaluations:course-evaluation-detail", - kwargs={"pk": course_evaluation.id}, - ) - response = api_client_no_auth.delete(url) - - assert response.status_code == status.HTTP_401_UNAUTHORIZED - assert CourseEvaluation.objects.count() == 1 From 0e5cef27250ee73fa28927ec6af2332e67b36565 Mon Sep 17 00:00:00 2001 From: Michael Nef Date: Tue, 19 Jul 2022 09:58:31 +0800 Subject: [PATCH 23/89] Revert "Added archive button toggle" This reverts commit a3d5fe9609f4e740612894c98050de8397b49356. --- frontend/pages/reviewer.tsx | 9 ++------- 1 file changed, 2 insertions(+), 7 deletions(-) diff --git a/frontend/pages/reviewer.tsx b/frontend/pages/reviewer.tsx index 550cf25..f035c02 100644 --- a/frontend/pages/reviewer.tsx +++ b/frontend/pages/reviewer.tsx @@ -11,20 +11,15 @@ import BodyCard from '../components/utils/BodyCard' import { Card, Button } from '@mui/material'; -import OngoingReviewsList from '../components/Reviewer/OngoingReviewsList.tsx' - - const Reviewer: NextPage = () => { - const [showArchived, setShowArchived] = useState(false); - const archivedButtonText = showArchived ? "Hide Archived" : "Show Archvied"; - const toggleArchived = (e) => setShowArchived(!showArchived); + const showArchived = useState(false); return (
- +
From 1a52e9026f0538a645788176ee7ed3388b518ef7 Mon Sep 17 00:00:00 2001 From: Michael Nef Date: Tue, 19 Jul 2022 09:58:55 +0800 Subject: [PATCH 24/89] Revert "Revert "Added archive button toggle"" This reverts commit 095244c7ac7ddfb04c2fd9b3cbbde942bb98372f. --- frontend/pages/reviewer.tsx | 9 +++++++-- 1 file changed, 7 insertions(+), 2 deletions(-) diff --git a/frontend/pages/reviewer.tsx b/frontend/pages/reviewer.tsx index f035c02..550cf25 100644 --- a/frontend/pages/reviewer.tsx +++ b/frontend/pages/reviewer.tsx @@ -11,15 +11,20 @@ import BodyCard from '../components/utils/BodyCard' import { Card, Button } from '@mui/material'; +import OngoingReviewsList from '../components/Reviewer/OngoingReviewsList.tsx' + + const Reviewer: NextPage = () => { - const showArchived = useState(false); + const [showArchived, setShowArchived] = useState(false); + const archivedButtonText = showArchived ? "Hide Archived" : "Show Archvied"; + const toggleArchived = (e) => setShowArchived(!showArchived); return (
- +
From aa25c41c19fac4dc692ce8a9edc1950841836aed Mon Sep 17 00:00:00 2001 From: Michael Nef Date: Tue, 19 Jul 2022 09:59:09 +0800 Subject: [PATCH 25/89] Revert "course evaluation tests" This reverts commit dd10698e8d0c0221bdfba81ff642234bdc7a9972. --- .../course_evaluations/tests/tests_crud.py | 112 ++++++++++++++++++ .../tests/tests_permissions.py | 75 ++++++++++++ 2 files changed, 187 insertions(+) create mode 100644 backend/course_evaluations/tests/tests_crud.py create mode 100644 backend/course_evaluations/tests/tests_permissions.py diff --git a/backend/course_evaluations/tests/tests_crud.py b/backend/course_evaluations/tests/tests_crud.py new file mode 100644 index 0000000..8cbe3c7 --- /dev/null +++ b/backend/course_evaluations/tests/tests_crud.py @@ -0,0 +1,112 @@ +""" +This test file is focused on the crud functionality of the course evaluations. + +Note: Permission testing is not the focus of this test file. +""" +from django.urls import reverse +from rest_framework import status + +from course_evaluations.models import CourseEvaluation, EOCSet + + +def test_list_view_course_evaluation(api_client_with_credentials_return_user, make_course_evaluation): + """ + GIVEN: There are multiple course evaluation assigned to a user + WHEN: The endpoint for the course evaluation is called + THEN: The course evaluation data is returned successfully with only a couple of information (eg. EOC info is not returned) + """ + api_client, user = api_client_with_credentials_return_user() + course_evaluation_1 = make_course_evaluation(coordinators=[user]) + course_evaluation_2 = make_course_evaluation(coordinators=[user]) + + # This user should not be able to see this + make_course_evaluation(coordinators=[]) + + url = reverse("api-v1:course_evaluations:course-evaluations-list") + response = api_client.get(url) + + assert response.status_code == status.HTTP_200_OK + data = response.data + + # Check that we can find the two course evaluations + assert len(data) == 2 + assert data[0]["id"] == str(course_evaluation_1.id) + assert data[1]["id"] == str(course_evaluation_2.id) + + # Check the content of one of the course_evaluation + course_evaluation_from_endpoint = data[0] + assert course_evaluation_from_endpoint["id"] == str(course_evaluation_1.id) + assert course_evaluation_from_endpoint["unit_code"] == course_evaluation_1.unit_code + assert course_evaluation_from_endpoint["description"] == course_evaluation_1.description + + # Check the coordinator + assert len(course_evaluation_from_endpoint["coordinators"]) == 1 + coordinator = course_evaluation_from_endpoint["coordinators"][0] + + assert coordinator["id"] == user.id + assert coordinator["username"] == user.username + + # Check that there are certain fields that does not exist + assert "eoc_set" not in course_evaluation_from_endpoint + assert "eoc_set_id" not in course_evaluation_from_endpoint + + +def test_create_view_course_evaluation(setup_indeaa, api_client_with_credentials_return_user): + """ + GIVEN: As a user of the system + WHEN: I create a course evaluation + THEN: The course evaluation is created successfully + """ + api_client, user = api_client_with_credentials_return_user() + + url = reverse("api-v1:course_evaluations:course-evaluations-list") + data = { + "eoc_set_id": str(EOCSet.objects.first().id), + "unit_code": "TEST1002", + "description": "Test Creation of CourseEvaluation", + } + response = api_client.post(url, data) + + assert response.status_code == status.HTTP_201_CREATED + + # Check that the course evaluation is created + course_evaluation = CourseEvaluation.objects.first() + assert course_evaluation.unit_code == data["unit_code"] + assert course_evaluation.description == data["description"] + assert course_evaluation.eoc_set == EOCSet.objects.first() + + +def test_update_view_course_evaluation(api_client_with_credentials_return_user, make_course_evaluation): + """ + GIVEN: A course evaluation is created + WHEN: I update the course evaluation + THEN: The course evaluation is updated successfully + """ + api_client, user = api_client_with_credentials_return_user() + course_evaluation_1 = make_course_evaluation(coordinators=[user]) + + url = reverse("api-v1:course_evaluations:course-evaluations-detail", kwargs={"pk": course_evaluation_1.id}) + data = { + "description": "Test Update of CourseEvaluation", + } + response = api_client.patch(url, data) + + assert response.status_code == status.HTTP_200_OK + assert response.data["unit_code"] == course_evaluation_1.unit_code + assert response.data["description"] == data["description"] + + +def test_delete_view_course_evaluation(api_client_with_credentials_return_user, make_course_evaluation): + """ + GIVEN: A course evaluation is created + WHEN: I delete the course evaluation + THEN: The course evaluation cannot be deleted + """ + api_client, user = api_client_with_credentials_return_user() + course_evaluation_1 = make_course_evaluation(coordinators=[user]) + + url = reverse("api-v1:course_evaluations:course-evaluations-detail", kwargs={"pk": course_evaluation_1.id}) + response = api_client.delete(url) + + assert response.status_code == status.HTTP_405_METHOD_NOT_ALLOWED + assert CourseEvaluation.objects.count() == 1 diff --git a/backend/course_evaluations/tests/tests_permissions.py b/backend/course_evaluations/tests/tests_permissions.py new file mode 100644 index 0000000..cfdb3fa --- /dev/null +++ b/backend/course_evaluations/tests/tests_permissions.py @@ -0,0 +1,75 @@ +""" +This test file is focused on permission testing. This is to ensure that unauthorised access cannot use the API +""" +from django.urls import reverse +from rest_framework import status + +from course_evaluations.models import CourseEvaluation + + +def test_list_view_course_evaluation_anonymous(api_client_no_auth): + """ + GIVEN: The user is not authenticated + WHEN: the user tries to use the endpoint + THEN: The user is not authorised to use the endpoint + """ + url = reverse("api-v1:course_evaluations:course-evaluations-list") + response = api_client_no_auth.get(url) + + assert response.status_code == status.HTTP_401_UNAUTHORIZED + + +def test_create_view_course_evaluation_anonymous(api_client_no_auth): + """ + GIVEN: The user is not authenticated + WHEN: I create a course evaluation + THEN: The user is not authorised to use the endpoint + """ + url = reverse("api-v1:course_evaluations:course-evaluations-list") + data = { + "unit_code": "TEST", + "description": "Test Course Evaluation", + } + response = api_client_no_auth.post(url, data) + + assert response.status_code == status.HTTP_401_UNAUTHORIZED + assert CourseEvaluation.objects.count() == 0 + + +def test_update_view_course_evaluation_anonymous(api_client_no_auth, create_user, make_course_evaluation): + """ + GIVEN: The user is not authenticated + WHEN: I update a course evaluation + THEN: The user is not authorised to use the endpoint + """ + user = create_user() + course_evaluation = make_course_evaluation(coordinators=[user]) + + url = reverse( + "api-v1:course_evaluations:course-evaluation-detail", + kwargs={"pk": course_evaluation.id}, + ) + data = {"unit_code": "TEST"} + response = api_client_no_auth.put(url, data) + + assert response.status_code == status.HTTP_401_UNAUTHORIZED + assert CourseEvaluation.objects.count() == 1 + + +def test_delete_view_course_evaluation_anonymous(api_client_no_auth, create_user, make_course_evaluation): + """ + GIVEN: The user is not authenticated + WHEN: I delete a course evaluation + THEN: The user is not authorised to use the endpoint + """ + user = create_user() + course_evaluation = make_course_evaluation(coordinators=[user]) + + url = reverse( + "api-v1:course_evaluations:course-evaluation-detail", + kwargs={"pk": course_evaluation.id}, + ) + response = api_client_no_auth.delete(url) + + assert response.status_code == status.HTTP_401_UNAUTHORIZED + assert CourseEvaluation.objects.count() == 1 From f79d848b8f3f8343ef1d913531147cdd8588cbd4 Mon Sep 17 00:00:00 2001 From: Michael Nef Date: Tue, 19 Jul 2022 10:01:42 +0800 Subject: [PATCH 26/89] fixed evaluation tests --- backend/course_evaluations/tests/tests_permissions.py | 9 +++++++-- backend/pytest.ini | 4 ++-- 2 files changed, 9 insertions(+), 4 deletions(-) diff --git a/backend/course_evaluations/tests/tests_permissions.py b/backend/course_evaluations/tests/tests_permissions.py index cfdb3fa..c16bc4e 100644 --- a/backend/course_evaluations/tests/tests_permissions.py +++ b/backend/course_evaluations/tests/tests_permissions.py @@ -5,8 +5,10 @@ from rest_framework import status from course_evaluations.models import CourseEvaluation +import pytest +@pytest.mark.django_db def test_list_view_course_evaluation_anonymous(api_client_no_auth): """ GIVEN: The user is not authenticated @@ -19,6 +21,7 @@ def test_list_view_course_evaluation_anonymous(api_client_no_auth): assert response.status_code == status.HTTP_401_UNAUTHORIZED +@pytest.mark.django_db def test_create_view_course_evaluation_anonymous(api_client_no_auth): """ GIVEN: The user is not authenticated @@ -36,6 +39,7 @@ def test_create_view_course_evaluation_anonymous(api_client_no_auth): assert CourseEvaluation.objects.count() == 0 +@pytest.mark.django_db def test_update_view_course_evaluation_anonymous(api_client_no_auth, create_user, make_course_evaluation): """ GIVEN: The user is not authenticated @@ -46,7 +50,7 @@ def test_update_view_course_evaluation_anonymous(api_client_no_auth, create_user course_evaluation = make_course_evaluation(coordinators=[user]) url = reverse( - "api-v1:course_evaluations:course-evaluation-detail", + "api-v1:course_evaluations:course-evaluations-detail", kwargs={"pk": course_evaluation.id}, ) data = {"unit_code": "TEST"} @@ -56,6 +60,7 @@ def test_update_view_course_evaluation_anonymous(api_client_no_auth, create_user assert CourseEvaluation.objects.count() == 1 +@pytest.mark.django_db def test_delete_view_course_evaluation_anonymous(api_client_no_auth, create_user, make_course_evaluation): """ GIVEN: The user is not authenticated @@ -66,7 +71,7 @@ def test_delete_view_course_evaluation_anonymous(api_client_no_auth, create_user course_evaluation = make_course_evaluation(coordinators=[user]) url = reverse( - "api-v1:course_evaluations:course-evaluation-detail", + "api-v1:course_evaluations:course-evaluations-detail", kwargs={"pk": course_evaluation.id}, ) response = api_client_no_auth.delete(url) diff --git a/backend/pytest.ini b/backend/pytest.ini index 3a83a6f..4eb0bf6 100644 --- a/backend/pytest.ini +++ b/backend/pytest.ini @@ -1,7 +1,7 @@ [pytest] DJANGO_SETTINGS_MODULE=config.settings.ci -python_files = tests.py test_*.py *_tests.py +python_files = tests.py test_*.py *_tests.py tests_*.py ; Automatic Process distribution of Test Files to CPU cores addopts = --strict-markers markers = - v1_0_0: All tests introduced in version 1.0.0 and previous versions \ No newline at end of file + v1_0_0: All tests introduced in version 1.0.0 and previous versions From 0ef98f83b250f6a83768302b46ec877b5931df82 Mon Sep 17 00:00:00 2001 From: Michael Nef Date: Tue, 19 Jul 2022 11:24:12 +0800 Subject: [PATCH 27/89] created reviews app --- backend/reviews/__init__.py | 0 backend/reviews/admin.py | 3 +++ backend/reviews/apps.py | 6 ++++++ backend/reviews/migrations/__init__.py | 0 backend/reviews/models.py | 3 +++ backend/reviews/tests.py | 3 +++ backend/reviews/views.py | 3 +++ 7 files changed, 18 insertions(+) create mode 100644 backend/reviews/__init__.py create mode 100644 backend/reviews/admin.py create mode 100644 backend/reviews/apps.py create mode 100644 backend/reviews/migrations/__init__.py create mode 100644 backend/reviews/models.py create mode 100644 backend/reviews/tests.py create mode 100644 backend/reviews/views.py diff --git a/backend/reviews/__init__.py b/backend/reviews/__init__.py new file mode 100644 index 0000000..e69de29 diff --git a/backend/reviews/admin.py b/backend/reviews/admin.py new file mode 100644 index 0000000..8c38f3f --- /dev/null +++ b/backend/reviews/admin.py @@ -0,0 +1,3 @@ +from django.contrib import admin + +# Register your models here. diff --git a/backend/reviews/apps.py b/backend/reviews/apps.py new file mode 100644 index 0000000..80e0186 --- /dev/null +++ b/backend/reviews/apps.py @@ -0,0 +1,6 @@ +from django.apps import AppConfig + + +class ReviewsConfig(AppConfig): + default_auto_field = 'django.db.models.BigAutoField' + name = 'reviews' diff --git a/backend/reviews/migrations/__init__.py b/backend/reviews/migrations/__init__.py new file mode 100644 index 0000000..e69de29 diff --git a/backend/reviews/models.py b/backend/reviews/models.py new file mode 100644 index 0000000..71a8362 --- /dev/null +++ b/backend/reviews/models.py @@ -0,0 +1,3 @@ +from django.db import models + +# Create your models here. diff --git a/backend/reviews/tests.py b/backend/reviews/tests.py new file mode 100644 index 0000000..7ce503c --- /dev/null +++ b/backend/reviews/tests.py @@ -0,0 +1,3 @@ +from django.test import TestCase + +# Create your tests here. diff --git a/backend/reviews/views.py b/backend/reviews/views.py new file mode 100644 index 0000000..91ea44a --- /dev/null +++ b/backend/reviews/views.py @@ -0,0 +1,3 @@ +from django.shortcuts import render + +# Create your views here. From 8019aae93ee8669cf49fed4c65586f90fc7a062a Mon Sep 17 00:00:00 2001 From: Michael Nef Date: Tue, 19 Jul 2022 12:08:31 +0800 Subject: [PATCH 28/89] moved reviews model to new app --- backend/config/settings/base.py | 1 + backend/config/urls.py | 7 ++++++ backend/course_evaluations/models.py | 1 + backend/course_evaluations/serializers.py | 1 + backend/reviews/__init__.py | 0 backend/reviews/admin.py | 5 +++++ backend/reviews/apps.py | 0 backend/reviews/models.py | 26 ++++++++++++++++++++++- backend/reviews/serializers.py | 8 +++++++ backend/reviews/tests.py | 0 backend/reviews/urls.py | 13 ++++++++++++ backend/reviews/views.py | 16 +++++++++++++- 12 files changed, 76 insertions(+), 2 deletions(-) mode change 100644 => 100755 backend/reviews/__init__.py mode change 100644 => 100755 backend/reviews/admin.py mode change 100644 => 100755 backend/reviews/apps.py mode change 100644 => 100755 backend/reviews/models.py create mode 100644 backend/reviews/serializers.py mode change 100644 => 100755 backend/reviews/tests.py create mode 100644 backend/reviews/urls.py mode change 100644 => 100755 backend/reviews/views.py diff --git a/backend/config/settings/base.py b/backend/config/settings/base.py index 0ebd6b8..8c272e6 100644 --- a/backend/config/settings/base.py +++ b/backend/config/settings/base.py @@ -69,6 +69,7 @@ "django_filters", "commands", "course_evaluations", + "reviews", ] # Refer to https://dj-rest-auth.readthedocs.io/en/latest/installation.html#registration-optional diff --git a/backend/config/urls.py b/backend/config/urls.py index 50a5b34..9a7376f 100644 --- a/backend/config/urls.py +++ b/backend/config/urls.py @@ -11,6 +11,13 @@ namespace="course_evaluations", # use this namespace for url reversal ), ), + path( + "reviews/", + include( + ("reviews.urls", "reviews"), + namespace="reviews", + ), + ), path("authentication/", include("dj_rest_auth.urls")), ], "api", diff --git a/backend/course_evaluations/models.py b/backend/course_evaluations/models.py index 9c4b1df..f9b88fc 100644 --- a/backend/course_evaluations/models.py +++ b/backend/course_evaluations/models.py @@ -80,3 +80,4 @@ class CourseEvaluation(models.Model): def __str__(self): return f"{self.eoc_set.name} - {self.unit_code} ({self.created_at})" + diff --git a/backend/course_evaluations/serializers.py b/backend/course_evaluations/serializers.py index a2386df..48a131d 100644 --- a/backend/course_evaluations/serializers.py +++ b/backend/course_evaluations/serializers.py @@ -54,3 +54,4 @@ class CourseEvaluationDetailSerializer(serializers.ModelSerializer): class Meta: model = CourseEvaluation fields = "__all__" + diff --git a/backend/reviews/__init__.py b/backend/reviews/__init__.py old mode 100644 new mode 100755 diff --git a/backend/reviews/admin.py b/backend/reviews/admin.py old mode 100644 new mode 100755 index 8c38f3f..ec81bac --- a/backend/reviews/admin.py +++ b/backend/reviews/admin.py @@ -1,3 +1,8 @@ from django.contrib import admin +from reviews.models import Review # Register your models here. +@admin.register(Review) +class ReviewAdmin(admin.ModelAdmin): + list_display = ("id", "course_evaluation", "final_comment", "date_submitted") + ordering = ("created_at",) diff --git a/backend/reviews/apps.py b/backend/reviews/apps.py old mode 100644 new mode 100755 diff --git a/backend/reviews/models.py b/backend/reviews/models.py old mode 100644 new mode 100755 index 71a8362..e94a6eb --- a/backend/reviews/models.py +++ b/backend/reviews/models.py @@ -1,3 +1,27 @@ -from django.db import models +import uuid # Create your models here. +from django.db import models +from course_evaluations.models import CourseEvaluation + + +class Review(models.Model): + """ + A review is what Reviewers make. There are many reviews per + course evaluation, one per reviewer. + """ + + # UUID for the review + id = models.UUIDField(primary_key=True, default=uuid.uuid4, editable=False) + + # One-to-many Relationship with Course Evaluations + course_evaluation = models.ForeignKey(CourseEvaluation, on_delete=models.CASCADE) + + coordinators = models.ForeignKey("auth.User", related_name="reviews", on_delete=models.CASCADE) + + final_comment = models.TextField(null=False, blank=True) + + date_submitted = models.DateTimeField() + + created_at = models.DateTimeField(auto_now_add=True) + updated_at = models.DateTimeField(auto_now=True) diff --git a/backend/reviews/serializers.py b/backend/reviews/serializers.py new file mode 100644 index 0000000..20ec6dc --- /dev/null +++ b/backend/reviews/serializers.py @@ -0,0 +1,8 @@ +from rest_framework import serializers +from reviews.models import Review + +class ReviewSerializer(serializers.ModelSerializer): + class Meta: + model = Review + fields = "__all__" + diff --git a/backend/reviews/tests.py b/backend/reviews/tests.py old mode 100644 new mode 100755 diff --git a/backend/reviews/urls.py b/backend/reviews/urls.py new file mode 100644 index 0000000..d2c4d70 --- /dev/null +++ b/backend/reviews/urls.py @@ -0,0 +1,13 @@ +from django.urls import include, path +from rest_framework.routers import DefaultRouter + +from reviews.views import ReviewsViewSet + +# Create a router and register our viewsets with it. +router = DefaultRouter() +router.register(r"", ReviewsViewSet, basename="reviews") + +# The API URLs are now determined automatically by the router. +urlpatterns = [ + path("", include(router.urls)), +] diff --git a/backend/reviews/views.py b/backend/reviews/views.py old mode 100644 new mode 100755 index 91ea44a..6cab2f3 --- a/backend/reviews/views.py +++ b/backend/reviews/views.py @@ -1,3 +1,17 @@ -from django.shortcuts import render +from rest_framework import permissions, status, viewsets +from rest_framework.exceptions import ValidationError +from rest_framework.response import Response + +from reviews.models import Review +from reviews.serializers import ReviewSerializer # Create your views here. + +class ReviewsViewSet(viewsets.ModelViewSet): + """ + Viewset that handles the following + """ + + queryset = Review.objects.all() + def get_serializer(self, *args, **kwargs): + return ReviewSerializer(*args, **kwargs) From 60b8e51fcb34ef28ae876986dc9b364c760ddc01 Mon Sep 17 00:00:00 2001 From: Michael Nef Date: Tue, 19 Jul 2022 12:12:26 +0800 Subject: [PATCH 29/89] created documents app --- backend/documents/__init__.py | 0 backend/documents/admin.py | 3 +++ backend/documents/apps.py | 6 ++++++ backend/documents/migrations/__init__.py | 0 backend/documents/models.py | 3 +++ backend/documents/tests.py | 3 +++ backend/documents/views.py | 3 +++ 7 files changed, 18 insertions(+) create mode 100644 backend/documents/__init__.py create mode 100644 backend/documents/admin.py create mode 100644 backend/documents/apps.py create mode 100644 backend/documents/migrations/__init__.py create mode 100644 backend/documents/models.py create mode 100644 backend/documents/tests.py create mode 100644 backend/documents/views.py diff --git a/backend/documents/__init__.py b/backend/documents/__init__.py new file mode 100644 index 0000000..e69de29 diff --git a/backend/documents/admin.py b/backend/documents/admin.py new file mode 100644 index 0000000..8c38f3f --- /dev/null +++ b/backend/documents/admin.py @@ -0,0 +1,3 @@ +from django.contrib import admin + +# Register your models here. diff --git a/backend/documents/apps.py b/backend/documents/apps.py new file mode 100644 index 0000000..ab8a093 --- /dev/null +++ b/backend/documents/apps.py @@ -0,0 +1,6 @@ +from django.apps import AppConfig + + +class DocumentsConfig(AppConfig): + default_auto_field = 'django.db.models.BigAutoField' + name = 'documents' diff --git a/backend/documents/migrations/__init__.py b/backend/documents/migrations/__init__.py new file mode 100644 index 0000000..e69de29 diff --git a/backend/documents/models.py b/backend/documents/models.py new file mode 100644 index 0000000..71a8362 --- /dev/null +++ b/backend/documents/models.py @@ -0,0 +1,3 @@ +from django.db import models + +# Create your models here. diff --git a/backend/documents/tests.py b/backend/documents/tests.py new file mode 100644 index 0000000..7ce503c --- /dev/null +++ b/backend/documents/tests.py @@ -0,0 +1,3 @@ +from django.test import TestCase + +# Create your tests here. diff --git a/backend/documents/views.py b/backend/documents/views.py new file mode 100644 index 0000000..91ea44a --- /dev/null +++ b/backend/documents/views.py @@ -0,0 +1,3 @@ +from django.shortcuts import render + +# Create your views here. From 54f03b23dce9518133a2c0a8cf75a802b8ade693 Mon Sep 17 00:00:00 2001 From: Michael Nef Date: Tue, 19 Jul 2022 15:47:56 +0800 Subject: [PATCH 30/89] added documents app --- backend/config/settings/base.py | 1 + backend/config/urls.py | 7 +++++++ backend/course_evaluations/admin.py | 1 - backend/documents/admin.py | 5 +++++ backend/documents/models.py | 22 ++++++++++++++++++++++ backend/documents/serializers.py | 10 ++++++++++ backend/documents/urls.py | 14 ++++++++++++++ backend/documents/views.py | 19 ++++++++++++++++++- 8 files changed, 77 insertions(+), 2 deletions(-) create mode 100644 backend/documents/serializers.py create mode 100644 backend/documents/urls.py diff --git a/backend/config/settings/base.py b/backend/config/settings/base.py index 8c272e6..972bc86 100644 --- a/backend/config/settings/base.py +++ b/backend/config/settings/base.py @@ -70,6 +70,7 @@ "commands", "course_evaluations", "reviews", + "documents", ] # Refer to https://dj-rest-auth.readthedocs.io/en/latest/installation.html#registration-optional diff --git a/backend/config/urls.py b/backend/config/urls.py index 9a7376f..fbd4fc4 100644 --- a/backend/config/urls.py +++ b/backend/config/urls.py @@ -18,6 +18,13 @@ namespace="reviews", ), ), + path( + "documents/", + include( + ("documents.urls", "documents"), + namespace="documents", + ) + ), path("authentication/", include("dj_rest_auth.urls")), ], "api", diff --git a/backend/course_evaluations/admin.py b/backend/course_evaluations/admin.py index 34f5a75..37a0394 100644 --- a/backend/course_evaluations/admin.py +++ b/backend/course_evaluations/admin.py @@ -1,5 +1,4 @@ from django.contrib import admin - from course_evaluations.models import CourseEvaluation, EOCGeneral, EOCSet, EOCSpecific diff --git a/backend/documents/admin.py b/backend/documents/admin.py index 8c38f3f..8df77a0 100644 --- a/backend/documents/admin.py +++ b/backend/documents/admin.py @@ -1,3 +1,8 @@ from django.contrib import admin +from documents.models import Document # Register your models here. + +@admin.register(Document) +class DocumentAdmin(admin.ModelAdmin): + list_display = ("id", "url", "is_introduction") diff --git a/backend/documents/models.py b/backend/documents/models.py index 71a8362..f44017c 100644 --- a/backend/documents/models.py +++ b/backend/documents/models.py @@ -1,3 +1,25 @@ +import uuid from django.db import models # Create your models here. +from course_evaluations.models import EOCSpecific, EOCGeneral + +class Document(models.Model): + """ + A document is a url to a resource that a reviewer will look at + to judge the quality of a course + """ + + # UUID for the review + id = models.UUIDField(primary_key=True, default=uuid.uuid4, editable=False) + + is_introduction = models.BooleanField() + + # The actual URL to the resource + url = models.URLField() + + eoc_generals = models.ManyToManyField(EOCGeneral) + eoc_specifics = models.ManyToManyField(EOCSpecific) + + created_at = models.DateTimeField(auto_now_add=True) + updated_at = models.DateTimeField(auto_now=True) diff --git a/backend/documents/serializers.py b/backend/documents/serializers.py new file mode 100644 index 0000000..ada6729 --- /dev/null +++ b/backend/documents/serializers.py @@ -0,0 +1,10 @@ +from rest_framework import serializers + +from documents.models import Document + +class DocumentSerializer(serializers.ModelSerializer): + class Meta: + model = Document + fields = "__all__" + + diff --git a/backend/documents/urls.py b/backend/documents/urls.py new file mode 100644 index 0000000..b8b86f1 --- /dev/null +++ b/backend/documents/urls.py @@ -0,0 +1,14 @@ +from django.urls import include, path +from rest_framework.routers import DefaultRouter + +from documents.views import DocumentsViewSet + +# Create a router and register our viewsets with it. +router = DefaultRouter() +router.register(r"", DocumentsViewSet, basename="documents") + +# The API URLs are now determined automatically by the router. +urlpatterns = [ + path("", include(router.urls)), +] + diff --git a/backend/documents/views.py b/backend/documents/views.py index 91ea44a..aac27ac 100644 --- a/backend/documents/views.py +++ b/backend/documents/views.py @@ -1,3 +1,20 @@ -from django.shortcuts import render +from rest_framework import permissions, status, viewsets +from rest_framework.exceptions import ValidationError +from rest_framework.response import Response +from documents.models import Document +from documents.serializers import DocumentSerializer # Create your views here. + + +class DocumentsViewSet(viewsets.ModelViewSet): + """ + Viewset that handles documents + """ + + queryset = Document.objects.all() + def get_serializer(self, *args, **kwargs): + """ + + """ + return DocumentSerializer(*args, **kwargs) \ No newline at end of file From e2323e56b41c58c70e88bfa7b432e3c3ec209f9a Mon Sep 17 00:00:00 2001 From: Michael Nef Date: Tue, 19 Jul 2022 15:49:12 +0800 Subject: [PATCH 31/89] added documents migration --- backend/documents/migrations/0001_initial.py | 28 ++++++++++++++++++++ 1 file changed, 28 insertions(+) create mode 100644 backend/documents/migrations/0001_initial.py diff --git a/backend/documents/migrations/0001_initial.py b/backend/documents/migrations/0001_initial.py new file mode 100644 index 0000000..6a316e5 --- /dev/null +++ b/backend/documents/migrations/0001_initial.py @@ -0,0 +1,28 @@ +# Generated by Django 3.2.13 on 2022-07-19 07:45 + +from django.db import migrations, models +import uuid + + +class Migration(migrations.Migration): + + initial = True + + dependencies = [ + ('course_evaluations', '0001_initial'), + ] + + operations = [ + migrations.CreateModel( + name='Document', + fields=[ + ('id', models.UUIDField(default=uuid.uuid4, editable=False, primary_key=True, serialize=False)), + ('is_introduction', models.BooleanField()), + ('url', models.URLField()), + ('created_at', models.DateTimeField(auto_now_add=True)), + ('updated_at', models.DateTimeField(auto_now=True)), + ('eoc_generals', models.ManyToManyField(to='course_evaluations.EOCGeneral')), + ('eoc_specifics', models.ManyToManyField(to='course_evaluations.EOCSpecific')), + ], + ), + ] From 6c8f4a88973780800d439f259eafa04756f29035 Mon Sep 17 00:00:00 2001 From: Michael Nef Date: Tue, 19 Jul 2022 15:49:36 +0800 Subject: [PATCH 32/89] added model for revieweocspecific --- backend/reviews/migrations/0001_initial.py | 40 ++++++++++++++++++++++ backend/reviews/models.py | 17 ++++++++- 2 files changed, 56 insertions(+), 1 deletion(-) create mode 100644 backend/reviews/migrations/0001_initial.py diff --git a/backend/reviews/migrations/0001_initial.py b/backend/reviews/migrations/0001_initial.py new file mode 100644 index 0000000..e56e852 --- /dev/null +++ b/backend/reviews/migrations/0001_initial.py @@ -0,0 +1,40 @@ +# Generated by Django 3.2.13 on 2022-07-19 07:45 + +from django.conf import settings +from django.db import migrations, models +import django.db.models.deletion +import uuid + + +class Migration(migrations.Migration): + + initial = True + + dependencies = [ + ('course_evaluations', '0001_initial'), + migrations.swappable_dependency(settings.AUTH_USER_MODEL), + ] + + operations = [ + migrations.CreateModel( + name='ReviewEocSpecific', + fields=[ + ('id', models.UUIDField(default=uuid.uuid4, editable=False, primary_key=True, serialize=False)), + ('suggestion', models.TextField(blank=True)), + ('justification', models.TextField(blank=True)), + ('eoc_specific', models.ForeignKey(on_delete=django.db.models.deletion.CASCADE, to='course_evaluations.eocspecific')), + ], + ), + migrations.CreateModel( + name='Review', + fields=[ + ('id', models.UUIDField(default=uuid.uuid4, editable=False, primary_key=True, serialize=False)), + ('final_comment', models.TextField(blank=True)), + ('date_submitted', models.DateTimeField()), + ('created_at', models.DateTimeField(auto_now_add=True)), + ('updated_at', models.DateTimeField(auto_now=True)), + ('coordinators', models.ForeignKey(on_delete=django.db.models.deletion.CASCADE, related_name='reviews', to=settings.AUTH_USER_MODEL)), + ('course_evaluation', models.ForeignKey(on_delete=django.db.models.deletion.CASCADE, to='course_evaluations.courseevaluation')), + ], + ), + ] diff --git a/backend/reviews/models.py b/backend/reviews/models.py index e94a6eb..7947ce4 100755 --- a/backend/reviews/models.py +++ b/backend/reviews/models.py @@ -2,7 +2,7 @@ # Create your models here. from django.db import models -from course_evaluations.models import CourseEvaluation +from course_evaluations.models import CourseEvaluation, EOCSpecific class Review(models.Model): @@ -25,3 +25,18 @@ class Review(models.Model): created_at = models.DateTimeField(auto_now_add=True) updated_at = models.DateTimeField(auto_now=True) + + +class ReviewEocSpecific(models.Model): + """ + For each eoc in a review, they each need specific infomration filled out. + """ + + id = models.UUIDField(primary_key=True, default=uuid.uuid4, editable=False) + + eoc_specific = models.ForeignKey(EOCSpecific, on_delete=models.CASCADE) + + development_level = models.IntegerChoices("DevelopmentLevel", "1 2 3 4 5") + + suggestion = models.TextField(null=False, blank=True) + justification = models.TextField(null=False, blank=True) From eeef153fa4e0b0990948baf9e07e4de910efdf51 Mon Sep 17 00:00:00 2001 From: Michael Nef Date: Tue, 19 Jul 2022 16:23:37 +0800 Subject: [PATCH 33/89] added document reviews and eoc specific reviews --- backend/reviews/admin.py | 15 ++++++++++++++- backend/reviews/models.py | 24 +++++++++++++++++++++++- backend/reviews/serializers.py | 13 ++++++++++++- backend/reviews/views.py | 25 +++++++++++++++++++++++-- 4 files changed, 72 insertions(+), 5 deletions(-) diff --git a/backend/reviews/admin.py b/backend/reviews/admin.py index ec81bac..e53148a 100755 --- a/backend/reviews/admin.py +++ b/backend/reviews/admin.py @@ -1,8 +1,21 @@ from django.contrib import admin -from reviews.models import Review +from reviews.models import Review, ReviewDocument, ReviewEocSpecific # Register your models here. @admin.register(Review) class ReviewAdmin(admin.ModelAdmin): list_display = ("id", "course_evaluation", "final_comment", "date_submitted") ordering = ("created_at",) + + +@admin.register(ReviewDocument) +class ReviewDocumentAdmin(admin.ModelAdmin): + list_display = ("id", "review", "document", "is_viewed", "comment") + ordering = ("created_at",) + + +@admin.register(ReviewEocSpecific) +class ReviewEocSpecificAdmin(admin.ModelAdmin): + list_display = ("id", "review", "eoc_specific", "development_level", "suggestion", "justification") + ordering = ("created_at",) + diff --git a/backend/reviews/models.py b/backend/reviews/models.py index 7947ce4..50994d3 100755 --- a/backend/reviews/models.py +++ b/backend/reviews/models.py @@ -1,9 +1,11 @@ +from re import I import uuid # Create your models here. from django.db import models from course_evaluations.models import CourseEvaluation, EOCSpecific +from documents.models import Document class Review(models.Model): """ @@ -33,10 +35,30 @@ class ReviewEocSpecific(models.Model): """ id = models.UUIDField(primary_key=True, default=uuid.uuid4, editable=False) - + review = models.ForeignKey(Review, null=True, related_name="eoc_specific_reviews", on_delete=models.CASCADE) eoc_specific = models.ForeignKey(EOCSpecific, on_delete=models.CASCADE) development_level = models.IntegerChoices("DevelopmentLevel", "1 2 3 4 5") suggestion = models.TextField(null=False, blank=True) justification = models.TextField(null=False, blank=True) + + created_at = models.DateTimeField(auto_now_add=True) + updated_at = models.DateTimeField(auto_now=True) + + + +class ReviewDocument(models.Model): + """ + For each review, the reviewer may supply comments on different documents. + ReviewDocument contains information regarding these comments. + """ + id = models.UUIDField(primary_key=True, default=uuid.uuid4, editable=False) + review = models.ForeignKey(Review, related_name="documents", on_delete=models.CASCADE) + document = models.ForeignKey(Document, related_name="review_messages", on_delete=models.CASCADE) + is_viewed = models.BooleanField() + comment = models.TextField() + + created_at = models.DateTimeField(auto_now_add=True) + updated_at = models.DateTimeField(auto_now=True) + diff --git a/backend/reviews/serializers.py b/backend/reviews/serializers.py index 20ec6dc..13e044c 100644 --- a/backend/reviews/serializers.py +++ b/backend/reviews/serializers.py @@ -1,8 +1,19 @@ from rest_framework import serializers -from reviews.models import Review +from reviews.models import Review, ReviewEocSpecific, ReviewDocument class ReviewSerializer(serializers.ModelSerializer): class Meta: model = Review fields = "__all__" + +class ReviewEOCSpecificSerializer(serializers.ModelSerializer): + class Meta: + model = ReviewEocSpecific + fields = "__all__" + + +class ReviewDocumentSerializer(serializers.ModelSerializer): + class Meta: + model = ReviewDocument + fields = "__all__" \ No newline at end of file diff --git a/backend/reviews/views.py b/backend/reviews/views.py index 6cab2f3..64e5b46 100755 --- a/backend/reviews/views.py +++ b/backend/reviews/views.py @@ -2,8 +2,8 @@ from rest_framework.exceptions import ValidationError from rest_framework.response import Response -from reviews.models import Review -from reviews.serializers import ReviewSerializer +from reviews.models import Review, ReviewDocument, ReviewEocSpecific +from reviews.serializers import ReviewSerializer, ReviewDocumentSerializer, ReviewEOCSpecificSerializer # Create your views here. @@ -15,3 +15,24 @@ class ReviewsViewSet(viewsets.ModelViewSet): queryset = Review.objects.all() def get_serializer(self, *args, **kwargs): return ReviewSerializer(*args, **kwargs) + + +class ReviewDocumentViewSet(viewsets.ModelViewSet): + """ + Viewset that handles the following + """ + + queryset = ReviewDocument.objects.all() + def get_serializer(self, *args, **kwargs): + return ReviewDocumentSerializer(*args, **kwargs) + + +class ReviewEocSpecificViewSet(viewsets.ModelViewSet): + """ + Viewset that handles the following + """ + + queryset = ReviewEocSpecific.objects.all() + def get_serializer(self, *args, **kwargs): + return ReviewEOCSpecificSerializer(*args, **kwargs) + From 8a6c5ccfd8c5d148daa1cefb027402e6c9b3f2f7 Mon Sep 17 00:00:00 2001 From: Michael Nef Date: Tue, 19 Jul 2022 16:23:57 +0800 Subject: [PATCH 34/89] document and eocspecific review migrations --- .../migrations/0002_auto_20220719_1622.py | 45 +++++++++++++++++++ 1 file changed, 45 insertions(+) create mode 100644 backend/reviews/migrations/0002_auto_20220719_1622.py diff --git a/backend/reviews/migrations/0002_auto_20220719_1622.py b/backend/reviews/migrations/0002_auto_20220719_1622.py new file mode 100644 index 0000000..907fbae --- /dev/null +++ b/backend/reviews/migrations/0002_auto_20220719_1622.py @@ -0,0 +1,45 @@ +# Generated by Django 3.2.13 on 2022-07-19 08:22 + +from django.db import migrations, models +import django.db.models.deletion +import django.utils.timezone +import uuid + + +class Migration(migrations.Migration): + + dependencies = [ + ('documents', '0001_initial'), + ('reviews', '0001_initial'), + ] + + operations = [ + migrations.AddField( + model_name='revieweocspecific', + name='created_at', + field=models.DateTimeField(auto_now_add=True, default=django.utils.timezone.now), + preserve_default=False, + ), + migrations.AddField( + model_name='revieweocspecific', + name='review', + field=models.ForeignKey(null=True, on_delete=django.db.models.deletion.CASCADE, related_name='eoc_specific_reviews', to='reviews.review'), + ), + migrations.AddField( + model_name='revieweocspecific', + name='updated_at', + field=models.DateTimeField(auto_now=True), + ), + migrations.CreateModel( + name='ReviewDocument', + fields=[ + ('id', models.UUIDField(default=uuid.uuid4, editable=False, primary_key=True, serialize=False)), + ('is_viewed', models.BooleanField()), + ('comment', models.TextField()), + ('created_at', models.DateTimeField(auto_now_add=True)), + ('updated_at', models.DateTimeField(auto_now=True)), + ('document', models.ForeignKey(on_delete=django.db.models.deletion.CASCADE, related_name='review_messages', to='documents.document')), + ('review', models.ForeignKey(on_delete=django.db.models.deletion.CASCADE, related_name='documents', to='reviews.review')), + ], + ), + ] From 3aac48a812ed3dde8a5447b66821ceebbfcd7d7c Mon Sep 17 00:00:00 2001 From: Michael Nef Date: Tue, 19 Jul 2022 16:41:07 +0800 Subject: [PATCH 35/89] ongoing reviews list component --- .../Reviewer/OngoingReviewsList.tsx | 39 +++++++++++++++++++ 1 file changed, 39 insertions(+) create mode 100644 frontend/components/Reviewer/OngoingReviewsList.tsx diff --git a/frontend/components/Reviewer/OngoingReviewsList.tsx b/frontend/components/Reviewer/OngoingReviewsList.tsx new file mode 100644 index 0000000..c4c3333 --- /dev/null +++ b/frontend/components/Reviewer/OngoingReviewsList.tsx @@ -0,0 +1,39 @@ +import * as React from 'react'; +import {useState, useEffect} from 'react'; + +type ReviewsListProps = { + showArchived: boolean; +} + +function getCourseEvaluations() { + return [ + { + 'id': 'f93046e0-bb2e-4043-b188-169aa78c7da9', + 'unit_code': 'geng1234', + 'description': 'a unit about important engineering stuff', + 'eoc_set': 'IAP Mechanical', + } + + ] +} + +const OngoingReviewsList = ({ showArchived }: ReviewsListProps): JSX.Element => { + const [reviews, setReviews] = useState({}); + + useEffect(() => { + const loadedReviews = getCourseEvaluations(); + const reviewPro + + setReviews(loadedReviews); + }, []) + + + + return ( + <> + {} + + ); +} + +export default OngoingReviewsList; From 48b90d4690472733f773022853d7ef1945149af9 Mon Sep 17 00:00:00 2001 From: Michael Nef Date: Thu, 21 Jul 2022 08:16:36 +0800 Subject: [PATCH 36/89] removed unecessary import --- backend/reviews/models.py | 1 - 1 file changed, 1 deletion(-) diff --git a/backend/reviews/models.py b/backend/reviews/models.py index 50994d3..3b0adf0 100755 --- a/backend/reviews/models.py +++ b/backend/reviews/models.py @@ -1,4 +1,3 @@ -from re import I import uuid # Create your models here. From ea690253ccca48073853435829eaae53ebb65e5a Mon Sep 17 00:00:00 2001 From: Michael Nef Date: Thu, 21 Jul 2022 08:16:53 +0800 Subject: [PATCH 37/89] added course evaluation justifications --- backend/course_evaluations/admin.py | 9 ++++++++- backend/course_evaluations/models.py | 15 ++++++++++++--- backend/course_evaluations/serializers.py | 10 +++++++++- backend/course_evaluations/views.py | 2 +- 4 files changed, 30 insertions(+), 6 deletions(-) diff --git a/backend/course_evaluations/admin.py b/backend/course_evaluations/admin.py index 37a0394..88d88de 100644 --- a/backend/course_evaluations/admin.py +++ b/backend/course_evaluations/admin.py @@ -1,5 +1,5 @@ from django.contrib import admin -from course_evaluations.models import CourseEvaluation, EOCGeneral, EOCSet, EOCSpecific +from course_evaluations.models import CourseEvaluation, EOCGeneral, EOCSet, EOCSpecific, CourseEvaluationJustification @admin.register(EOCSet) @@ -38,6 +38,13 @@ class EOCSpecificAdmin(admin.ModelAdmin): ordering = ("number",) +@admin.register(CourseEvaluationJustification) +class CourseEvaluationJustificationAdmin(admin.ModelAdmin): + list_display = ("id", "course_evaluation", "justification") + list_filter = ("course_evaluation",) + search_fields = ("id", "justification") + ordering = ("id",) + @admin.register(CourseEvaluation) class CourseEvaluationAdmin(admin.ModelAdmin): list_display = ("id", "unit_code", "description", "eoc_set") diff --git a/backend/course_evaluations/models.py b/backend/course_evaluations/models.py index f9b88fc..6ca4448 100644 --- a/backend/course_evaluations/models.py +++ b/backend/course_evaluations/models.py @@ -4,7 +4,6 @@ from django.contrib.postgres.fields import ArrayField from django.db import models - class EOCSet(models.Model): """ EOCSet is a group of EOCs (Elements of Competencies) @@ -72,12 +71,22 @@ class CourseEvaluation(models.Model): # Many-to-many Relationship with the Django User model # related_name: Allows to reference `CourseEvaluation` from User model coordinators = models.ManyToManyField("auth.User", related_name="course_evaluation_coordinator") - eoc_set = models.ForeignKey(EOCSet, on_delete=models.CASCADE) - created_at = models.DateTimeField(auto_now_add=True) updated_at = models.DateTimeField(auto_now=True) def __str__(self): return f"{self.eoc_set.name} - {self.unit_code} ({self.created_at})" + +class CourseEvaluationJustification(models.Model): + """ + A course coodinator must provide some written justification for how their course + is acheiving a variety of EOCs. It may be the case that a single written justification + can be used for multiple different EOC specifics. + """ + + course_evaluation = models.ForeignKey(CourseEvaluation, on_delete=models.CASCADE) + eoc_specific = models.ForeignKey(EOCSpecific, on_delete=models.CASCADE) + justification = models.TextField(null=False, blank=True) + development_level = models.IntegerChoices("DevelopmentLevel", "1 2 3 4 5") \ No newline at end of file diff --git a/backend/course_evaluations/serializers.py b/backend/course_evaluations/serializers.py index 48a131d..2a08d02 100644 --- a/backend/course_evaluations/serializers.py +++ b/backend/course_evaluations/serializers.py @@ -1,7 +1,7 @@ from django.contrib.auth.models import User from rest_framework import serializers -from course_evaluations.models import CourseEvaluation, EOCGeneral, EOCSet, EOCSpecific +from course_evaluations.models import CourseEvaluation, EOCGeneral, EOCSet, EOCSpecific, CourseEvaluationJustification class UserSerializer(serializers.ModelSerializer): @@ -45,11 +45,19 @@ class Meta: ) +class CourseEvaluationJustificationSerializer(serializers.ModelSerializer): + class Meta: + model = CourseEvaluationJustification + fields = "__all__" + + class CourseEvaluationDetailSerializer(serializers.ModelSerializer): eoc_set = EOCSetSerializer(read_only=True) coordinators = UserSerializer(many=True, read_only=True) + course_evalution_justifications = CourseEvaluationJustificationSerializer(many=True, read_only=True) eoc_set_id = serializers.IntegerField(required=True) + class Meta: model = CourseEvaluation diff --git a/backend/course_evaluations/views.py b/backend/course_evaluations/views.py index 9fe64fc..3120e76 100644 --- a/backend/course_evaluations/views.py +++ b/backend/course_evaluations/views.py @@ -8,9 +8,9 @@ CourseEvaluationDetailSerializer, CourseEvaluationListSerializer, EOCSet, + CourseEvaluationJustificationSerializer, ) - class CourseEvaluationViewSet(viewsets.ModelViewSet): """ Viewset that handles the following From 6d4ea52bb686fb77a0f860b25309df73690dc49d Mon Sep 17 00:00:00 2001 From: Michael Nef Date: Thu, 21 Jul 2022 08:17:10 +0800 Subject: [PATCH 38/89] migrations for courseevaluationsjustifications --- .../0002_courseevaluationjustification.py | 23 +++++++++++++++++++ 1 file changed, 23 insertions(+) create mode 100644 backend/course_evaluations/migrations/0002_courseevaluationjustification.py diff --git a/backend/course_evaluations/migrations/0002_courseevaluationjustification.py b/backend/course_evaluations/migrations/0002_courseevaluationjustification.py new file mode 100644 index 0000000..0e29700 --- /dev/null +++ b/backend/course_evaluations/migrations/0002_courseevaluationjustification.py @@ -0,0 +1,23 @@ +# Generated by Django 3.2.13 on 2022-07-19 10:25 + +from django.db import migrations, models +import django.db.models.deletion + + +class Migration(migrations.Migration): + + dependencies = [ + ('course_evaluations', '0001_initial'), + ] + + operations = [ + migrations.CreateModel( + name='CourseEvaluationJustification', + fields=[ + ('id', models.BigAutoField(auto_created=True, primary_key=True, serialize=False, verbose_name='ID')), + ('justification', models.TextField(blank=True)), + ('course_evaluation', models.ForeignKey(on_delete=django.db.models.deletion.CASCADE, to='course_evaluations.courseevaluation')), + ('eoc_specific', models.ForeignKey(on_delete=django.db.models.deletion.CASCADE, to='course_evaluations.eocspecific')), + ], + ), + ] From 02ca60b8e8c9b6d7accaf929a2cf26a9793fed5e Mon Sep 17 00:00:00 2001 From: Frinze Erin Lapuz Date: Tue, 16 Aug 2022 12:14:24 +0800 Subject: [PATCH 39/89] reorganise django installed apps --- backend/config/settings/base.py | 7 ++----- 1 file changed, 2 insertions(+), 5 deletions(-) diff --git a/backend/config/settings/base.py b/backend/config/settings/base.py index 65cf111..aaeaa9f 100644 --- a/backend/config/settings/base.py +++ b/backend/config/settings/base.py @@ -66,6 +66,8 @@ "django_filters", "commands", "course_evaluations", + "reviews", + "documents", # Authentication "rest_framework", "rest_framework.authtoken", @@ -76,11 +78,6 @@ "allauth.account", "allauth.socialaccount", "allauth.socialaccount.providers.google", - "django_filters", - "commands", - "course_evaluations", - "reviews", - "documents", ] # Refer to https://dj-rest-auth.readthedocs.io/en/latest/installation.html#registration-optional From eed2dc41c422303eb823dbaecaa35aefe81faa27 Mon Sep 17 00:00:00 2001 From: Frinze Erin Lapuz Date: Tue, 16 Aug 2022 12:14:27 +0800 Subject: [PATCH 40/89] linting --- backend/config/settings/base.py | 8 ++++++-- 1 file changed, 6 insertions(+), 2 deletions(-) diff --git a/backend/config/settings/base.py b/backend/config/settings/base.py index aaeaa9f..505497e 100644 --- a/backend/config/settings/base.py +++ b/backend/config/settings/base.py @@ -37,7 +37,9 @@ # Build paths inside the project like this: BASE_DIR / 'subdir'. BASE_DIR = Path(__file__).resolve().parent.parent # <-- '/config -PROJECT_ROOT = os.path.dirname(os.path.dirname(os.path.dirname(os.path.abspath(__file__)))) # <- '/' directory +PROJECT_ROOT = os.path.dirname( + os.path.dirname(os.path.dirname(os.path.abspath(__file__))) +) # <- '/' directory # Quick-start development settings - unsuitable for production # See https://docs.djangoproject.com/en/3.1/howto/deployment/checklist/ @@ -277,7 +279,9 @@ "disable_existing_loggers": False, "filters": {"request_id": {"()": "log_request_id.filters.RequestIDFilter"}}, "formatters": { - "standard": {"format": "[%(asctime)s] [%(request_id)s] %(levelname)s [%(name)s:%(lineno)s] %(message)s"}, + "standard": { + "format": "[%(asctime)s] [%(request_id)s] %(levelname)s [%(name)s:%(lineno)s] %(message)s" + }, }, "handlers": { "console": { From 6ed1940721a00f5d9698cc93ec4f558259ec1810 Mon Sep 17 00:00:00 2001 From: Frinze Erin Lapuz Date: Tue, 16 Aug 2022 12:19:23 +0800 Subject: [PATCH 41/89] running the linter and sorter --- backend/config/settings/base.py | 8 +--- backend/config/urls.py | 2 +- backend/course_evaluations/admin.py | 10 ++++- .../0002_courseevaluationjustification.py | 14 +++---- backend/course_evaluations/models.py | 5 ++- backend/course_evaluations/serializers.py | 10 +++-- .../tests/tests_permissions.py | 2 +- backend/course_evaluations/views.py | 3 +- backend/documents/admin.py | 2 + backend/documents/apps.py | 4 +- backend/documents/migrations/0001_initial.py | 21 +++++----- backend/documents/models.py | 6 ++- backend/documents/serializers.py | 3 +- backend/documents/urls.py | 1 - backend/documents/views.py | 8 ++-- backend/reviews/admin.py | 3 +- backend/reviews/apps.py | 4 +- backend/reviews/migrations/0001_initial.py | 33 ++++++++-------- .../migrations/0002_auto_20220719_1622.py | 39 ++++++++++--------- backend/reviews/models.py | 8 ++-- backend/reviews/serializers.py | 8 ++-- backend/reviews/views.py | 11 +++++- 22 files changed, 115 insertions(+), 90 deletions(-) diff --git a/backend/config/settings/base.py b/backend/config/settings/base.py index 505497e..aaeaa9f 100644 --- a/backend/config/settings/base.py +++ b/backend/config/settings/base.py @@ -37,9 +37,7 @@ # Build paths inside the project like this: BASE_DIR / 'subdir'. BASE_DIR = Path(__file__).resolve().parent.parent # <-- '/config -PROJECT_ROOT = os.path.dirname( - os.path.dirname(os.path.dirname(os.path.abspath(__file__))) -) # <- '/' directory +PROJECT_ROOT = os.path.dirname(os.path.dirname(os.path.dirname(os.path.abspath(__file__)))) # <- '/' directory # Quick-start development settings - unsuitable for production # See https://docs.djangoproject.com/en/3.1/howto/deployment/checklist/ @@ -279,9 +277,7 @@ "disable_existing_loggers": False, "filters": {"request_id": {"()": "log_request_id.filters.RequestIDFilter"}}, "formatters": { - "standard": { - "format": "[%(asctime)s] [%(request_id)s] %(levelname)s [%(name)s:%(lineno)s] %(message)s" - }, + "standard": {"format": "[%(asctime)s] [%(request_id)s] %(levelname)s [%(name)s:%(lineno)s] %(message)s"}, }, "handlers": { "console": { diff --git a/backend/config/urls.py b/backend/config/urls.py index c5bc8db..17f4fbc 100644 --- a/backend/config/urls.py +++ b/backend/config/urls.py @@ -24,7 +24,7 @@ include( ("documents.urls", "documents"), namespace="documents", - ) + ), ), ], "api", diff --git a/backend/course_evaluations/admin.py b/backend/course_evaluations/admin.py index 88d88de..94ebd8b 100644 --- a/backend/course_evaluations/admin.py +++ b/backend/course_evaluations/admin.py @@ -1,5 +1,12 @@ from django.contrib import admin -from course_evaluations.models import CourseEvaluation, EOCGeneral, EOCSet, EOCSpecific, CourseEvaluationJustification + +from course_evaluations.models import ( + CourseEvaluation, + CourseEvaluationJustification, + EOCGeneral, + EOCSet, + EOCSpecific, +) @admin.register(EOCSet) @@ -45,6 +52,7 @@ class CourseEvaluationJustificationAdmin(admin.ModelAdmin): search_fields = ("id", "justification") ordering = ("id",) + @admin.register(CourseEvaluation) class CourseEvaluationAdmin(admin.ModelAdmin): list_display = ("id", "unit_code", "description", "eoc_set") diff --git a/backend/course_evaluations/migrations/0002_courseevaluationjustification.py b/backend/course_evaluations/migrations/0002_courseevaluationjustification.py index 0e29700..778bd4a 100644 --- a/backend/course_evaluations/migrations/0002_courseevaluationjustification.py +++ b/backend/course_evaluations/migrations/0002_courseevaluationjustification.py @@ -1,23 +1,23 @@ # Generated by Django 3.2.13 on 2022-07-19 10:25 -from django.db import migrations, models import django.db.models.deletion +from django.db import migrations, models class Migration(migrations.Migration): dependencies = [ - ('course_evaluations', '0001_initial'), + ("course_evaluations", "0001_initial"), ] operations = [ migrations.CreateModel( - name='CourseEvaluationJustification', + name="CourseEvaluationJustification", fields=[ - ('id', models.BigAutoField(auto_created=True, primary_key=True, serialize=False, verbose_name='ID')), - ('justification', models.TextField(blank=True)), - ('course_evaluation', models.ForeignKey(on_delete=django.db.models.deletion.CASCADE, to='course_evaluations.courseevaluation')), - ('eoc_specific', models.ForeignKey(on_delete=django.db.models.deletion.CASCADE, to='course_evaluations.eocspecific')), + ("id", models.BigAutoField(auto_created=True, primary_key=True, serialize=False, verbose_name="ID")), + ("justification", models.TextField(blank=True)), + ("course_evaluation", models.ForeignKey(on_delete=django.db.models.deletion.CASCADE, to="course_evaluations.courseevaluation")), + ("eoc_specific", models.ForeignKey(on_delete=django.db.models.deletion.CASCADE, to="course_evaluations.eocspecific")), ], ), ] diff --git a/backend/course_evaluations/models.py b/backend/course_evaluations/models.py index 6ca4448..a470393 100644 --- a/backend/course_evaluations/models.py +++ b/backend/course_evaluations/models.py @@ -4,6 +4,7 @@ from django.contrib.postgres.fields import ArrayField from django.db import models + class EOCSet(models.Model): """ EOCSet is a group of EOCs (Elements of Competencies) @@ -87,6 +88,6 @@ class CourseEvaluationJustification(models.Model): """ course_evaluation = models.ForeignKey(CourseEvaluation, on_delete=models.CASCADE) - eoc_specific = models.ForeignKey(EOCSpecific, on_delete=models.CASCADE) + eoc_specific = models.ManyToManyField(EOCSpecific, related_name="justification") justification = models.TextField(null=False, blank=True) - development_level = models.IntegerChoices("DevelopmentLevel", "1 2 3 4 5") \ No newline at end of file + development_level = models.IntegerChoices("DevelopmentLevel", "1 2 3 4 5") diff --git a/backend/course_evaluations/serializers.py b/backend/course_evaluations/serializers.py index 2a08d02..617ff5f 100644 --- a/backend/course_evaluations/serializers.py +++ b/backend/course_evaluations/serializers.py @@ -1,7 +1,13 @@ from django.contrib.auth.models import User from rest_framework import serializers -from course_evaluations.models import CourseEvaluation, EOCGeneral, EOCSet, EOCSpecific, CourseEvaluationJustification +from course_evaluations.models import ( + CourseEvaluation, + CourseEvaluationJustification, + EOCGeneral, + EOCSet, + EOCSpecific, +) class UserSerializer(serializers.ModelSerializer): @@ -57,9 +63,7 @@ class CourseEvaluationDetailSerializer(serializers.ModelSerializer): course_evalution_justifications = CourseEvaluationJustificationSerializer(many=True, read_only=True) eoc_set_id = serializers.IntegerField(required=True) - class Meta: model = CourseEvaluation fields = "__all__" - diff --git a/backend/course_evaluations/tests/tests_permissions.py b/backend/course_evaluations/tests/tests_permissions.py index c16bc4e..c7da496 100644 --- a/backend/course_evaluations/tests/tests_permissions.py +++ b/backend/course_evaluations/tests/tests_permissions.py @@ -1,11 +1,11 @@ """ This test file is focused on permission testing. This is to ensure that unauthorised access cannot use the API """ +import pytest from django.urls import reverse from rest_framework import status from course_evaluations.models import CourseEvaluation -import pytest @pytest.mark.django_db diff --git a/backend/course_evaluations/views.py b/backend/course_evaluations/views.py index 3120e76..acb871e 100644 --- a/backend/course_evaluations/views.py +++ b/backend/course_evaluations/views.py @@ -6,11 +6,12 @@ from course_evaluations.permissions import IsCoordinatorAllowAll from course_evaluations.serializers import ( CourseEvaluationDetailSerializer, + CourseEvaluationJustificationSerializer, CourseEvaluationListSerializer, EOCSet, - CourseEvaluationJustificationSerializer, ) + class CourseEvaluationViewSet(viewsets.ModelViewSet): """ Viewset that handles the following diff --git a/backend/documents/admin.py b/backend/documents/admin.py index 8df77a0..81a4118 100644 --- a/backend/documents/admin.py +++ b/backend/documents/admin.py @@ -1,8 +1,10 @@ from django.contrib import admin + from documents.models import Document # Register your models here. + @admin.register(Document) class DocumentAdmin(admin.ModelAdmin): list_display = ("id", "url", "is_introduction") diff --git a/backend/documents/apps.py b/backend/documents/apps.py index ab8a093..37ce729 100644 --- a/backend/documents/apps.py +++ b/backend/documents/apps.py @@ -2,5 +2,5 @@ class DocumentsConfig(AppConfig): - default_auto_field = 'django.db.models.BigAutoField' - name = 'documents' + default_auto_field = "django.db.models.BigAutoField" + name = "documents" diff --git a/backend/documents/migrations/0001_initial.py b/backend/documents/migrations/0001_initial.py index 6a316e5..ca5f738 100644 --- a/backend/documents/migrations/0001_initial.py +++ b/backend/documents/migrations/0001_initial.py @@ -1,28 +1,29 @@ # Generated by Django 3.2.13 on 2022-07-19 07:45 -from django.db import migrations, models import uuid +from django.db import migrations, models + class Migration(migrations.Migration): initial = True dependencies = [ - ('course_evaluations', '0001_initial'), + ("course_evaluations", "0001_initial"), ] operations = [ migrations.CreateModel( - name='Document', + name="Document", fields=[ - ('id', models.UUIDField(default=uuid.uuid4, editable=False, primary_key=True, serialize=False)), - ('is_introduction', models.BooleanField()), - ('url', models.URLField()), - ('created_at', models.DateTimeField(auto_now_add=True)), - ('updated_at', models.DateTimeField(auto_now=True)), - ('eoc_generals', models.ManyToManyField(to='course_evaluations.EOCGeneral')), - ('eoc_specifics', models.ManyToManyField(to='course_evaluations.EOCSpecific')), + ("id", models.UUIDField(default=uuid.uuid4, editable=False, primary_key=True, serialize=False)), + ("is_introduction", models.BooleanField()), + ("url", models.URLField()), + ("created_at", models.DateTimeField(auto_now_add=True)), + ("updated_at", models.DateTimeField(auto_now=True)), + ("eoc_generals", models.ManyToManyField(to="course_evaluations.EOCGeneral")), + ("eoc_specifics", models.ManyToManyField(to="course_evaluations.EOCSpecific")), ], ), ] diff --git a/backend/documents/models.py b/backend/documents/models.py index f44017c..3a763c0 100644 --- a/backend/documents/models.py +++ b/backend/documents/models.py @@ -1,12 +1,14 @@ import uuid + from django.db import models # Create your models here. -from course_evaluations.models import EOCSpecific, EOCGeneral +from course_evaluations.models import EOCGeneral, EOCSpecific + class Document(models.Model): """ - A document is a url to a resource that a reviewer will look at + A document is a url to a resource that a reviewer will look at to judge the quality of a course """ diff --git a/backend/documents/serializers.py b/backend/documents/serializers.py index ada6729..ec813e7 100644 --- a/backend/documents/serializers.py +++ b/backend/documents/serializers.py @@ -2,9 +2,8 @@ from documents.models import Document + class DocumentSerializer(serializers.ModelSerializer): class Meta: model = Document fields = "__all__" - - diff --git a/backend/documents/urls.py b/backend/documents/urls.py index b8b86f1..3df12de 100644 --- a/backend/documents/urls.py +++ b/backend/documents/urls.py @@ -11,4 +11,3 @@ urlpatterns = [ path("", include(router.urls)), ] - diff --git a/backend/documents/views.py b/backend/documents/views.py index aac27ac..69165fb 100644 --- a/backend/documents/views.py +++ b/backend/documents/views.py @@ -4,6 +4,7 @@ from documents.models import Document from documents.serializers import DocumentSerializer + # Create your views here. @@ -13,8 +14,7 @@ class DocumentsViewSet(viewsets.ModelViewSet): """ queryset = Document.objects.all() - def get_serializer(self, *args, **kwargs): - """ - """ - return DocumentSerializer(*args, **kwargs) \ No newline at end of file + def get_serializer(self, *args, **kwargs): + """ """ + return DocumentSerializer(*args, **kwargs) diff --git a/backend/reviews/admin.py b/backend/reviews/admin.py index e53148a..2806417 100755 --- a/backend/reviews/admin.py +++ b/backend/reviews/admin.py @@ -1,6 +1,8 @@ from django.contrib import admin + from reviews.models import Review, ReviewDocument, ReviewEocSpecific + # Register your models here. @admin.register(Review) class ReviewAdmin(admin.ModelAdmin): @@ -18,4 +20,3 @@ class ReviewDocumentAdmin(admin.ModelAdmin): class ReviewEocSpecificAdmin(admin.ModelAdmin): list_display = ("id", "review", "eoc_specific", "development_level", "suggestion", "justification") ordering = ("created_at",) - diff --git a/backend/reviews/apps.py b/backend/reviews/apps.py index 80e0186..31ca513 100755 --- a/backend/reviews/apps.py +++ b/backend/reviews/apps.py @@ -2,5 +2,5 @@ class ReviewsConfig(AppConfig): - default_auto_field = 'django.db.models.BigAutoField' - name = 'reviews' + default_auto_field = "django.db.models.BigAutoField" + name = "reviews" diff --git a/backend/reviews/migrations/0001_initial.py b/backend/reviews/migrations/0001_initial.py index e56e852..6d00a12 100644 --- a/backend/reviews/migrations/0001_initial.py +++ b/backend/reviews/migrations/0001_initial.py @@ -1,9 +1,10 @@ # Generated by Django 3.2.13 on 2022-07-19 07:45 +import uuid + +import django.db.models.deletion from django.conf import settings from django.db import migrations, models -import django.db.models.deletion -import uuid class Migration(migrations.Migration): @@ -11,30 +12,30 @@ class Migration(migrations.Migration): initial = True dependencies = [ - ('course_evaluations', '0001_initial'), + ("course_evaluations", "0001_initial"), migrations.swappable_dependency(settings.AUTH_USER_MODEL), ] operations = [ migrations.CreateModel( - name='ReviewEocSpecific', + name="ReviewEocSpecific", fields=[ - ('id', models.UUIDField(default=uuid.uuid4, editable=False, primary_key=True, serialize=False)), - ('suggestion', models.TextField(blank=True)), - ('justification', models.TextField(blank=True)), - ('eoc_specific', models.ForeignKey(on_delete=django.db.models.deletion.CASCADE, to='course_evaluations.eocspecific')), + ("id", models.UUIDField(default=uuid.uuid4, editable=False, primary_key=True, serialize=False)), + ("suggestion", models.TextField(blank=True)), + ("justification", models.TextField(blank=True)), + ("eoc_specific", models.ForeignKey(on_delete=django.db.models.deletion.CASCADE, to="course_evaluations.eocspecific")), ], ), migrations.CreateModel( - name='Review', + name="Review", fields=[ - ('id', models.UUIDField(default=uuid.uuid4, editable=False, primary_key=True, serialize=False)), - ('final_comment', models.TextField(blank=True)), - ('date_submitted', models.DateTimeField()), - ('created_at', models.DateTimeField(auto_now_add=True)), - ('updated_at', models.DateTimeField(auto_now=True)), - ('coordinators', models.ForeignKey(on_delete=django.db.models.deletion.CASCADE, related_name='reviews', to=settings.AUTH_USER_MODEL)), - ('course_evaluation', models.ForeignKey(on_delete=django.db.models.deletion.CASCADE, to='course_evaluations.courseevaluation')), + ("id", models.UUIDField(default=uuid.uuid4, editable=False, primary_key=True, serialize=False)), + ("final_comment", models.TextField(blank=True)), + ("date_submitted", models.DateTimeField()), + ("created_at", models.DateTimeField(auto_now_add=True)), + ("updated_at", models.DateTimeField(auto_now=True)), + ("coordinators", models.ForeignKey(on_delete=django.db.models.deletion.CASCADE, related_name="reviews", to=settings.AUTH_USER_MODEL)), + ("course_evaluation", models.ForeignKey(on_delete=django.db.models.deletion.CASCADE, to="course_evaluations.courseevaluation")), ], ), ] diff --git a/backend/reviews/migrations/0002_auto_20220719_1622.py b/backend/reviews/migrations/0002_auto_20220719_1622.py index 907fbae..5e42f0c 100644 --- a/backend/reviews/migrations/0002_auto_20220719_1622.py +++ b/backend/reviews/migrations/0002_auto_20220719_1622.py @@ -1,45 +1,46 @@ # Generated by Django 3.2.13 on 2022-07-19 08:22 -from django.db import migrations, models +import uuid + import django.db.models.deletion import django.utils.timezone -import uuid +from django.db import migrations, models class Migration(migrations.Migration): dependencies = [ - ('documents', '0001_initial'), - ('reviews', '0001_initial'), + ("documents", "0001_initial"), + ("reviews", "0001_initial"), ] operations = [ migrations.AddField( - model_name='revieweocspecific', - name='created_at', + model_name="revieweocspecific", + name="created_at", field=models.DateTimeField(auto_now_add=True, default=django.utils.timezone.now), preserve_default=False, ), migrations.AddField( - model_name='revieweocspecific', - name='review', - field=models.ForeignKey(null=True, on_delete=django.db.models.deletion.CASCADE, related_name='eoc_specific_reviews', to='reviews.review'), + model_name="revieweocspecific", + name="review", + field=models.ForeignKey(null=True, on_delete=django.db.models.deletion.CASCADE, related_name="eoc_specific_reviews", to="reviews.review"), ), migrations.AddField( - model_name='revieweocspecific', - name='updated_at', + model_name="revieweocspecific", + name="updated_at", field=models.DateTimeField(auto_now=True), ), migrations.CreateModel( - name='ReviewDocument', + name="ReviewDocument", fields=[ - ('id', models.UUIDField(default=uuid.uuid4, editable=False, primary_key=True, serialize=False)), - ('is_viewed', models.BooleanField()), - ('comment', models.TextField()), - ('created_at', models.DateTimeField(auto_now_add=True)), - ('updated_at', models.DateTimeField(auto_now=True)), - ('document', models.ForeignKey(on_delete=django.db.models.deletion.CASCADE, related_name='review_messages', to='documents.document')), - ('review', models.ForeignKey(on_delete=django.db.models.deletion.CASCADE, related_name='documents', to='reviews.review')), + ("id", models.UUIDField(default=uuid.uuid4, editable=False, primary_key=True, serialize=False)), + ("is_viewed", models.BooleanField()), + ("comment", models.TextField()), + ("created_at", models.DateTimeField(auto_now_add=True)), + ("updated_at", models.DateTimeField(auto_now=True)), + ("document", models.ForeignKey(on_delete=django.db.models.deletion.CASCADE, related_name="review_messages", to="documents.document")), + ("review", models.ForeignKey(on_delete=django.db.models.deletion.CASCADE, related_name="documents", to="reviews.review")), ], ), ] diff --git a/backend/reviews/models.py b/backend/reviews/models.py index 3b0adf0..a987e7d 100755 --- a/backend/reviews/models.py +++ b/backend/reviews/models.py @@ -2,10 +2,11 @@ # Create your models here. from django.db import models -from course_evaluations.models import CourseEvaluation, EOCSpecific +from course_evaluations.models import CourseEvaluation, EOCSpecific from documents.models import Document + class Review(models.Model): """ A review is what Reviewers make. There are many reviews per @@ -46,18 +47,17 @@ class ReviewEocSpecific(models.Model): updated_at = models.DateTimeField(auto_now=True) - class ReviewDocument(models.Model): """ For each review, the reviewer may supply comments on different documents. ReviewDocument contains information regarding these comments. """ + id = models.UUIDField(primary_key=True, default=uuid.uuid4, editable=False) review = models.ForeignKey(Review, related_name="documents", on_delete=models.CASCADE) document = models.ForeignKey(Document, related_name="review_messages", on_delete=models.CASCADE) is_viewed = models.BooleanField() - comment = models.TextField() + comment = models.TextField() created_at = models.DateTimeField(auto_now_add=True) updated_at = models.DateTimeField(auto_now=True) - diff --git a/backend/reviews/serializers.py b/backend/reviews/serializers.py index 13e044c..9b7bee3 100644 --- a/backend/reviews/serializers.py +++ b/backend/reviews/serializers.py @@ -1,5 +1,7 @@ from rest_framework import serializers -from reviews.models import Review, ReviewEocSpecific, ReviewDocument + +from reviews.models import Review, ReviewDocument, ReviewEocSpecific + class ReviewSerializer(serializers.ModelSerializer): class Meta: @@ -11,9 +13,9 @@ class ReviewEOCSpecificSerializer(serializers.ModelSerializer): class Meta: model = ReviewEocSpecific fields = "__all__" - + class ReviewDocumentSerializer(serializers.ModelSerializer): class Meta: model = ReviewDocument - fields = "__all__" \ No newline at end of file + fields = "__all__" diff --git a/backend/reviews/views.py b/backend/reviews/views.py index 64e5b46..52d684c 100755 --- a/backend/reviews/views.py +++ b/backend/reviews/views.py @@ -3,16 +3,22 @@ from rest_framework.response import Response from reviews.models import Review, ReviewDocument, ReviewEocSpecific -from reviews.serializers import ReviewSerializer, ReviewDocumentSerializer, ReviewEOCSpecificSerializer +from reviews.serializers import ( + ReviewDocumentSerializer, + ReviewEOCSpecificSerializer, + ReviewSerializer, +) # Create your views here. + class ReviewsViewSet(viewsets.ModelViewSet): """ Viewset that handles the following """ queryset = Review.objects.all() + def get_serializer(self, *args, **kwargs): return ReviewSerializer(*args, **kwargs) @@ -23,6 +29,7 @@ class ReviewDocumentViewSet(viewsets.ModelViewSet): """ queryset = ReviewDocument.objects.all() + def get_serializer(self, *args, **kwargs): return ReviewDocumentSerializer(*args, **kwargs) @@ -33,6 +40,6 @@ class ReviewEocSpecificViewSet(viewsets.ModelViewSet): """ queryset = ReviewEocSpecific.objects.all() + def get_serializer(self, *args, **kwargs): return ReviewEOCSpecificSerializer(*args, **kwargs) - From 8c281ca3715d53a8f14de6d74a6bc0d4e3af5f91 Mon Sep 17 00:00:00 2001 From: Frinze Erin Lapuz Date: Tue, 16 Aug 2022 13:18:51 +0800 Subject: [PATCH 42/89] resetting migrations for this changes --- .../0002_courseevaluationjustification.py | 23 ---------- backend/reviews/migrations/0001_initial.py | 41 ----------------- .../migrations/0002_auto_20220719_1622.py | 46 ------------------- 3 files changed, 110 deletions(-) delete mode 100644 backend/course_evaluations/migrations/0002_courseevaluationjustification.py delete mode 100644 backend/reviews/migrations/0001_initial.py delete mode 100644 backend/reviews/migrations/0002_auto_20220719_1622.py diff --git a/backend/course_evaluations/migrations/0002_courseevaluationjustification.py b/backend/course_evaluations/migrations/0002_courseevaluationjustification.py deleted file mode 100644 index 778bd4a..0000000 --- a/backend/course_evaluations/migrations/0002_courseevaluationjustification.py +++ /dev/null @@ -1,23 +0,0 @@ -# Generated by Django 3.2.13 on 2022-07-19 10:25 - -import django.db.models.deletion -from django.db import migrations, models - - -class Migration(migrations.Migration): - - dependencies = [ - ("course_evaluations", "0001_initial"), - ] - - operations = [ - migrations.CreateModel( - name="CourseEvaluationJustification", - fields=[ - ("id", models.BigAutoField(auto_created=True, primary_key=True, serialize=False, verbose_name="ID")), - ("justification", models.TextField(blank=True)), - ("course_evaluation", models.ForeignKey(on_delete=django.db.models.deletion.CASCADE, to="course_evaluations.courseevaluation")), - ("eoc_specific", models.ForeignKey(on_delete=django.db.models.deletion.CASCADE, to="course_evaluations.eocspecific")), - ], - ), - ] diff --git a/backend/reviews/migrations/0001_initial.py b/backend/reviews/migrations/0001_initial.py deleted file mode 100644 index 6d00a12..0000000 --- a/backend/reviews/migrations/0001_initial.py +++ /dev/null @@ -1,41 +0,0 @@ -# Generated by Django 3.2.13 on 2022-07-19 07:45 - -import uuid - -import django.db.models.deletion -from django.conf import settings -from django.db import migrations, models - - -class Migration(migrations.Migration): - - initial = True - - dependencies = [ - ("course_evaluations", "0001_initial"), - migrations.swappable_dependency(settings.AUTH_USER_MODEL), - ] - - operations = [ - migrations.CreateModel( - name="ReviewEocSpecific", - fields=[ - ("id", models.UUIDField(default=uuid.uuid4, editable=False, primary_key=True, serialize=False)), - ("suggestion", models.TextField(blank=True)), - ("justification", models.TextField(blank=True)), - ("eoc_specific", models.ForeignKey(on_delete=django.db.models.deletion.CASCADE, to="course_evaluations.eocspecific")), - ], - ), - migrations.CreateModel( - name="Review", - fields=[ - ("id", models.UUIDField(default=uuid.uuid4, editable=False, primary_key=True, serialize=False)), - ("final_comment", models.TextField(blank=True)), - ("date_submitted", models.DateTimeField()), - ("created_at", models.DateTimeField(auto_now_add=True)), - ("updated_at", models.DateTimeField(auto_now=True)), - ("coordinators", models.ForeignKey(on_delete=django.db.models.deletion.CASCADE, related_name="reviews", to=settings.AUTH_USER_MODEL)), - ("course_evaluation", models.ForeignKey(on_delete=django.db.models.deletion.CASCADE, to="course_evaluations.courseevaluation")), - ], - ), - ] diff --git a/backend/reviews/migrations/0002_auto_20220719_1622.py b/backend/reviews/migrations/0002_auto_20220719_1622.py deleted file mode 100644 index 5e42f0c..0000000 --- a/backend/reviews/migrations/0002_auto_20220719_1622.py +++ /dev/null @@ -1,46 +0,0 @@ -# Generated by Django 3.2.13 on 2022-07-19 08:22 - -import uuid - -import django.db.models.deletion -import django.utils.timezone -from django.db import migrations, models - - -class Migration(migrations.Migration): - - dependencies = [ - ("documents", "0001_initial"), - ("reviews", "0001_initial"), - ] - - operations = [ - migrations.AddField( - model_name="revieweocspecific", - name="created_at", - field=models.DateTimeField(auto_now_add=True, default=django.utils.timezone.now), - preserve_default=False, - ), - migrations.AddField( - model_name="revieweocspecific", - name="review", - field=models.ForeignKey(null=True, on_delete=django.db.models.deletion.CASCADE, related_name="eoc_specific_reviews", to="reviews.review"), - ), - migrations.AddField( - model_name="revieweocspecific", - name="updated_at", - field=models.DateTimeField(auto_now=True), - ), - migrations.CreateModel( - name="ReviewDocument", - fields=[ - ("id", models.UUIDField(default=uuid.uuid4, editable=False, primary_key=True, serialize=False)), - ("is_viewed", models.BooleanField()), - ("comment", models.TextField()), - ("created_at", models.DateTimeField(auto_now_add=True)), - ("updated_at", models.DateTimeField(auto_now=True)), - ("document", models.ForeignKey(on_delete=django.db.models.deletion.CASCADE, related_name="review_messages", to="documents.document")), - ("review", models.ForeignKey(on_delete=django.db.models.deletion.CASCADE, related_name="documents", to="reviews.review")), - ], - ), - ] From dfbd5c1595a799d6710359a54bcd3fc570e49103 Mon Sep 17 00:00:00 2001 From: Frinze Erin Lapuz Date: Tue, 16 Aug 2022 13:19:56 +0800 Subject: [PATCH 43/89] corrections for the field and choices --- backend/course_evaluations/admin.py | 10 +++++++++- backend/course_evaluations/models.py | 22 ++++++++++++++++++++-- backend/reviews/models.py | 4 ++-- 3 files changed, 31 insertions(+), 5 deletions(-) diff --git a/backend/course_evaluations/admin.py b/backend/course_evaluations/admin.py index 94ebd8b..cd60442 100644 --- a/backend/course_evaluations/admin.py +++ b/backend/course_evaluations/admin.py @@ -39,7 +39,13 @@ class EOCGeneralAdmin(admin.ModelAdmin): @admin.register(EOCSpecific) class EOCSpecificAdmin(admin.ModelAdmin): - list_display = ("id", "number", "eoc_general", "get_general_and_specific_eoc", "description") + list_display = ( + "id", + "number", + "eoc_general", + "get_general_and_specific_eoc", + "description", + ) list_filter = ("eoc_general",) search_fields = ("id", "number", "description") ordering = ("number",) @@ -52,6 +58,8 @@ class CourseEvaluationJustificationAdmin(admin.ModelAdmin): search_fields = ("id", "justification") ordering = ("id",) + filter_horizontal = ("eoc_specifics",) + @admin.register(CourseEvaluation) class CourseEvaluationAdmin(admin.ModelAdmin): diff --git a/backend/course_evaluations/models.py b/backend/course_evaluations/models.py index a470393..7a90d9c 100644 --- a/backend/course_evaluations/models.py +++ b/backend/course_evaluations/models.py @@ -80,6 +80,20 @@ def __str__(self): return f"{self.eoc_set.name} - {self.unit_code} ({self.created_at})" +class DevelopmentLevels(models.IntegerChoices): + """ + Foundational - Developing a foundation for university level study + Broad and Coherent - Sufficient capability to enter the workforce as a non-engineer + Advanced - Sufficient capability for professional practice as a starting engineer + Specialist - Selected areas of strength beyond the requirement for entering professional practice + """ + + FOUNDATIONAL = 1 + BROAD_AND_COHERENT = 2 + ADVANCED = 3 + SPECIALIST = 4 + + class CourseEvaluationJustification(models.Model): """ A course coodinator must provide some written justification for how their course @@ -88,6 +102,10 @@ class CourseEvaluationJustification(models.Model): """ course_evaluation = models.ForeignKey(CourseEvaluation, on_delete=models.CASCADE) - eoc_specific = models.ManyToManyField(EOCSpecific, related_name="justification") + eoc_specifics = models.ManyToManyField(EOCSpecific, related_name="justification") justification = models.TextField(null=False, blank=True) - development_level = models.IntegerChoices("DevelopmentLevel", "1 2 3 4 5") + development_level = models.IntegerField(choices=DevelopmentLevels.choices) + + def __str__(self): + eoc_specifics = ", ".join([eoc_specific.get_general_and_specific_eoc() for eoc_specific in self.eoc_specifics.all()]) + return f"{self.course_evaluation.unit_code} - {eoc_specifics}" diff --git a/backend/reviews/models.py b/backend/reviews/models.py index a987e7d..f733a01 100755 --- a/backend/reviews/models.py +++ b/backend/reviews/models.py @@ -3,7 +3,7 @@ # Create your models here. from django.db import models -from course_evaluations.models import CourseEvaluation, EOCSpecific +from course_evaluations.models import CourseEvaluation, DevelopmentLevels, EOCSpecific from documents.models import Document @@ -38,7 +38,7 @@ class ReviewEocSpecific(models.Model): review = models.ForeignKey(Review, null=True, related_name="eoc_specific_reviews", on_delete=models.CASCADE) eoc_specific = models.ForeignKey(EOCSpecific, on_delete=models.CASCADE) - development_level = models.IntegerChoices("DevelopmentLevel", "1 2 3 4 5") + development_level = models.IntegerField(choices=DevelopmentLevels.choices) suggestion = models.TextField(null=False, blank=True) justification = models.TextField(null=False, blank=True) From f37c7996e8cb573028e028d9c79ebc5044da5b0b Mon Sep 17 00:00:00 2001 From: Frinze Erin Lapuz Date: Tue, 16 Aug 2022 13:20:26 +0800 Subject: [PATCH 44/89] recreate migrations --- .../0002_courseevaluationjustification.py | 27 ++++++++ backend/reviews/migrations/0001_initial.py | 66 +++++++++++++++++++ 2 files changed, 93 insertions(+) create mode 100644 backend/course_evaluations/migrations/0002_courseevaluationjustification.py create mode 100644 backend/reviews/migrations/0001_initial.py diff --git a/backend/course_evaluations/migrations/0002_courseevaluationjustification.py b/backend/course_evaluations/migrations/0002_courseevaluationjustification.py new file mode 100644 index 0000000..87eba2d --- /dev/null +++ b/backend/course_evaluations/migrations/0002_courseevaluationjustification.py @@ -0,0 +1,27 @@ +# Generated by Django 3.2.13 on 2022-08-16 05:20 + +import django.db.models.deletion +from django.db import migrations, models + + +class Migration(migrations.Migration): + + dependencies = [ + ("course_evaluations", "0001_initial"), + ] + + operations = [ + migrations.CreateModel( + name="CourseEvaluationJustification", + fields=[ + ("id", models.BigAutoField(auto_created=True, primary_key=True, serialize=False, verbose_name="ID")), + ("justification", models.TextField(blank=True)), + ( + "development_level", + models.IntegerField(choices=[(1, "Foundational"), (2, "Broad And Coherent"), (3, "Advanced"), (4, "Specialist")]), + ), + ("course_evaluation", models.ForeignKey(on_delete=django.db.models.deletion.CASCADE, to="course_evaluations.courseevaluation")), + ("eoc_specifics", models.ManyToManyField(related_name="justification", to="course_evaluations.EOCSpecific")), + ], + ), + ] diff --git a/backend/reviews/migrations/0001_initial.py b/backend/reviews/migrations/0001_initial.py new file mode 100644 index 0000000..603d4ce --- /dev/null +++ b/backend/reviews/migrations/0001_initial.py @@ -0,0 +1,66 @@ +# Generated by Django 3.2.13 on 2022-08-16 05:20 + +import uuid + +import django.db.models.deletion +from django.conf import settings +from django.db import migrations, models + + +class Migration(migrations.Migration): + + initial = True + + dependencies = [ + ("course_evaluations", "0002_courseevaluationjustification"), + migrations.swappable_dependency(settings.AUTH_USER_MODEL), + ("documents", "0001_initial"), + ] + + operations = [ + migrations.CreateModel( + name="Review", + fields=[ + ("id", models.UUIDField(default=uuid.uuid4, editable=False, primary_key=True, serialize=False)), + ("final_comment", models.TextField(blank=True)), + ("date_submitted", models.DateTimeField()), + ("created_at", models.DateTimeField(auto_now_add=True)), + ("updated_at", models.DateTimeField(auto_now=True)), + ("coordinators", models.ForeignKey(on_delete=django.db.models.deletion.CASCADE, related_name="reviews", to=settings.AUTH_USER_MODEL)), + ("course_evaluation", models.ForeignKey(on_delete=django.db.models.deletion.CASCADE, to="course_evaluations.courseevaluation")), + ], + ), + migrations.CreateModel( + name="ReviewEocSpecific", + fields=[ + ("id", models.UUIDField(default=uuid.uuid4, editable=False, primary_key=True, serialize=False)), + ( + "development_level", + models.IntegerField(choices=[(1, "Foundational"), (2, "Broad And Coherent"), (3, "Advanced"), (4, "Specialist")]), + ), + ("suggestion", models.TextField(blank=True)), + ("justification", models.TextField(blank=True)), + ("created_at", models.DateTimeField(auto_now_add=True)), + ("updated_at", models.DateTimeField(auto_now=True)), + ("eoc_specific", models.ForeignKey(on_delete=django.db.models.deletion.CASCADE, to="course_evaluations.eocspecific")), + ( + "review", + models.ForeignKey( + null=True, on_delete=django.db.models.deletion.CASCADE, related_name="eoc_specific_reviews", to="reviews.review" + ), + ), + ], + ), + migrations.CreateModel( + name="ReviewDocument", + fields=[ + ("id", models.UUIDField(default=uuid.uuid4, editable=False, primary_key=True, serialize=False)), + ("is_viewed", models.BooleanField()), + ("comment", models.TextField()), + ("created_at", models.DateTimeField(auto_now_add=True)), + ("updated_at", models.DateTimeField(auto_now=True)), + ("document", models.ForeignKey(on_delete=django.db.models.deletion.CASCADE, related_name="review_messages", to="documents.document")), + ("review", models.ForeignKey(on_delete=django.db.models.deletion.CASCADE, related_name="documents", to="reviews.review")), + ], + ), + ] From 77098120f3586af1405cb805ec09b0cec90cf117 Mon Sep 17 00:00:00 2001 From: Frinze Erin Lapuz Date: Tue, 16 Aug 2022 13:22:53 +0800 Subject: [PATCH 45/89] change string repr of the justification --- backend/course_evaluations/models.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/backend/course_evaluations/models.py b/backend/course_evaluations/models.py index 7a90d9c..8c43c32 100644 --- a/backend/course_evaluations/models.py +++ b/backend/course_evaluations/models.py @@ -108,4 +108,4 @@ class CourseEvaluationJustification(models.Model): def __str__(self): eoc_specifics = ", ".join([eoc_specific.get_general_and_specific_eoc() for eoc_specific in self.eoc_specifics.all()]) - return f"{self.course_evaluation.unit_code} - {eoc_specifics}" + return f"Course Evaluation Justification ({self.course_evaluation.unit_code}) - {eoc_specifics}" From 258e5d8ee188ecc28e53a284756acfc78689aef2 Mon Sep 17 00:00:00 2001 From: Frinze Erin Lapuz Date: Tue, 16 Aug 2022 13:39:31 +0800 Subject: [PATCH 46/89] relations and information about the documents --- backend/documents/models.py | 12 ++++++++---- 1 file changed, 8 insertions(+), 4 deletions(-) diff --git a/backend/documents/models.py b/backend/documents/models.py index 3a763c0..9f82a73 100644 --- a/backend/documents/models.py +++ b/backend/documents/models.py @@ -3,7 +3,7 @@ from django.db import models # Create your models here. -from course_evaluations.models import EOCGeneral, EOCSpecific +from course_evaluations.models import CourseEvaluation, EOCGeneral, EOCSpecific class Document(models.Model): @@ -12,14 +12,18 @@ class Document(models.Model): to judge the quality of a course """ - # UUID for the review id = models.UUIDField(primary_key=True, default=uuid.uuid4, editable=False) - is_introduction = models.BooleanField() + # Main relations + course_evaluation = models.ForeignKey(CourseEvaluation, on_delete=models.CASCADE) - # The actual URL to the resource + # Information about the document + name = models.CharField(max_length=50, null=False, blank=False) + description = models.TextField(null=False, blank=True) url = models.URLField() + # Tags Equivalence + is_introduction = models.BooleanField() eoc_generals = models.ManyToManyField(EOCGeneral) eoc_specifics = models.ManyToManyField(EOCSpecific) From 1d7b5bb5c782d92d1386f85d4a98c5b1bff491b8 Mon Sep 17 00:00:00 2001 From: Frinze Erin Lapuz Date: Tue, 16 Aug 2022 13:40:28 +0800 Subject: [PATCH 47/89] revise django admin for documents --- backend/documents/admin.py | 13 ++++++++++++- 1 file changed, 12 insertions(+), 1 deletion(-) diff --git a/backend/documents/admin.py b/backend/documents/admin.py index 81a4118..df358c2 100644 --- a/backend/documents/admin.py +++ b/backend/documents/admin.py @@ -7,4 +7,15 @@ @admin.register(Document) class DocumentAdmin(admin.ModelAdmin): - list_display = ("id", "url", "is_introduction") + list_display = ("id", "course_evaluation", "name", "url", "is_introduction") + search_fields = ("id", "name", "description", "justification") + + list_filter = ( + "course_evaluation", + "is_introduction", + ) + + filter_horizontal = ( + "eoc_generals", + "eoc_specifics", + ) From c46e52eec9b95a9ff39079706c7a31efb6923940 Mon Sep 17 00:00:00 2001 From: Frinze Erin Lapuz Date: Tue, 16 Aug 2022 13:46:55 +0800 Subject: [PATCH 48/89] permissions for editting documents --- backend/documents/permissions.py | 14 ++++++++++++++ backend/documents/views.py | 7 +++++++ 2 files changed, 21 insertions(+) create mode 100644 backend/documents/permissions.py diff --git a/backend/documents/permissions.py b/backend/documents/permissions.py new file mode 100644 index 0000000..5ee6c3e --- /dev/null +++ b/backend/documents/permissions.py @@ -0,0 +1,14 @@ +from rest_framework import permissions + + +class DocumentCoordinatorAllowAll(permissions.BasePermission): + """ + Custom permission to only allow coordinators the API + """ + + def has_object_permission(self, request, view, obj): + """ + A coordinator should be allowed to perform any operation + """ + + return request.user in obj.course_evaluation.coordinators.all() diff --git a/backend/documents/views.py b/backend/documents/views.py index 69165fb..632de12 100644 --- a/backend/documents/views.py +++ b/backend/documents/views.py @@ -3,6 +3,7 @@ from rest_framework.response import Response from documents.models import Document +from documents.permissions import DocumentCoordinatorAllowAll from documents.serializers import DocumentSerializer # Create your views here. @@ -11,9 +12,15 @@ class DocumentsViewSet(viewsets.ModelViewSet): """ Viewset that handles documents + + Permissions: + - Coordinator (ALL) + + Note: A reviewer should only see documents as part of their specific endpoint. See `reviews/serializers.py` or `reviews/views.py` """ queryset = Document.objects.all() + permission_classes = [DocumentCoordinatorAllowAll] def get_serializer(self, *args, **kwargs): """ """ From 85f0a8bf7211ea12fa2acdbfb6d93c31dd3f7c1b Mon Sep 17 00:00:00 2001 From: Frinze Erin Lapuz Date: Tue, 16 Aug 2022 14:16:20 +0800 Subject: [PATCH 49/89] listing for the documents we can just turn this off really... because we won't be using this --- backend/documents/views.py | 14 ++++++++++++++ 1 file changed, 14 insertions(+) diff --git a/backend/documents/views.py b/backend/documents/views.py index 632de12..155bf97 100644 --- a/backend/documents/views.py +++ b/backend/documents/views.py @@ -25,3 +25,17 @@ class DocumentsViewSet(viewsets.ModelViewSet): def get_serializer(self, *args, **kwargs): """ """ return DocumentSerializer(*args, **kwargs) + + def list(self, request, *args, **kwargs): + """ + List only the documents that the user is a coordinator + """ + queryset = self.filter_queryset(self.get_queryset()) + + # Find out all the IDs of the course evaluation at which the user is a coordinator + course_evaluation_ids = request.user.course_evaluation_coordinator.values_list("id", flat=True) + filtered_queryset = queryset.filter( + course_evaluation_id__in=course_evaluation_ids + ) + serializer = DocumentSerializer(filtered_queryset, many=True) + return Response(serializer.data) From 88b953a87af6004b178cb456f2554f82a42b584c Mon Sep 17 00:00:00 2001 From: Frinze Erin Lapuz Date: Tue, 16 Aug 2022 14:17:59 +0800 Subject: [PATCH 50/89] remove need of the endpoint --- backend/documents/views.py | 17 +++++++---------- 1 file changed, 7 insertions(+), 10 deletions(-) diff --git a/backend/documents/views.py b/backend/documents/views.py index 155bf97..2ad9f22 100644 --- a/backend/documents/views.py +++ b/backend/documents/views.py @@ -28,14 +28,11 @@ def get_serializer(self, *args, **kwargs): def list(self, request, *args, **kwargs): """ - List only the documents that the user is a coordinator + This is method is not needed. We will never be using a way to list all the documents that the user can view. + + If the role of the user is: + - Coordinator: the user should use the endpoint at `course_evaluations/views.py` + - Reviewer: the user should use the endpoint at `reviews/views.py` """ - queryset = self.filter_queryset(self.get_queryset()) - - # Find out all the IDs of the course evaluation at which the user is a coordinator - course_evaluation_ids = request.user.course_evaluation_coordinator.values_list("id", flat=True) - filtered_queryset = queryset.filter( - course_evaluation_id__in=course_evaluation_ids - ) - serializer = DocumentSerializer(filtered_queryset, many=True) - return Response(serializer.data) + return Response(status=status.HTTP_405_METHOD_NOT_ALLOWED) + From c01e62a01c99f325275ae0b7e3b6bf6e569921cd Mon Sep 17 00:00:00 2001 From: Frinze Erin Lapuz Date: Tue, 16 Aug 2022 14:25:11 +0800 Subject: [PATCH 51/89] more comment of the document --- backend/documents/views.py | 3 +-- 1 file changed, 1 insertion(+), 2 deletions(-) diff --git a/backend/documents/views.py b/backend/documents/views.py index 2ad9f22..6341730 100644 --- a/backend/documents/views.py +++ b/backend/documents/views.py @@ -14,7 +14,7 @@ class DocumentsViewSet(viewsets.ModelViewSet): Viewset that handles documents Permissions: - - Coordinator (ALL) + - Coordinator (DETAIL, CREATE, UPDATE, DELETE). Essentially for management of the documents for a particular course_evaluation Note: A reviewer should only see documents as part of their specific endpoint. See `reviews/serializers.py` or `reviews/views.py` """ @@ -35,4 +35,3 @@ def list(self, request, *args, **kwargs): - Reviewer: the user should use the endpoint at `reviews/views.py` """ return Response(status=status.HTTP_405_METHOD_NOT_ALLOWED) - From 899ad5761a3ef188dccc90a3f1eb54a04e55b5b0 Mon Sep 17 00:00:00 2001 From: Frinze Erin Lapuz Date: Tue, 16 Aug 2022 14:26:38 +0800 Subject: [PATCH 52/89] comment for the serializer --- backend/documents/serializers.py | 17 +++++++++++++++++ 1 file changed, 17 insertions(+) diff --git a/backend/documents/serializers.py b/backend/documents/serializers.py index ec813e7..ae16308 100644 --- a/backend/documents/serializers.py +++ b/backend/documents/serializers.py @@ -1,9 +1,26 @@ from rest_framework import serializers +from course_evaluations.models import ( + CourseEvaluation, + CourseEvaluationJustification, + EOCGeneral, + EOCSet, + EOCSpecific, +) from documents.models import Document class DocumentSerializer(serializers.ModelSerializer): + """ + Note: It is important to understand that this serializer is only used for write operations for the most parts. + + This means that `eoc_generals` and `eoc_specifics` are expecting the an id (not the EOC number) + """ + + # Note: `read_only=False` is important to do patching and creations + eoc_generals = serializers.PrimaryKeyRelatedField(many=True, read_only=False, queryset=EOCGeneral.objects.all()) + eoc_specifics = serializers.PrimaryKeyRelatedField(many=True, read_only=False, queryset=EOCSpecific.objects.all()) + class Meta: model = Document fields = "__all__" From 7374a85720c6ba42253e9320772bc447ccafee11 Mon Sep 17 00:00:00 2001 From: Frinze Erin Lapuz Date: Tue, 16 Aug 2022 14:26:50 +0800 Subject: [PATCH 53/89] renaming of the course_evaluation --- backend/course_evaluations/permissions.py | 2 +- backend/course_evaluations/views.py | 4 ++-- 2 files changed, 3 insertions(+), 3 deletions(-) diff --git a/backend/course_evaluations/permissions.py b/backend/course_evaluations/permissions.py index 511e458..5554a0a 100644 --- a/backend/course_evaluations/permissions.py +++ b/backend/course_evaluations/permissions.py @@ -1,7 +1,7 @@ from rest_framework import permissions -class IsCoordinatorAllowAll(permissions.BasePermission): +class CourseEvaluationIsCoordinatorAllowAll(permissions.BasePermission): """ Custom permission to only allow coordinators the API """ diff --git a/backend/course_evaluations/views.py b/backend/course_evaluations/views.py index acb871e..b0153d5 100644 --- a/backend/course_evaluations/views.py +++ b/backend/course_evaluations/views.py @@ -3,7 +3,7 @@ from rest_framework.response import Response from course_evaluations.models import CourseEvaluation -from course_evaluations.permissions import IsCoordinatorAllowAll +from course_evaluations.permissions import CourseEvaluationIsCoordinatorAllowAll from course_evaluations.serializers import ( CourseEvaluationDetailSerializer, CourseEvaluationJustificationSerializer, @@ -19,7 +19,7 @@ class CourseEvaluationViewSet(viewsets.ModelViewSet): """ queryset = CourseEvaluation.objects.all() - permission_classes = [permissions.IsAuthenticated, IsCoordinatorAllowAll] + permission_classes = [CourseEvaluationIsCoordinatorAllowAll] def get_serializer(self, *args, **kwargs): """ From 859cf0ceee19a955ad4e2b602d2588e21a6ff6ee Mon Sep 17 00:00:00 2001 From: Frinze Erin Lapuz Date: Tue, 16 Aug 2022 14:33:38 +0800 Subject: [PATCH 54/89] squash new migrations --- backend/documents/migrations/0001_initial.py | 10 +++++++--- backend/reviews/migrations/0001_initial.py | 4 ++-- 2 files changed, 9 insertions(+), 5 deletions(-) diff --git a/backend/documents/migrations/0001_initial.py b/backend/documents/migrations/0001_initial.py index ca5f738..a0611b6 100644 --- a/backend/documents/migrations/0001_initial.py +++ b/backend/documents/migrations/0001_initial.py @@ -1,7 +1,8 @@ -# Generated by Django 3.2.13 on 2022-07-19 07:45 +# Generated by Django 3.2.13 on 2022-08-16 06:32 import uuid +import django.db.models.deletion from django.db import migrations, models @@ -10,7 +11,7 @@ class Migration(migrations.Migration): initial = True dependencies = [ - ("course_evaluations", "0001_initial"), + ("course_evaluations", "0002_courseevaluationjustification"), ] operations = [ @@ -18,10 +19,13 @@ class Migration(migrations.Migration): name="Document", fields=[ ("id", models.UUIDField(default=uuid.uuid4, editable=False, primary_key=True, serialize=False)), - ("is_introduction", models.BooleanField()), + ("name", models.CharField(max_length=50)), + ("description", models.TextField(blank=True)), ("url", models.URLField()), + ("is_introduction", models.BooleanField()), ("created_at", models.DateTimeField(auto_now_add=True)), ("updated_at", models.DateTimeField(auto_now=True)), + ("course_evaluation", models.ForeignKey(on_delete=django.db.models.deletion.CASCADE, to="course_evaluations.courseevaluation")), ("eoc_generals", models.ManyToManyField(to="course_evaluations.EOCGeneral")), ("eoc_specifics", models.ManyToManyField(to="course_evaluations.EOCSpecific")), ], diff --git a/backend/reviews/migrations/0001_initial.py b/backend/reviews/migrations/0001_initial.py index 603d4ce..c1e13cf 100644 --- a/backend/reviews/migrations/0001_initial.py +++ b/backend/reviews/migrations/0001_initial.py @@ -1,4 +1,4 @@ -# Generated by Django 3.2.13 on 2022-08-16 05:20 +# Generated by Django 3.2.13 on 2022-08-16 06:32 import uuid @@ -12,9 +12,9 @@ class Migration(migrations.Migration): initial = True dependencies = [ - ("course_evaluations", "0002_courseevaluationjustification"), migrations.swappable_dependency(settings.AUTH_USER_MODEL), ("documents", "0001_initial"), + ("course_evaluations", "0002_courseevaluationjustification"), ] operations = [ From bd6a773bf40c33e2cc6c856b62d4d4446adfe3b9 Mon Sep 17 00:00:00 2001 From: Frinze Erin Lapuz Date: Thu, 18 Aug 2022 09:15:40 +0800 Subject: [PATCH 55/89] comment for the "write" of part of the eoc_set --- backend/course_evaluations/serializers.py | 1 + 1 file changed, 1 insertion(+) diff --git a/backend/course_evaluations/serializers.py b/backend/course_evaluations/serializers.py index 617ff5f..0b8d2ce 100644 --- a/backend/course_evaluations/serializers.py +++ b/backend/course_evaluations/serializers.py @@ -62,6 +62,7 @@ class CourseEvaluationDetailSerializer(serializers.ModelSerializer): coordinators = UserSerializer(many=True, read_only=True) course_evalution_justifications = CourseEvaluationJustificationSerializer(many=True, read_only=True) + # Note: This is used for write, by creating the `eoc_set` relationship eoc_set_id = serializers.IntegerField(required=True) class Meta: From 68b66515eba970dcb230bb66303931aaa317e8ab Mon Sep 17 00:00:00 2001 From: Frinze Erin Lapuz Date: Thu, 18 Aug 2022 09:55:13 +0800 Subject: [PATCH 56/89] fix test with object level permission --- backend/course_evaluations/permissions.py | 13 +++++++++++- .../tests/tests_permissions.py | 20 +++++++++++++++++++ 2 files changed, 32 insertions(+), 1 deletion(-) diff --git a/backend/course_evaluations/permissions.py b/backend/course_evaluations/permissions.py index 5554a0a..82bf5f5 100644 --- a/backend/course_evaluations/permissions.py +++ b/backend/course_evaluations/permissions.py @@ -6,9 +6,20 @@ class CourseEvaluationIsCoordinatorAllowAll(permissions.BasePermission): Custom permission to only allow coordinators the API """ + def has_permission(self, request, view): + """ + This permission applies on request level + """ + # Check if authenticated + if request.user is None or not request.user.is_authenticated: + return False + return True + def has_object_permission(self, request, view, obj): """ A coordinator should be allowed to perform any operation - """ + Note: This only applies for methods that calls `.get_object()` + see https://stackoverflow.com/questions/69959797/has-object-permission-not-working-for-detail-action-decorator + """ return request.user in obj.coordinators.all() diff --git a/backend/course_evaluations/tests/tests_permissions.py b/backend/course_evaluations/tests/tests_permissions.py index c7da496..d4fc0ef 100644 --- a/backend/course_evaluations/tests/tests_permissions.py +++ b/backend/course_evaluations/tests/tests_permissions.py @@ -78,3 +78,23 @@ def test_delete_view_course_evaluation_anonymous(api_client_no_auth, create_user assert response.status_code == status.HTTP_401_UNAUTHORIZED assert CourseEvaluation.objects.count() == 1 + +@pytest.mark.django_db +def test_delete_view_course_evaluation_user_but_not_coordinator(api_client_with_credentials_return_user, create_user, make_course_evaluation): + """ + GIVEN: The user is authenticated but not the coordinator + WHEN: I delete a course evaluation + THEN: The user is not authorised to use the endpoint + """ + user = create_user() + course_evaluation = make_course_evaluation(coordinators=[user]) + + url = reverse( + "api-v1:course_evaluations:course-evaluations-detail", + kwargs={"pk": course_evaluation.id}, + ) + api_client, user = api_client_with_credentials_return_user() + response = api_client.delete(url) + + assert response.status_code == status.HTTP_403_FORBIDDEN + assert CourseEvaluation.objects.count() == 1 From 362ed0c3aeb6b286f090e42cc411e8840b6f85c1 Mon Sep 17 00:00:00 2001 From: Frinze Erin Lapuz Date: Thu, 18 Aug 2022 10:11:07 +0800 Subject: [PATCH 57/89] move the document viewset as part of course evaluation --- backend/course_evaluations/urls.py | 11 +++++++++++ backend/documents/urls.py | 18 ++++++------------ backend/documents/views.py | 19 ++++--------------- 3 files changed, 21 insertions(+), 27 deletions(-) diff --git a/backend/course_evaluations/urls.py b/backend/course_evaluations/urls.py index 4f3c20b..025e645 100644 --- a/backend/course_evaluations/urls.py +++ b/backend/course_evaluations/urls.py @@ -2,9 +2,20 @@ from rest_framework.routers import DefaultRouter from course_evaluations.views import CourseEvaluationViewSet +from documents.views import CourseEvaluationDocumentViewSet # Create a router and register our viewsets with it. router = DefaultRouter() +""" +Additional Model-Viewset using the ID of the course evaluation as a parameter +Note: Additional Model-Viewset needs to go through first due to the order of the route evaluation +""" +router.register( + r"(?P[^/.]+)/documents", + CourseEvaluationDocumentViewSet, + basename="documents", +) + router.register(r"", CourseEvaluationViewSet, basename="course-evaluations") # The API URLs are now determined automatically by the router. diff --git a/backend/documents/urls.py b/backend/documents/urls.py index 3df12de..38bbe8a 100644 --- a/backend/documents/urls.py +++ b/backend/documents/urls.py @@ -1,13 +1,7 @@ -from django.urls import include, path -from rest_framework.routers import DefaultRouter +""" +Note: This is intentionally left out. -from documents.views import DocumentsViewSet - -# Create a router and register our viewsets with it. -router = DefaultRouter() -router.register(r"", DocumentsViewSet, basename="documents") - -# The API URLs are now determined automatically by the router. -urlpatterns = [ - path("", include(router.urls)), -] +See: +- `course_evaluations/urls.py` +- `reviews/urls.py` +""" diff --git a/backend/documents/views.py b/backend/documents/views.py index 6341730..ad09158 100644 --- a/backend/documents/views.py +++ b/backend/documents/views.py @@ -9,7 +9,7 @@ # Create your views here. -class DocumentsViewSet(viewsets.ModelViewSet): +class CourseEvaluationDocumentViewSet(viewsets.ModelViewSet): """ Viewset that handles documents @@ -19,19 +19,8 @@ class DocumentsViewSet(viewsets.ModelViewSet): Note: A reviewer should only see documents as part of their specific endpoint. See `reviews/serializers.py` or `reviews/views.py` """ - queryset = Document.objects.all() permission_classes = [DocumentCoordinatorAllowAll] + serializer_class = DocumentSerializer - def get_serializer(self, *args, **kwargs): - """ """ - return DocumentSerializer(*args, **kwargs) - - def list(self, request, *args, **kwargs): - """ - This is method is not needed. We will never be using a way to list all the documents that the user can view. - - If the role of the user is: - - Coordinator: the user should use the endpoint at `course_evaluations/views.py` - - Reviewer: the user should use the endpoint at `reviews/views.py` - """ - return Response(status=status.HTTP_405_METHOD_NOT_ALLOWED) + def get_queryset(self): + return Document.objects.all().filter(course_evaluation=self.kwargs["course_evaluation_id"]) From bf73ac26d2de0d3928a1207a40993a2c53b67e81 Mon Sep 17 00:00:00 2001 From: Frinze Erin Lapuz Date: Thu, 18 Aug 2022 10:11:16 +0800 Subject: [PATCH 58/89] fix tests permissions --- backend/course_evaluations/tests/tests_permissions.py | 1 + backend/course_evaluations/views.py | 5 ++++- 2 files changed, 5 insertions(+), 1 deletion(-) diff --git a/backend/course_evaluations/tests/tests_permissions.py b/backend/course_evaluations/tests/tests_permissions.py index d4fc0ef..e0de6aa 100644 --- a/backend/course_evaluations/tests/tests_permissions.py +++ b/backend/course_evaluations/tests/tests_permissions.py @@ -79,6 +79,7 @@ def test_delete_view_course_evaluation_anonymous(api_client_no_auth, create_user assert response.status_code == status.HTTP_401_UNAUTHORIZED assert CourseEvaluation.objects.count() == 1 + @pytest.mark.django_db def test_delete_view_course_evaluation_user_but_not_coordinator(api_client_with_credentials_return_user, create_user, make_course_evaluation): """ diff --git a/backend/course_evaluations/views.py b/backend/course_evaluations/views.py index b0153d5..18f1802 100644 --- a/backend/course_evaluations/views.py +++ b/backend/course_evaluations/views.py @@ -19,7 +19,9 @@ class CourseEvaluationViewSet(viewsets.ModelViewSet): """ queryset = CourseEvaluation.objects.all() - permission_classes = [CourseEvaluationIsCoordinatorAllowAll] + permission_classes = [ + CourseEvaluationIsCoordinatorAllowAll, + ] def get_serializer(self, *args, **kwargs): """ @@ -62,4 +64,5 @@ def destroy(self, request, *args, **kwargs): """ Deletion of a Course Evaluation should never be allowed """ + object = self.get_object() # this is to kick off the usual object level permission return self.http_method_not_allowed(request, *args, **kwargs) From e914598fcb80e3d9ff8185fc7812407f1ba7ca45 Mon Sep 17 00:00:00 2001 From: Frinze Erin Lapuz Date: Thu, 18 Aug 2022 10:15:20 +0800 Subject: [PATCH 59/89] remove documents in the path --- backend/config/urls.py | 7 ------- 1 file changed, 7 deletions(-) diff --git a/backend/config/urls.py b/backend/config/urls.py index 17f4fbc..89dd2c1 100644 --- a/backend/config/urls.py +++ b/backend/config/urls.py @@ -19,13 +19,6 @@ namespace="reviews", ), ), - path( - "documents/", - include( - ("documents.urls", "documents"), - namespace="documents", - ), - ), ], "api", ) From 28ba2f985cb417bc6e1769c11e088dfc0f2bc511 Mon Sep 17 00:00:00 2001 From: Frinze Erin Lapuz Date: Thu, 18 Aug 2022 10:33:48 +0800 Subject: [PATCH 60/89] permissions for documents --- backend/course_evaluations/permissions.py | 9 --------- backend/course_evaluations/views.py | 1 + backend/documents/permissions.py | 20 +++++++++++++------- backend/documents/views.py | 7 +++++-- 4 files changed, 19 insertions(+), 18 deletions(-) diff --git a/backend/course_evaluations/permissions.py b/backend/course_evaluations/permissions.py index 82bf5f5..193ace6 100644 --- a/backend/course_evaluations/permissions.py +++ b/backend/course_evaluations/permissions.py @@ -6,15 +6,6 @@ class CourseEvaluationIsCoordinatorAllowAll(permissions.BasePermission): Custom permission to only allow coordinators the API """ - def has_permission(self, request, view): - """ - This permission applies on request level - """ - # Check if authenticated - if request.user is None or not request.user.is_authenticated: - return False - return True - def has_object_permission(self, request, view, obj): """ A coordinator should be allowed to perform any operation diff --git a/backend/course_evaluations/views.py b/backend/course_evaluations/views.py index 18f1802..dccaf1d 100644 --- a/backend/course_evaluations/views.py +++ b/backend/course_evaluations/views.py @@ -20,6 +20,7 @@ class CourseEvaluationViewSet(viewsets.ModelViewSet): queryset = CourseEvaluation.objects.all() permission_classes = [ + permissions.IsAuthenticated, CourseEvaluationIsCoordinatorAllowAll, ] diff --git a/backend/documents/permissions.py b/backend/documents/permissions.py index 5ee6c3e..78557ca 100644 --- a/backend/documents/permissions.py +++ b/backend/documents/permissions.py @@ -1,14 +1,20 @@ from rest_framework import permissions +from course_evaluations.models import CourseEvaluation +from course_evaluations.permissions import CourseEvaluationIsCoordinatorAllowAll -class DocumentCoordinatorAllowAll(permissions.BasePermission): + +class DocumentCoordinatorAllowAllReviewerReadOnly(CourseEvaluationIsCoordinatorAllowAll): """ - Custom permission to only allow coordinators the API + Custom permission to Coordinators for all permissions, and read only for Reviewer """ - def has_object_permission(self, request, view, obj): - """ - A coordinator should be allowed to perform any operation - """ + def has_permission(self, request, view): + + # Check whether the user has a review for the course_evaluation + reviews = request.user.reviews.all() + course_evaluation_id = view.kwargs["course_evaluation_id"] + if reviews.filter(course_evaluation_id=course_evaluation_id).exists(): + return True - return request.user in obj.course_evaluation.coordinators.all() + return super().has_permission(request, view) diff --git a/backend/documents/views.py b/backend/documents/views.py index ad09158..ed5695a 100644 --- a/backend/documents/views.py +++ b/backend/documents/views.py @@ -3,7 +3,7 @@ from rest_framework.response import Response from documents.models import Document -from documents.permissions import DocumentCoordinatorAllowAll +from documents.permissions import DocumentCoordinatorAllowAllReviewerReadOnly from documents.serializers import DocumentSerializer # Create your views here. @@ -19,7 +19,10 @@ class CourseEvaluationDocumentViewSet(viewsets.ModelViewSet): Note: A reviewer should only see documents as part of their specific endpoint. See `reviews/serializers.py` or `reviews/views.py` """ - permission_classes = [DocumentCoordinatorAllowAll] + permission_classes = [ + permissions.IsAuthenticated, + DocumentCoordinatorAllowAllReviewerReadOnly, + ] serializer_class = DocumentSerializer def get_queryset(self): From 5ec1750346fe044f79ee490efd00ab065ae6ae53 Mon Sep 17 00:00:00 2001 From: Frinze Erin Lapuz Date: Thu, 18 Aug 2022 10:35:37 +0800 Subject: [PATCH 61/89] make date_submitted not required --- .../0002_alter_review_date_submitted.py | 18 ++++++++++++++++++ backend/reviews/models.py | 3 ++- 2 files changed, 20 insertions(+), 1 deletion(-) create mode 100644 backend/reviews/migrations/0002_alter_review_date_submitted.py diff --git a/backend/reviews/migrations/0002_alter_review_date_submitted.py b/backend/reviews/migrations/0002_alter_review_date_submitted.py new file mode 100644 index 0000000..dd622ef --- /dev/null +++ b/backend/reviews/migrations/0002_alter_review_date_submitted.py @@ -0,0 +1,18 @@ +# Generated by Django 3.2.13 on 2022-08-18 02:35 + +from django.db import migrations, models + + +class Migration(migrations.Migration): + + dependencies = [ + ('reviews', '0001_initial'), + ] + + operations = [ + migrations.AlterField( + model_name='review', + name='date_submitted', + field=models.DateTimeField(null=True), + ), + ] diff --git a/backend/reviews/models.py b/backend/reviews/models.py index f733a01..1c6a7da 100755 --- a/backend/reviews/models.py +++ b/backend/reviews/models.py @@ -1,3 +1,4 @@ +from typing_extensions import Required import uuid # Create your models here. @@ -23,7 +24,7 @@ class Review(models.Model): final_comment = models.TextField(null=False, blank=True) - date_submitted = models.DateTimeField() + date_submitted = models.DateTimeField(null=True) created_at = models.DateTimeField(auto_now_add=True) updated_at = models.DateTimeField(auto_now=True) From 49dca7571d2fc67a2cd540df4ebcf4a0ce9165d3 Mon Sep 17 00:00:00 2001 From: Frinze Erin Lapuz Date: Thu, 18 Aug 2022 10:38:30 +0800 Subject: [PATCH 62/89] correct the reviewer field (typo: coordinator) --- .../0002_alter_review_date_submitted.py | 18 --------------- .../migrations/0002_auto_20220818_1037.py | 23 +++++++++++++++++++ backend/reviews/models.py | 4 ++-- 3 files changed, 25 insertions(+), 20 deletions(-) delete mode 100644 backend/reviews/migrations/0002_alter_review_date_submitted.py create mode 100644 backend/reviews/migrations/0002_auto_20220818_1037.py diff --git a/backend/reviews/migrations/0002_alter_review_date_submitted.py b/backend/reviews/migrations/0002_alter_review_date_submitted.py deleted file mode 100644 index dd622ef..0000000 --- a/backend/reviews/migrations/0002_alter_review_date_submitted.py +++ /dev/null @@ -1,18 +0,0 @@ -# Generated by Django 3.2.13 on 2022-08-18 02:35 - -from django.db import migrations, models - - -class Migration(migrations.Migration): - - dependencies = [ - ('reviews', '0001_initial'), - ] - - operations = [ - migrations.AlterField( - model_name='review', - name='date_submitted', - field=models.DateTimeField(null=True), - ), - ] diff --git a/backend/reviews/migrations/0002_auto_20220818_1037.py b/backend/reviews/migrations/0002_auto_20220818_1037.py new file mode 100644 index 0000000..c23064e --- /dev/null +++ b/backend/reviews/migrations/0002_auto_20220818_1037.py @@ -0,0 +1,23 @@ +# Generated by Django 3.2.13 on 2022-08-18 02:37 + +from django.db import migrations, models + + +class Migration(migrations.Migration): + + dependencies = [ + ("reviews", "0001_initial"), + ] + + operations = [ + migrations.RenameField( + model_name="review", + old_name="coordinators", + new_name="reviewer", + ), + migrations.AlterField( + model_name="review", + name="date_submitted", + field=models.DateTimeField(null=True), + ), + ] diff --git a/backend/reviews/models.py b/backend/reviews/models.py index 1c6a7da..64d9c23 100755 --- a/backend/reviews/models.py +++ b/backend/reviews/models.py @@ -1,8 +1,8 @@ -from typing_extensions import Required import uuid # Create your models here. from django.db import models +from typing_extensions import Required from course_evaluations.models import CourseEvaluation, DevelopmentLevels, EOCSpecific from documents.models import Document @@ -20,7 +20,7 @@ class Review(models.Model): # One-to-many Relationship with Course Evaluations course_evaluation = models.ForeignKey(CourseEvaluation, on_delete=models.CASCADE) - coordinators = models.ForeignKey("auth.User", related_name="reviews", on_delete=models.CASCADE) + reviewer = models.ForeignKey("auth.User", related_name="reviews", on_delete=models.CASCADE) final_comment = models.TextField(null=False, blank=True) From 23cb3342089bf19ad8b2a89c5ae4867123879ea5 Mon Sep 17 00:00:00 2001 From: Frinze Erin Lapuz Date: Thu, 18 Aug 2022 10:47:16 +0800 Subject: [PATCH 63/89] fix where date time field is required --- ...{0002_auto_20220818_1037.py => 0002_auto_20220818_1039.py} | 4 ++-- backend/reviews/models.py | 2 +- 2 files changed, 3 insertions(+), 3 deletions(-) rename backend/reviews/migrations/{0002_auto_20220818_1037.py => 0002_auto_20220818_1039.py} (79%) diff --git a/backend/reviews/migrations/0002_auto_20220818_1037.py b/backend/reviews/migrations/0002_auto_20220818_1039.py similarity index 79% rename from backend/reviews/migrations/0002_auto_20220818_1037.py rename to backend/reviews/migrations/0002_auto_20220818_1039.py index c23064e..ce15067 100644 --- a/backend/reviews/migrations/0002_auto_20220818_1037.py +++ b/backend/reviews/migrations/0002_auto_20220818_1039.py @@ -1,4 +1,4 @@ -# Generated by Django 3.2.13 on 2022-08-18 02:37 +# Generated by Django 3.2.13 on 2022-08-18 02:39 from django.db import migrations, models @@ -18,6 +18,6 @@ class Migration(migrations.Migration): migrations.AlterField( model_name="review", name="date_submitted", - field=models.DateTimeField(null=True), + field=models.DateTimeField(blank=True, null=True), ), ] diff --git a/backend/reviews/models.py b/backend/reviews/models.py index 64d9c23..dc3ebee 100755 --- a/backend/reviews/models.py +++ b/backend/reviews/models.py @@ -24,7 +24,7 @@ class Review(models.Model): final_comment = models.TextField(null=False, blank=True) - date_submitted = models.DateTimeField(null=True) + date_submitted = models.DateTimeField(null=True, blank=True) created_at = models.DateTimeField(auto_now_add=True) updated_at = models.DateTimeField(auto_now=True) From 26487f6a7817a8112103223f5ff4dc2ef11cdc43 Mon Sep 17 00:00:00 2001 From: Frinze Erin Lapuz Date: Thu, 18 Aug 2022 10:47:39 +0800 Subject: [PATCH 64/89] rework on the permissions for coordinator and reviewer --- backend/documents/permissions.py | 19 ++++++++++++------- 1 file changed, 12 insertions(+), 7 deletions(-) diff --git a/backend/documents/permissions.py b/backend/documents/permissions.py index 78557ca..db73c1f 100644 --- a/backend/documents/permissions.py +++ b/backend/documents/permissions.py @@ -1,20 +1,25 @@ from rest_framework import permissions from course_evaluations.models import CourseEvaluation -from course_evaluations.permissions import CourseEvaluationIsCoordinatorAllowAll -class DocumentCoordinatorAllowAllReviewerReadOnly(CourseEvaluationIsCoordinatorAllowAll): +class DocumentCoordinatorAllowAllReviewerReadOnly(permissions.BasePermission): """ Custom permission to Coordinators for all permissions, and read only for Reviewer """ def has_permission(self, request, view): - - # Check whether the user has a review for the course_evaluation - reviews = request.user.reviews.all() course_evaluation_id = view.kwargs["course_evaluation_id"] - if reviews.filter(course_evaluation_id=course_evaluation_id).exists(): + + # Check whether the user has a course_evaluation that is desired + course_evaluations = request.user.course_evaluation_coordinator.all() + if course_evaluations.filter(id=course_evaluation_id).exists(): return True - return super().has_permission(request, view) + # Check whether the user has a review for the course_evaluation + if request.method in permissions.SAFE_METHODS: + reviews = request.user.reviews.all() + if reviews.filter(course_evaluation_id=course_evaluation_id).exists(): + return True + + return False From 6cefcd699b490deadd24d5766256ff1472168db8 Mon Sep 17 00:00:00 2001 From: Frinze Erin Lapuz Date: Thu, 18 Aug 2022 11:03:37 +0800 Subject: [PATCH 65/89] read and write only serializers --- backend/course_evaluations/serializers.py | 22 +++++++++++++-- backend/documents/models.py | 2 +- backend/documents/serializers.py | 33 ++++++++++++++++++++++- backend/documents/views.py | 9 +++++-- 4 files changed, 60 insertions(+), 6 deletions(-) diff --git a/backend/course_evaluations/serializers.py b/backend/course_evaluations/serializers.py index 0b8d2ce..e6f293a 100644 --- a/backend/course_evaluations/serializers.py +++ b/backend/course_evaluations/serializers.py @@ -8,18 +8,35 @@ EOCSet, EOCSpecific, ) +from documents.serializers import DocumentWriteSerializer class UserSerializer(serializers.ModelSerializer): class Meta: model = User - fields = ("id", "username", "first_name", "last_name", "email", "is_staff", "is_active", "is_superuser") + fields = ( + "id", + "username", + "first_name", + "last_name", + "email", + "is_staff", + "is_active", + "is_superuser", + ) class EOCSpecificSerializer(serializers.ModelSerializer): class Meta: model = EOCSpecific - fields = ("id", "number", "eoc_general", "get_general_and_specific_eoc", "description", "indicators_of_attainment") + fields = ( + "id", + "number", + "eoc_general", + "get_general_and_specific_eoc", + "description", + "indicators_of_attainment", + ) class EOCGeneralSerializer(serializers.ModelSerializer): @@ -61,6 +78,7 @@ class CourseEvaluationDetailSerializer(serializers.ModelSerializer): eoc_set = EOCSetSerializer(read_only=True) coordinators = UserSerializer(many=True, read_only=True) course_evalution_justifications = CourseEvaluationJustificationSerializer(many=True, read_only=True) + documents = DocumentWriteSerializer(many=True, read_only=True) # Note: This is used for write, by creating the `eoc_set` relationship eoc_set_id = serializers.IntegerField(required=True) diff --git a/backend/documents/models.py b/backend/documents/models.py index 9f82a73..dfbdebc 100644 --- a/backend/documents/models.py +++ b/backend/documents/models.py @@ -15,7 +15,7 @@ class Document(models.Model): id = models.UUIDField(primary_key=True, default=uuid.uuid4, editable=False) # Main relations - course_evaluation = models.ForeignKey(CourseEvaluation, on_delete=models.CASCADE) + course_evaluation = models.ForeignKey(CourseEvaluation, on_delete=models.CASCADE, related_name="documents") # Information about the document name = models.CharField(max_length=50, null=False, blank=False) diff --git a/backend/documents/serializers.py b/backend/documents/serializers.py index ae16308..d4a9668 100644 --- a/backend/documents/serializers.py +++ b/backend/documents/serializers.py @@ -10,7 +10,7 @@ from documents.models import Document -class DocumentSerializer(serializers.ModelSerializer): +class DocumentWriteSerializer(serializers.ModelSerializer): """ Note: It is important to understand that this serializer is only used for write operations for the most parts. @@ -24,3 +24,34 @@ class DocumentSerializer(serializers.ModelSerializer): class Meta: model = Document fields = "__all__" + + +class EOCSpecificSerializerReadOnly(serializers.ModelSerializer): + class Meta: + model = EOCSpecific + fields = ("id", "number", "get_general_and_specific_eoc") + + +class EOCGeneralSerializerReadOnly(serializers.ModelSerializer): + class Meta: + model = EOCGeneral + fields = ("id", "number") + + +class DocumentReadOnlySerializer(serializers.ModelSerializer): + """ + Read only serializer for the Document model. + """ + + eoc_generals = EOCGeneralSerializerReadOnly( + many=True, + read_only=True, + ) + eoc_specifics = EOCSpecificSerializerReadOnly( + many=True, + read_only=True, + ) + + class Meta: + model = Document + fields = "__all__" diff --git a/backend/documents/views.py b/backend/documents/views.py index ed5695a..a0cc11b 100644 --- a/backend/documents/views.py +++ b/backend/documents/views.py @@ -4,7 +4,7 @@ from documents.models import Document from documents.permissions import DocumentCoordinatorAllowAllReviewerReadOnly -from documents.serializers import DocumentSerializer +from documents.serializers import DocumentReadOnlySerializer, DocumentWriteSerializer # Create your views here. @@ -23,7 +23,12 @@ class CourseEvaluationDocumentViewSet(viewsets.ModelViewSet): permissions.IsAuthenticated, DocumentCoordinatorAllowAllReviewerReadOnly, ] - serializer_class = DocumentSerializer + + def get_serializer_class(self): + if self.action == "list": + return DocumentReadOnlySerializer + else: + return DocumentWriteSerializer def get_queryset(self): return Document.objects.all().filter(course_evaluation=self.kwargs["course_evaluation_id"]) From ab7a4b2b37cfd5016e19155f298bd52bcdb09572 Mon Sep 17 00:00:00 2001 From: Frinze Erin Lapuz Date: Thu, 18 Aug 2022 11:04:18 +0800 Subject: [PATCH 66/89] rename `get_general_and_specific_eoc` -> `general_and_specific_eoc` --- backend/course_evaluations/admin.py | 2 +- backend/course_evaluations/models.py | 6 +++--- backend/course_evaluations/serializers.py | 2 +- backend/documents/serializers.py | 2 +- backend/documents/urls.py | 1 + 5 files changed, 7 insertions(+), 6 deletions(-) diff --git a/backend/course_evaluations/admin.py b/backend/course_evaluations/admin.py index cd60442..98ddfee 100644 --- a/backend/course_evaluations/admin.py +++ b/backend/course_evaluations/admin.py @@ -43,7 +43,7 @@ class EOCSpecificAdmin(admin.ModelAdmin): "id", "number", "eoc_general", - "get_general_and_specific_eoc", + "general_and_specific_eoc", "description", ) list_filter = ("eoc_general",) diff --git a/backend/course_evaluations/models.py b/backend/course_evaluations/models.py index 8c43c32..945f081 100644 --- a/backend/course_evaluations/models.py +++ b/backend/course_evaluations/models.py @@ -50,9 +50,9 @@ class Meta: eoc_general = models.ForeignKey(EOCGeneral, on_delete=models.CASCADE) def __str__(self): - return f"{self.eoc_general.eoc_set.name} - {self.get_general_and_specific_eoc()}" + return f"{self.eoc_general.eoc_set.name} - {self.general_and_specific_eoc()}" - def get_general_and_specific_eoc(self): + def general_and_specific_eoc(self): return f"{self.eoc_general.number}.{self.number}" @@ -107,5 +107,5 @@ class CourseEvaluationJustification(models.Model): development_level = models.IntegerField(choices=DevelopmentLevels.choices) def __str__(self): - eoc_specifics = ", ".join([eoc_specific.get_general_and_specific_eoc() for eoc_specific in self.eoc_specifics.all()]) + eoc_specifics = ", ".join([eoc_specific.general_and_specific_eoc() for eoc_specific in self.eoc_specifics.all()]) return f"Course Evaluation Justification ({self.course_evaluation.unit_code}) - {eoc_specifics}" diff --git a/backend/course_evaluations/serializers.py b/backend/course_evaluations/serializers.py index e6f293a..3c59b15 100644 --- a/backend/course_evaluations/serializers.py +++ b/backend/course_evaluations/serializers.py @@ -33,7 +33,7 @@ class Meta: "id", "number", "eoc_general", - "get_general_and_specific_eoc", + "general_and_specific_eoc", "description", "indicators_of_attainment", ) diff --git a/backend/documents/serializers.py b/backend/documents/serializers.py index d4a9668..12cd5ff 100644 --- a/backend/documents/serializers.py +++ b/backend/documents/serializers.py @@ -29,7 +29,7 @@ class Meta: class EOCSpecificSerializerReadOnly(serializers.ModelSerializer): class Meta: model = EOCSpecific - fields = ("id", "number", "get_general_and_specific_eoc") + fields = ("id", "number", "general_and_specific_eoc") class EOCGeneralSerializerReadOnly(serializers.ModelSerializer): diff --git a/backend/documents/urls.py b/backend/documents/urls.py index 38bbe8a..4aaa30d 100644 --- a/backend/documents/urls.py +++ b/backend/documents/urls.py @@ -1,5 +1,6 @@ """ Note: This is intentionally left out. +Reason: This resource See: - `course_evaluations/urls.py` From 981cb3b1c198f762529b4a8b7d07d77c9778bc4a Mon Sep 17 00:00:00 2001 From: Frinze Erin Lapuz Date: Thu, 18 Aug 2022 11:30:35 +0800 Subject: [PATCH 67/89] force the course_evaluation value to the url --- backend/documents/serializers.py | 3 +++ backend/documents/views.py | 13 ++++++++++++- 2 files changed, 15 insertions(+), 1 deletion(-) diff --git a/backend/documents/serializers.py b/backend/documents/serializers.py index 12cd5ff..5d3b2ba 100644 --- a/backend/documents/serializers.py +++ b/backend/documents/serializers.py @@ -21,6 +21,9 @@ class DocumentWriteSerializer(serializers.ModelSerializer): eoc_generals = serializers.PrimaryKeyRelatedField(many=True, read_only=False, queryset=EOCGeneral.objects.all()) eoc_specifics = serializers.PrimaryKeyRelatedField(many=True, read_only=False, queryset=EOCSpecific.objects.all()) + # This is not required as we will force this to a value, see `documents.views,py` for `perform_create` and `perform_update` + course_evaluation = serializers.CharField(required=False) + class Meta: model = Document fields = "__all__" diff --git a/backend/documents/views.py b/backend/documents/views.py index a0cc11b..ddd1086 100644 --- a/backend/documents/views.py +++ b/backend/documents/views.py @@ -25,10 +25,21 @@ class CourseEvaluationDocumentViewSet(viewsets.ModelViewSet): ] def get_serializer_class(self): - if self.action == "list": + if self.request.method == "GET": return DocumentReadOnlySerializer else: return DocumentWriteSerializer def get_queryset(self): return Document.objects.all().filter(course_evaluation=self.kwargs["course_evaluation_id"]) + + """ + For CREATE and UPDATE, we have to force the value of + `course_evaluation_id` to the url parameters (disregarding what the actual payload was) + """ + + def perform_create(self, serializer): + serializer.save(course_evaluation_id=self.kwargs["course_evaluation_id"]) + + def perform_update(self, serializer): + serializer.save(course_evaluation_id=self.kwargs["course_evaluation_id"]) From 0e8aea0bf3870c846282c0e9fb4a006811793f78 Mon Sep 17 00:00:00 2001 From: Frinze Erin Lapuz Date: Thu, 18 Aug 2022 11:30:54 +0800 Subject: [PATCH 68/89] change the serializer being displayed to show the general_and_specific numbers --- backend/course_evaluations/serializers.py | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/backend/course_evaluations/serializers.py b/backend/course_evaluations/serializers.py index 3c59b15..44ebdf1 100644 --- a/backend/course_evaluations/serializers.py +++ b/backend/course_evaluations/serializers.py @@ -8,7 +8,7 @@ EOCSet, EOCSpecific, ) -from documents.serializers import DocumentWriteSerializer +from documents.serializers import DocumentReadOnlySerializer class UserSerializer(serializers.ModelSerializer): @@ -78,7 +78,7 @@ class CourseEvaluationDetailSerializer(serializers.ModelSerializer): eoc_set = EOCSetSerializer(read_only=True) coordinators = UserSerializer(many=True, read_only=True) course_evalution_justifications = CourseEvaluationJustificationSerializer(many=True, read_only=True) - documents = DocumentWriteSerializer(many=True, read_only=True) + documents = DocumentReadOnlySerializer(many=True, read_only=True) # Note: This is used for write, by creating the `eoc_set` relationship eoc_set_id = serializers.IntegerField(required=True) From 3fe2b20c911ffcfd418be2e09617d915da0848f9 Mon Sep 17 00:00:00 2001 From: Frinze Erin Lapuz Date: Thu, 18 Aug 2022 11:40:48 +0800 Subject: [PATCH 69/89] make `course_evaluation` field only a write field --- backend/documents/serializers.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/backend/documents/serializers.py b/backend/documents/serializers.py index 5d3b2ba..eee14da 100644 --- a/backend/documents/serializers.py +++ b/backend/documents/serializers.py @@ -22,7 +22,7 @@ class DocumentWriteSerializer(serializers.ModelSerializer): eoc_specifics = serializers.PrimaryKeyRelatedField(many=True, read_only=False, queryset=EOCSpecific.objects.all()) # This is not required as we will force this to a value, see `documents.views,py` for `perform_create` and `perform_update` - course_evaluation = serializers.CharField(required=False) + course_evaluation = serializers.CharField(write_only=True, required=False) class Meta: model = Document From f2daa4e9d794cb50bb8573d4803c73a4c7bcafac Mon Sep 17 00:00:00 2001 From: Frinze Erin Lapuz Date: Thu, 18 Aug 2022 11:57:05 +0800 Subject: [PATCH 70/89] make assignment of eocs to a document optional --- .../migrations/0002_auto_20220818_1156.py | 30 +++++++++++++++++++ backend/documents/models.py | 4 +-- 2 files changed, 32 insertions(+), 2 deletions(-) create mode 100644 backend/documents/migrations/0002_auto_20220818_1156.py diff --git a/backend/documents/migrations/0002_auto_20220818_1156.py b/backend/documents/migrations/0002_auto_20220818_1156.py new file mode 100644 index 0000000..ede77a2 --- /dev/null +++ b/backend/documents/migrations/0002_auto_20220818_1156.py @@ -0,0 +1,30 @@ +# Generated by Django 3.2.13 on 2022-08-18 03:56 + +import django.db.models.deletion +from django.db import migrations, models + + +class Migration(migrations.Migration): + + dependencies = [ + ("course_evaluations", "0002_courseevaluationjustification"), + ("documents", "0001_initial"), + ] + + operations = [ + migrations.AlterField( + model_name="document", + name="course_evaluation", + field=models.ForeignKey(on_delete=django.db.models.deletion.CASCADE, related_name="documents", to="course_evaluations.courseevaluation"), + ), + migrations.AlterField( + model_name="document", + name="eoc_generals", + field=models.ManyToManyField(blank=True, to="course_evaluations.EOCGeneral"), + ), + migrations.AlterField( + model_name="document", + name="eoc_specifics", + field=models.ManyToManyField(blank=True, to="course_evaluations.EOCSpecific"), + ), + ] diff --git a/backend/documents/models.py b/backend/documents/models.py index dfbdebc..37ef302 100644 --- a/backend/documents/models.py +++ b/backend/documents/models.py @@ -24,8 +24,8 @@ class Document(models.Model): # Tags Equivalence is_introduction = models.BooleanField() - eoc_generals = models.ManyToManyField(EOCGeneral) - eoc_specifics = models.ManyToManyField(EOCSpecific) + eoc_generals = models.ManyToManyField(EOCGeneral, blank=True) + eoc_specifics = models.ManyToManyField(EOCSpecific, blank=True) created_at = models.DateTimeField(auto_now_add=True) updated_at = models.DateTimeField(auto_now=True) From 6ffde586fc133e5e71a3c9b543d008ecc02bd814 Mon Sep 17 00:00:00 2001 From: Frinze Erin Lapuz Date: Thu, 18 Aug 2022 12:02:17 +0800 Subject: [PATCH 71/89] initialise testing for course evaluation document --- backend/conftest.py | 44 ++++++++++++++++++++++++++++++++++++++++++++ 1 file changed, 44 insertions(+) diff --git a/backend/conftest.py b/backend/conftest.py index dd16901..4d556ae 100644 --- a/backend/conftest.py +++ b/backend/conftest.py @@ -7,6 +7,7 @@ from rest_framework.test import APIClient from course_evaluations.models import CourseEvaluation, EOCSet +from documents.models import Document @pytest.fixture @@ -142,3 +143,46 @@ def _make_course_evaluation( # Teardown for course_evaluation in created_course_evaluation: course_evaluation.delete() + + +def make_course_evaluation_document(setup_indeaa, make_course_evaluation) -> Document: + """Make CourseEvaluation on demand inside tests""" + created_course_evaluation_document = [] + + # Create a CourseEvaluation record + def _make_course_evaluation_document( + course_evaluation=None, + name="Test CourseEvaluation Document", + description="Test CourseEvaluation Document", + url="https://systemhealthlab.com/", + is_introduction=False, + eoc_generals=None, + eoc_specifics=None, + ): + if course_evaluation is None: + course_evaluation = make_course_evaluation() + # Create the record + document = Document.objects.create( + name=name, + description=description, + url=url, + is_introduction=is_introduction, + course_evaluation=course_evaluation, + ) + + for eoc_general in eoc_generals: + document.eoc_generals.add(eoc_general) + + for eoc_specific in eoc_specifics: + document.eoc_specifics.add(eoc_specific) + + created_course_evaluation_document.append(document) + + return document + + # Recommended reading: https://docs.pytest.org/en/stable/fixture.html#yield-fixtures-recommended + yield _make_course_evaluation_document + + # Teardown + for document in created_course_evaluation_document: + document.delete() From a83a7ce9f9b4fb9daa44f21de535f8c2c67c4c4c Mon Sep 17 00:00:00 2001 From: Frinze Erin Lapuz Date: Thu, 18 Aug 2022 12:18:05 +0800 Subject: [PATCH 72/89] tests fixtures for making reviews --- backend/conftest.py | 43 +++++++++++++++++++++++++++++++++++++++++-- 1 file changed, 41 insertions(+), 2 deletions(-) diff --git a/backend/conftest.py b/backend/conftest.py index 4d556ae..e248610 100644 --- a/backend/conftest.py +++ b/backend/conftest.py @@ -8,6 +8,7 @@ from course_evaluations.models import CourseEvaluation, EOCSet from documents.models import Document +from reviews.models import Review @pytest.fixture @@ -145,6 +146,8 @@ def _make_course_evaluation( course_evaluation.delete() +@pytest.fixture +@pytest.mark.django_db def make_course_evaluation_document(setup_indeaa, make_course_evaluation) -> Document: """Make CourseEvaluation on demand inside tests""" created_course_evaluation_document = [] @@ -156,8 +159,8 @@ def _make_course_evaluation_document( description="Test CourseEvaluation Document", url="https://systemhealthlab.com/", is_introduction=False, - eoc_generals=None, - eoc_specifics=None, + eoc_generals=[], + eoc_specifics=[], ): if course_evaluation is None: course_evaluation = make_course_evaluation() @@ -186,3 +189,39 @@ def _make_course_evaluation_document( # Teardown for document in created_course_evaluation_document: document.delete() + + +@pytest.fixture +@pytest.mark.django_db +def make_course_review(setup_indeaa, make_course_evaluation, create_user) -> CourseEvaluation: + """Make CourseEvaluation on demand inside tests""" + created_course_review = [] + + # Create a CourseEvaluation record + def _make_course_review( + course_evaluation=None, + reviewer=None, + final_comment="Test CourseEvaluation Review", + date_submitted=None, + ): + if course_evaluation is None: + course_evaluation = make_course_evaluation() + if reviewer is None: + reviewer = create_user() + + # Create the record + course_review = Review.objects.create( + course_evaluation=course_evaluation, + reviewer=reviewer, + final_comment=final_comment, + date_submitted=date_submitted, + ) + + return course_review + + # Recommended reading: https://docs.pytest.org/en/stable/fixture.html#yield-fixtures-recommended + yield _make_course_review + + # Teardown + for document in created_course_review: + document.delete() From b7e8ce465a90c239d95e7cc652b45c5789bbd579 Mon Sep 17 00:00:00 2001 From: Frinze Erin Lapuz Date: Thu, 18 Aug 2022 12:18:16 +0800 Subject: [PATCH 73/89] initialise test permissions --- backend/documents/tests.py | 3 - backend/documents/tests/tests_permissions.py | 165 +++++++++++++++++++ 2 files changed, 165 insertions(+), 3 deletions(-) delete mode 100644 backend/documents/tests.py create mode 100644 backend/documents/tests/tests_permissions.py diff --git a/backend/documents/tests.py b/backend/documents/tests.py deleted file mode 100644 index 7ce503c..0000000 --- a/backend/documents/tests.py +++ /dev/null @@ -1,3 +0,0 @@ -from django.test import TestCase - -# Create your tests here. diff --git a/backend/documents/tests/tests_permissions.py b/backend/documents/tests/tests_permissions.py new file mode 100644 index 0000000..abc8f49 --- /dev/null +++ b/backend/documents/tests/tests_permissions.py @@ -0,0 +1,165 @@ +""" +This test file is focused on permission testing. This is to ensure that unauthorised access cannot use the API +""" +import pytest +from django.urls import reverse +from rest_framework import status + +from course_evaluations.models import CourseEvaluation +from documents.models import Document + + +@pytest.mark.django_db +def test_list_view_course_evaluation_documents_anonymous(api_client_no_auth, make_course_evaluation): + """ + GIVEN: The user is not authenticated + WHEN: the user tries to use the endpoint + THEN: The user is not authorised to use the endpoint + """ + course_evaluation = make_course_evaluation(coordinators=[]) + url = reverse( + "api-v1:course_evaluations:documents-list", + kwargs={"course_evaluation_id": course_evaluation.id}, + ) + response = api_client_no_auth.get(url) + + assert response.status_code == status.HTTP_401_UNAUTHORIZED + + +@pytest.mark.django_db +def test_create_view_course_evaluation_documents_anonymous(api_client_no_auth, make_course_evaluation): + """ + GIVEN: The user is not authenticated + WHEN: I create a course evaluation document + THEN: The user is not authorised to use the endpoint + """ + course_evaluation = make_course_evaluation(coordinators=[]) + url = reverse( + "api-v1:course_evaluations:documents-list", + kwargs={"course_evaluation_id": course_evaluation.id}, + ) + data = { + # Data does not matter as the user is not authenticated (the return error is 400 if data is invalid) + } + response = api_client_no_auth.post(url, data) + + assert response.status_code == status.HTTP_401_UNAUTHORIZED + assert CourseEvaluation.objects.count() == 1 + assert Document.objects.count() == 0 + + +@pytest.mark.django_db +def test_update_view_course_evaluation_documents_anonymous(api_client_no_auth, make_course_evaluation_document, make_course_evaluation): + """ + GIVEN: The user is not authenticated + WHEN: I update a course evaluation document + THEN: The user is not authorised to use the endpoint + """ + course_evaluation = make_course_evaluation() + document = make_course_evaluation_document(course_evaluation=course_evaluation) + + url = reverse( + "api-v1:course_evaluations:documents-detail", + kwargs={"course_evaluation_id": course_evaluation.id, "pk": document.id}, + ) + data = {"name": "new name"} + response = api_client_no_auth.put(url, data) + + assert response.status_code == status.HTTP_401_UNAUTHORIZED + assert CourseEvaluation.objects.count() == 1 + + +@pytest.mark.django_db +def test_delete_view_course_evaluation_documents_anonymous(api_client_no_auth, make_course_evaluation_document, make_course_evaluation): + """ + GIVEN: The user is not authenticated + WHEN: I delete a course evaluation document + THEN: The user is not authorised to use the endpoint + """ + course_evaluation = make_course_evaluation() + + document = make_course_evaluation_document(course_evaluation=course_evaluation) + + url = reverse( + "api-v1:course_evaluations:documents-detail", + kwargs={"course_evaluation_id": course_evaluation.id, "pk": document.id}, + ) + response = api_client_no_auth.delete(url) + + assert response.status_code == status.HTTP_401_UNAUTHORIZED + assert CourseEvaluation.objects.count() == 1 + + +@pytest.mark.django_db +def test_create_view_course_evaluation_documents_reviewer(api_client_with_credentials_return_user, make_course_evaluation, make_course_review): + """ + GIVEN: The user is authenticated and is a reviewer + WHEN: I create a course evaluation document + THEN: The user is not authorised to use the endpoint + """ + course_evaluation = make_course_evaluation(coordinators=[]) + api_client, user = api_client_with_credentials_return_user() + make_course_review(course_evaluation=course_evaluation, reviewer=user) + url = reverse( + "api-v1:course_evaluations:documents-list", + kwargs={"course_evaluation_id": course_evaluation.id}, + ) + data = { + # Data does not matter as The user is authenticated and is a reviewer (the return error is 400 if data is invalid) + } + response = api_client.post(url, data) + + assert response.status_code == status.HTTP_403_FORBIDDEN + assert CourseEvaluation.objects.count() == 1 + assert Document.objects.count() == 0 + + +@pytest.mark.django_db +def test_update_view_course_evaluation_documents_reviewer( + api_client_with_credentials_return_user, make_course_evaluation_document, make_course_evaluation, make_course_review +): + """ + GIVEN: The user is authenticated and is a reviewer + WHEN: I update a course evaluation document + THEN: The user is not authorised to use the endpoint + """ + course_evaluation = make_course_evaluation() + api_client, user = api_client_with_credentials_return_user() + make_course_review(course_evaluation=course_evaluation, reviewer=user) + + document = make_course_evaluation_document(course_evaluation=course_evaluation) + + url = reverse( + "api-v1:course_evaluations:documents-detail", + kwargs={"course_evaluation_id": course_evaluation.id, "pk": document.id}, + ) + data = {"name": "new name"} + response = api_client.put(url, data) + + assert response.status_code == status.HTTP_403_FORBIDDEN + assert CourseEvaluation.objects.count() == 1 + + +@pytest.mark.django_db +def test_delete_view_course_evaluation_documents_reviewer( + api_client_with_credentials_return_user, make_course_evaluation_document, make_course_evaluation, make_course_review +): + """ + GIVEN: The user is authenticated and is a reviewer + WHEN: I delete a course evaluation document + THEN: The user is not authorised to use the endpoint + """ + course_evaluation = make_course_evaluation() + api_client, user = api_client_with_credentials_return_user() + make_course_review(course_evaluation=course_evaluation, reviewer=user) + + document = make_course_evaluation_document(course_evaluation=course_evaluation) + + url = reverse( + "api-v1:course_evaluations:documents-detail", + kwargs={"course_evaluation_id": course_evaluation.id, "pk": document.id}, + ) + response = api_client.delete(url) + + assert response.status_code == status.HTTP_403_FORBIDDEN + assert CourseEvaluation.objects.count() == 1 From 70580c9aade141c1f7b22f9cb0772c8569afb470 Mon Sep 17 00:00:00 2001 From: Frinze Erin Lapuz Date: Thu, 18 Aug 2022 12:18:48 +0800 Subject: [PATCH 74/89] added constraint to make a course evaluation and a reviewer unique --- .../0003_alter_review_unique_together.py | 20 +++++++++++++++++++ backend/reviews/models.py | 7 +++++++ 2 files changed, 27 insertions(+) create mode 100644 backend/reviews/migrations/0003_alter_review_unique_together.py diff --git a/backend/reviews/migrations/0003_alter_review_unique_together.py b/backend/reviews/migrations/0003_alter_review_unique_together.py new file mode 100644 index 0000000..42dda78 --- /dev/null +++ b/backend/reviews/migrations/0003_alter_review_unique_together.py @@ -0,0 +1,20 @@ +# Generated by Django 3.2.13 on 2022-08-18 04:18 + +from django.conf import settings +from django.db import migrations + + +class Migration(migrations.Migration): + + dependencies = [ + ('course_evaluations', '0002_courseevaluationjustification'), + migrations.swappable_dependency(settings.AUTH_USER_MODEL), + ('reviews', '0002_auto_20220818_1039'), + ] + + operations = [ + migrations.AlterUniqueTogether( + name='review', + unique_together={('course_evaluation', 'reviewer')}, + ), + ] diff --git a/backend/reviews/models.py b/backend/reviews/models.py index dc3ebee..ffd7ec5 100755 --- a/backend/reviews/models.py +++ b/backend/reviews/models.py @@ -29,6 +29,13 @@ class Review(models.Model): created_at = models.DateTimeField(auto_now_add=True) updated_at = models.DateTimeField(auto_now=True) + class Meta: + # A particular reviewer and a course_evaluation can only exist as once + unique_together = (("course_evaluation", "reviewer"),) + + def __str__(self): + return f"Review ({self.id}) {self.course_evaluation.id} - {self.reviewer.username}" + class ReviewEocSpecific(models.Model): """ From 4f78e9ea8dcdeef5ceb3456adddfbfaca3b60f7d Mon Sep 17 00:00:00 2001 From: Frinze Erin Lapuz Date: Thu, 18 Aug 2022 12:20:55 +0800 Subject: [PATCH 75/89] initialise permission for review owner or coordinator perms --- backend/reviews/permissions.py | 16 ++++++++++++++++ 1 file changed, 16 insertions(+) create mode 100644 backend/reviews/permissions.py diff --git a/backend/reviews/permissions.py b/backend/reviews/permissions.py new file mode 100644 index 0000000..aa6811a --- /dev/null +++ b/backend/reviews/permissions.py @@ -0,0 +1,16 @@ +from rest_framework import permissions + + +class IsReviewOwnerAllOrCoordinatorReadOnly(permissions.BasePermission): + """ + Custom permission to only allow owners of an object to edit it or read only by coordinators. + Note: This is a modified version of https://www.django-rest-framework.org/tutorial/4-authentication-and-permissions/#object-level-permissions + """ + + def has_object_permission(self, request, view, obj): + if request.method in permissions.SAFE_METHODS: + if request.user in obj.course_evaluation.coordinators.all(): + return True + + # Write permissions only for the owner + return obj.reviewer == request.user From cc6f75f0b83999b47d742b2a71779a0e9535e7fe Mon Sep 17 00:00:00 2001 From: Frinze Erin Lapuz Date: Thu, 18 Aug 2022 12:33:27 +0800 Subject: [PATCH 76/89] permission for a course review --- backend/reviews/permissions.py | 17 +++++++++++------ 1 file changed, 11 insertions(+), 6 deletions(-) diff --git a/backend/reviews/permissions.py b/backend/reviews/permissions.py index aa6811a..4bcdd10 100644 --- a/backend/reviews/permissions.py +++ b/backend/reviews/permissions.py @@ -3,14 +3,19 @@ class IsReviewOwnerAllOrCoordinatorReadOnly(permissions.BasePermission): """ - Custom permission to only allow owners of an object to edit it or read only by coordinators. - Note: This is a modified version of https://www.django-rest-framework.org/tutorial/4-authentication-and-permissions/#object-level-permissions + Usecase: + - As a coordinator, I would like to create a Course Review (to indicate that someone is appointed as a reviewer) + - As a reviewer (owner), I will be able to update my review + - As a review (not owner), I am not able to do anything + - As anonymous, I am not able to do anything """ def has_object_permission(self, request, view, obj): - if request.method in permissions.SAFE_METHODS: + if request.method is "POST": + # Check if coordinator if request.user in obj.course_evaluation.coordinators.all(): return True - - # Write permissions only for the owner - return obj.reviewer == request.user + else: + # Reviewer in this context is the associated reviewer in the object + return request.user == obj.reviewer + From 8d3fe8ac221031051cb379274d28c4f8c0029203 Mon Sep 17 00:00:00 2001 From: Frinze Erin Lapuz Date: Thu, 18 Aug 2022 12:34:45 +0800 Subject: [PATCH 77/89] ability to admit and revoke permissions --- backend/reviews/permissions.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/backend/reviews/permissions.py b/backend/reviews/permissions.py index 4bcdd10..a1db848 100644 --- a/backend/reviews/permissions.py +++ b/backend/reviews/permissions.py @@ -11,7 +11,7 @@ class IsReviewOwnerAllOrCoordinatorReadOnly(permissions.BasePermission): """ def has_object_permission(self, request, view, obj): - if request.method is "POST": + if request.method in ["POST", "DELETE"]: # Check if coordinator if request.user in obj.course_evaluation.coordinators.all(): return True From a6fea5e7b246fc0875146542f12870f89d79c3c0 Mon Sep 17 00:00:00 2001 From: Frinze Erin Lapuz Date: Thu, 18 Aug 2022 13:31:11 +0800 Subject: [PATCH 78/89] fix missing __init__.py --- backend/course_evaluations/tests/__init__.py | 0 backend/documents/tests/__init__.py | 0 2 files changed, 0 insertions(+), 0 deletions(-) create mode 100644 backend/course_evaluations/tests/__init__.py create mode 100644 backend/documents/tests/__init__.py diff --git a/backend/course_evaluations/tests/__init__.py b/backend/course_evaluations/tests/__init__.py new file mode 100644 index 0000000..e69de29 diff --git a/backend/documents/tests/__init__.py b/backend/documents/tests/__init__.py new file mode 100644 index 0000000..e69de29 From 44a5f1181e222efa898aa9f08232dd7bfe0c2559 Mon Sep 17 00:00:00 2001 From: Frinze Erin Lapuz Date: Thu, 18 Aug 2022 13:31:35 +0800 Subject: [PATCH 79/89] remove unnecessary tagging in pytest --- backend/pytest.ini | 6 +----- 1 file changed, 1 insertion(+), 5 deletions(-) diff --git a/backend/pytest.ini b/backend/pytest.ini index 4eb0bf6..e6ec37f 100644 --- a/backend/pytest.ini +++ b/backend/pytest.ini @@ -1,7 +1,3 @@ [pytest] DJANGO_SETTINGS_MODULE=config.settings.ci -python_files = tests.py test_*.py *_tests.py tests_*.py -; Automatic Process distribution of Test Files to CPU cores -addopts = --strict-markers -markers = - v1_0_0: All tests introduced in version 1.0.0 and previous versions +python_files = tests.py test_*.py *_tests.py tests_*.py \ No newline at end of file From 252e202da624dbf27d7aca4e62da38e41ca4ebd3 Mon Sep 17 00:00:00 2001 From: Frinze Erin Lapuz Date: Thu, 18 Aug 2022 13:31:49 +0800 Subject: [PATCH 80/89] initiialise permission of a reviewer --- backend/reviews/permissions.py | 17 ++++++++--------- 1 file changed, 8 insertions(+), 9 deletions(-) diff --git a/backend/reviews/permissions.py b/backend/reviews/permissions.py index a1db848..5b9b270 100644 --- a/backend/reviews/permissions.py +++ b/backend/reviews/permissions.py @@ -4,18 +4,17 @@ class IsReviewOwnerAllOrCoordinatorReadOnly(permissions.BasePermission): """ Usecase: - - As a coordinator, I would like to create a Course Review (to indicate that someone is appointed as a reviewer) + - As a coordinator, I would like to create/delete a Course Review (to indicate that someone is appointed/removed as a reviewer) + - As a coordinator, I would like to open the submission - As a reviewer (owner), I will be able to update my review - As a review (not owner), I am not able to do anything - As anonymous, I am not able to do anything """ def has_object_permission(self, request, view, obj): - if request.method in ["POST", "DELETE"]: - # Check if coordinator - if request.user in obj.course_evaluation.coordinators.all(): - return True - else: - # Reviewer in this context is the associated reviewer in the object - return request.user == obj.reviewer - + # A coordinator can do anything + if request.user in obj.course_evaluation.coordinators.all(): + return True + + # Reviewer in this context is the associated reviewer in the object + return request.user == obj.reviewer From 0b6cfe22cbb9c22fb889ad5902ee170ae962b5e4 Mon Sep 17 00:00:00 2001 From: Frinze Erin Lapuz Date: Thu, 18 Aug 2022 14:12:15 +0800 Subject: [PATCH 81/89] linting for migration --- .../migrations/0003_alter_review_unique_together.py | 8 ++++---- 1 file changed, 4 insertions(+), 4 deletions(-) diff --git a/backend/reviews/migrations/0003_alter_review_unique_together.py b/backend/reviews/migrations/0003_alter_review_unique_together.py index 42dda78..ce45f28 100644 --- a/backend/reviews/migrations/0003_alter_review_unique_together.py +++ b/backend/reviews/migrations/0003_alter_review_unique_together.py @@ -7,14 +7,14 @@ class Migration(migrations.Migration): dependencies = [ - ('course_evaluations', '0002_courseevaluationjustification'), + ("course_evaluations", "0002_courseevaluationjustification"), migrations.swappable_dependency(settings.AUTH_USER_MODEL), - ('reviews', '0002_auto_20220818_1039'), + ("reviews", "0002_auto_20220818_1039"), ] operations = [ migrations.AlterUniqueTogether( - name='review', - unique_together={('course_evaluation', 'reviewer')}, + name="review", + unique_together={("course_evaluation", "reviewer")}, ), ] From 39c3fa2f7623987ce2d69ef50a89231334fd377e Mon Sep 17 00:00:00 2001 From: Frinze Erin Lapuz Date: Thu, 18 Aug 2022 14:13:17 +0800 Subject: [PATCH 82/89] initialise test crud as a coordinator --- backend/documents/tests/tests_crud.py | 120 ++++++++++++++++++++++++++ 1 file changed, 120 insertions(+) create mode 100644 backend/documents/tests/tests_crud.py diff --git a/backend/documents/tests/tests_crud.py b/backend/documents/tests/tests_crud.py new file mode 100644 index 0000000..423c1a0 --- /dev/null +++ b/backend/documents/tests/tests_crud.py @@ -0,0 +1,120 @@ +""" +This test file is focused on the crud functionality of the course evaluations. + +Note: Permission testing is not the focus of this test file. +""" +from django.urls import reverse +from rest_framework import status + +from course_evaluations.models import CourseEvaluation, EOCSet +from documents.models import Document + + +def test_list_view_course_evaluation_document_as_coordinator( + api_client_with_credentials_return_user, make_course_evaluation, make_course_evaluation_document +): + """ + GIVEN: There are multiple course evaluation documents + WHEN: The endpoint for the course evaluation is called + THEN: The course evaluation data is returned successfully with only a couple of information (eg. EOC info is not returned) + """ + api_client, user = api_client_with_credentials_return_user() + course_evaluation_1 = make_course_evaluation(coordinators=[user]) + course_evaluation_2 = make_course_evaluation(coordinators=[user]) + document_1 = make_course_evaluation_document(course_evaluation=course_evaluation_1) + document_2 = make_course_evaluation_document(course_evaluation=course_evaluation_1) + + # This user should not be able to see this + make_course_evaluation_document(course_evaluation=course_evaluation_2) + + url = reverse("api-v1:course_evaluations:documents-list", kwargs={"course_evaluation_id": course_evaluation_1.id}) + response = api_client.get(url) + + assert response.status_code == status.HTTP_200_OK + data = response.data["results"] + + # Check that we can find the two course evaluations + assert len(data) == 2 + assert data[0]["id"] == str(document_1.id) + assert data[1]["id"] == str(document_2.id) + + +def test_create_view_course_evaluation_document_as_coordinator(setup_indeaa, api_client_with_credentials_return_user, make_course_evaluation): + """ + GIVEN: As a coordinator of a course evaluation + WHEN: I create a document for the course evaluation + THEN: The document is created successfully + """ + api_client, user = api_client_with_credentials_return_user() + course_evaluation = make_course_evaluation(coordinators=[user]) + + url = reverse("api-v1:course_evaluations:documents-list", kwargs={"course_evaluation_id": course_evaluation.id}) + data = { + "eoc_generals": [], + "eoc_specifics": [], + "name": "Test Document", + "description": "test description", + "url": "https://google.com", + "is_introduction": False, + } + response = api_client.post(url, data) + + assert response.status_code == status.HTTP_201_CREATED + + # Check that the course evaluation is created + document = Document.objects.first() + assert document.name == data["name"] + assert document.description == data["description"] + assert document.url == data["url"] + assert document.is_introduction == data["is_introduction"] + assert document.course_evaluation == course_evaluation + assert document.eoc_generals.count() == 0 + assert document.eoc_specifics.count() == 0 + + +def test_update_view_course_evaluation_document_as_coordinator( + api_client_with_credentials_return_user, make_course_evaluation, make_course_evaluation_document +): + """ + GIVEN: As a coordinator of a course evaluation + WHEN: I update the course evaluation document + THEN: The course evaluation document is updated successfully + """ + api_client, user = api_client_with_credentials_return_user() + course_evaluation = make_course_evaluation(coordinators=[user]) + document = make_course_evaluation_document(course_evaluation=course_evaluation) + + url = reverse("api-v1:course_evaluations:documents-detail", kwargs={"course_evaluation_id": course_evaluation.id, "pk": document.id}) + data = { + "description": "New description", + } + + # Before doing the patch/update, just check + assert document.description != data["description"] + + response = api_client.patch(url, data) + + assert response.status_code == status.HTTP_200_OK + assert response.data["description"] == data["description"] + # Refresh from db + document.refresh_from_db() + assert document.description == data["description"] + + +def test_delete_view_course_evaluation_document_as_coordinator( + api_client_with_credentials_return_user, make_course_evaluation, make_course_evaluation_document +): + """ + GIVEN: As a coordinator of a course evaluation + WHEN: I delete the course evaluation document + THEN: The course evaluation document is deleted successfully + """ + api_client, user = api_client_with_credentials_return_user() + course_evaluation = make_course_evaluation(coordinators=[user]) + document = make_course_evaluation_document(course_evaluation=course_evaluation) + + url = reverse("api-v1:course_evaluations:documents-detail", kwargs={"course_evaluation_id": course_evaluation.id, "pk": document.id}) + response = api_client.delete(url) + + assert response.status_code == status.HTTP_204_NO_CONTENT + assert Document.objects.count() == 0 \ No newline at end of file From 768151c9f535453ed69bdf96e89710acf2ce1ea0 Mon Sep 17 00:00:00 2001 From: Frinze Erin Lapuz Date: Thu, 18 Aug 2022 14:25:38 +0800 Subject: [PATCH 83/89] change to a coordinator exclusive endpoint permission --- backend/course_evaluations/permissions.py | 11 ++++++++++ backend/documents/permissions.py | 25 ----------------------- backend/documents/views.py | 4 ++-- 3 files changed, 13 insertions(+), 27 deletions(-) delete mode 100644 backend/documents/permissions.py diff --git a/backend/course_evaluations/permissions.py b/backend/course_evaluations/permissions.py index 193ace6..701b9f1 100644 --- a/backend/course_evaluations/permissions.py +++ b/backend/course_evaluations/permissions.py @@ -14,3 +14,14 @@ def has_object_permission(self, request, view, obj): see https://stackoverflow.com/questions/69959797/has-object-permission-not-working-for-detail-action-decorator """ return request.user in obj.coordinators.all() + + +class CourseEvaluationIsCoordinatorAllowAllViaObjectReference(CourseEvaluationIsCoordinatorAllowAll): + """ + Custom permission to only allow coordinators the API (via Object Reference) + Similar to CourseEvaluationIsCoordinatorAllowAll + """ + + def has_object_permission(self, request, view, obj): + # See that the difference is that we are going to be using the course_evaluation_id in the url + return request.user in obj.course_evaluation.coordinators.all() diff --git a/backend/documents/permissions.py b/backend/documents/permissions.py deleted file mode 100644 index db73c1f..0000000 --- a/backend/documents/permissions.py +++ /dev/null @@ -1,25 +0,0 @@ -from rest_framework import permissions - -from course_evaluations.models import CourseEvaluation - - -class DocumentCoordinatorAllowAllReviewerReadOnly(permissions.BasePermission): - """ - Custom permission to Coordinators for all permissions, and read only for Reviewer - """ - - def has_permission(self, request, view): - course_evaluation_id = view.kwargs["course_evaluation_id"] - - # Check whether the user has a course_evaluation that is desired - course_evaluations = request.user.course_evaluation_coordinator.all() - if course_evaluations.filter(id=course_evaluation_id).exists(): - return True - - # Check whether the user has a review for the course_evaluation - if request.method in permissions.SAFE_METHODS: - reviews = request.user.reviews.all() - if reviews.filter(course_evaluation_id=course_evaluation_id).exists(): - return True - - return False diff --git a/backend/documents/views.py b/backend/documents/views.py index ddd1086..3bb563b 100644 --- a/backend/documents/views.py +++ b/backend/documents/views.py @@ -2,8 +2,8 @@ from rest_framework.exceptions import ValidationError from rest_framework.response import Response +from course_evaluations.permissions import CourseEvaluationIsCoordinatorAllowAllViaObjectReference from documents.models import Document -from documents.permissions import DocumentCoordinatorAllowAllReviewerReadOnly from documents.serializers import DocumentReadOnlySerializer, DocumentWriteSerializer # Create your views here. @@ -21,7 +21,7 @@ class CourseEvaluationDocumentViewSet(viewsets.ModelViewSet): permission_classes = [ permissions.IsAuthenticated, - DocumentCoordinatorAllowAllReviewerReadOnly, + CourseEvaluationIsCoordinatorAllowAllViaObjectReference ] def get_serializer_class(self): From b5e7ded7dc4ede9448259d0bba8a914a501bdac3 Mon Sep 17 00:00:00 2001 From: Frinze Erin Lapuz Date: Thu, 18 Aug 2022 14:36:56 +0800 Subject: [PATCH 84/89] flake8 fix --- backend/course_evaluations/views.py | 3 +-- backend/documents/serializers.py | 3 --- backend/documents/tests/tests_crud.py | 35 ++++++++++++++++++++------- backend/documents/urls.py | 2 +- backend/documents/views.py | 14 +++++------ backend/reviews/models.py | 2 -- backend/reviews/tests.py | 3 --- backend/reviews/views.py | 4 +-- 8 files changed, 35 insertions(+), 31 deletions(-) delete mode 100755 backend/reviews/tests.py diff --git a/backend/course_evaluations/views.py b/backend/course_evaluations/views.py index dccaf1d..6e54d1c 100644 --- a/backend/course_evaluations/views.py +++ b/backend/course_evaluations/views.py @@ -6,7 +6,6 @@ from course_evaluations.permissions import CourseEvaluationIsCoordinatorAllowAll from course_evaluations.serializers import ( CourseEvaluationDetailSerializer, - CourseEvaluationJustificationSerializer, CourseEvaluationListSerializer, EOCSet, ) @@ -65,5 +64,5 @@ def destroy(self, request, *args, **kwargs): """ Deletion of a Course Evaluation should never be allowed """ - object = self.get_object() # this is to kick off the usual object level permission + self.get_object() # this is to kick off the usual object level permission return self.http_method_not_allowed(request, *args, **kwargs) diff --git a/backend/documents/serializers.py b/backend/documents/serializers.py index eee14da..883aae6 100644 --- a/backend/documents/serializers.py +++ b/backend/documents/serializers.py @@ -1,10 +1,7 @@ from rest_framework import serializers from course_evaluations.models import ( - CourseEvaluation, - CourseEvaluationJustification, EOCGeneral, - EOCSet, EOCSpecific, ) from documents.models import Document diff --git a/backend/documents/tests/tests_crud.py b/backend/documents/tests/tests_crud.py index 423c1a0..4027a7c 100644 --- a/backend/documents/tests/tests_crud.py +++ b/backend/documents/tests/tests_crud.py @@ -6,12 +6,13 @@ from django.urls import reverse from rest_framework import status -from course_evaluations.models import CourseEvaluation, EOCSet from documents.models import Document def test_list_view_course_evaluation_document_as_coordinator( - api_client_with_credentials_return_user, make_course_evaluation, make_course_evaluation_document + api_client_with_credentials_return_user, + make_course_evaluation, + make_course_evaluation_document, ): """ GIVEN: There are multiple course evaluation documents @@ -27,7 +28,10 @@ def test_list_view_course_evaluation_document_as_coordinator( # This user should not be able to see this make_course_evaluation_document(course_evaluation=course_evaluation_2) - url = reverse("api-v1:course_evaluations:documents-list", kwargs={"course_evaluation_id": course_evaluation_1.id}) + url = reverse( + "api-v1:course_evaluations:documents-list", + kwargs={"course_evaluation_id": course_evaluation_1.id}, + ) response = api_client.get(url) assert response.status_code == status.HTTP_200_OK @@ -48,7 +52,10 @@ def test_create_view_course_evaluation_document_as_coordinator(setup_indeaa, api api_client, user = api_client_with_credentials_return_user() course_evaluation = make_course_evaluation(coordinators=[user]) - url = reverse("api-v1:course_evaluations:documents-list", kwargs={"course_evaluation_id": course_evaluation.id}) + url = reverse( + "api-v1:course_evaluations:documents-list", + kwargs={"course_evaluation_id": course_evaluation.id}, + ) data = { "eoc_generals": [], "eoc_specifics": [], @@ -73,7 +80,9 @@ def test_create_view_course_evaluation_document_as_coordinator(setup_indeaa, api def test_update_view_course_evaluation_document_as_coordinator( - api_client_with_credentials_return_user, make_course_evaluation, make_course_evaluation_document + api_client_with_credentials_return_user, + make_course_evaluation, + make_course_evaluation_document, ): """ GIVEN: As a coordinator of a course evaluation @@ -84,7 +93,10 @@ def test_update_view_course_evaluation_document_as_coordinator( course_evaluation = make_course_evaluation(coordinators=[user]) document = make_course_evaluation_document(course_evaluation=course_evaluation) - url = reverse("api-v1:course_evaluations:documents-detail", kwargs={"course_evaluation_id": course_evaluation.id, "pk": document.id}) + url = reverse( + "api-v1:course_evaluations:documents-detail", + kwargs={"course_evaluation_id": course_evaluation.id, "pk": document.id}, + ) data = { "description": "New description", } @@ -102,7 +114,9 @@ def test_update_view_course_evaluation_document_as_coordinator( def test_delete_view_course_evaluation_document_as_coordinator( - api_client_with_credentials_return_user, make_course_evaluation, make_course_evaluation_document + api_client_with_credentials_return_user, + make_course_evaluation, + make_course_evaluation_document, ): """ GIVEN: As a coordinator of a course evaluation @@ -113,8 +127,11 @@ def test_delete_view_course_evaluation_document_as_coordinator( course_evaluation = make_course_evaluation(coordinators=[user]) document = make_course_evaluation_document(course_evaluation=course_evaluation) - url = reverse("api-v1:course_evaluations:documents-detail", kwargs={"course_evaluation_id": course_evaluation.id, "pk": document.id}) + url = reverse( + "api-v1:course_evaluations:documents-detail", + kwargs={"course_evaluation_id": course_evaluation.id, "pk": document.id}, + ) response = api_client.delete(url) assert response.status_code == status.HTTP_204_NO_CONTENT - assert Document.objects.count() == 0 \ No newline at end of file + assert Document.objects.count() == 0 diff --git a/backend/documents/urls.py b/backend/documents/urls.py index 4aaa30d..697d4b1 100644 --- a/backend/documents/urls.py +++ b/backend/documents/urls.py @@ -1,6 +1,6 @@ """ Note: This is intentionally left out. -Reason: This resource +Reason: This resource is crossed between the CourseEvaluation and the Document models. See: - `course_evaluations/urls.py` diff --git a/backend/documents/views.py b/backend/documents/views.py index 3bb563b..7962c8b 100644 --- a/backend/documents/views.py +++ b/backend/documents/views.py @@ -1,13 +1,11 @@ -from rest_framework import permissions, status, viewsets -from rest_framework.exceptions import ValidationError -from rest_framework.response import Response +from rest_framework import permissions, viewsets -from course_evaluations.permissions import CourseEvaluationIsCoordinatorAllowAllViaObjectReference +from course_evaluations.permissions import ( + CourseEvaluationIsCoordinatorAllowAllViaObjectReference, +) from documents.models import Document from documents.serializers import DocumentReadOnlySerializer, DocumentWriteSerializer -# Create your views here. - class CourseEvaluationDocumentViewSet(viewsets.ModelViewSet): """ @@ -21,7 +19,7 @@ class CourseEvaluationDocumentViewSet(viewsets.ModelViewSet): permission_classes = [ permissions.IsAuthenticated, - CourseEvaluationIsCoordinatorAllowAllViaObjectReference + CourseEvaluationIsCoordinatorAllowAllViaObjectReference, ] def get_serializer_class(self): @@ -34,7 +32,7 @@ def get_queryset(self): return Document.objects.all().filter(course_evaluation=self.kwargs["course_evaluation_id"]) """ - For CREATE and UPDATE, we have to force the value of + For CREATE and UPDATE, we have to force the value of `course_evaluation_id` to the url parameters (disregarding what the actual payload was) """ diff --git a/backend/reviews/models.py b/backend/reviews/models.py index ffd7ec5..2d499e0 100755 --- a/backend/reviews/models.py +++ b/backend/reviews/models.py @@ -1,8 +1,6 @@ import uuid -# Create your models here. from django.db import models -from typing_extensions import Required from course_evaluations.models import CourseEvaluation, DevelopmentLevels, EOCSpecific from documents.models import Document diff --git a/backend/reviews/tests.py b/backend/reviews/tests.py deleted file mode 100755 index 7ce503c..0000000 --- a/backend/reviews/tests.py +++ /dev/null @@ -1,3 +0,0 @@ -from django.test import TestCase - -# Create your tests here. diff --git a/backend/reviews/views.py b/backend/reviews/views.py index 52d684c..185fdc7 100755 --- a/backend/reviews/views.py +++ b/backend/reviews/views.py @@ -1,6 +1,4 @@ -from rest_framework import permissions, status, viewsets -from rest_framework.exceptions import ValidationError -from rest_framework.response import Response +from rest_framework import viewsets from reviews.models import Review, ReviewDocument, ReviewEocSpecific from reviews.serializers import ( From b6dc3a5bf3f18c6ec370bc89b415d9bc7d9c8289 Mon Sep 17 00:00:00 2001 From: Frinze Erin Lapuz Date: Thu, 18 Aug 2022 14:50:47 +0800 Subject: [PATCH 85/89] move `documents` folder to `course_evaluation` --- backend/config/settings/base.py | 1 - backend/conftest.py | 3 +- backend/course_evaluations/models.py | 25 ++++++++ backend/course_evaluations/serializers.py | 52 ++++++++++++++++- .../tests}/documents/__init__.py | 0 .../tests/documents}/tests_crud.py | 2 +- .../tests/documents}/tests_permissions.py | 13 +++-- backend/course_evaluations/urls.py | 6 +- backend/course_evaluations/views.py | 45 ++++++++++++++- backend/documents/admin.py | 21 ------- backend/documents/apps.py | 6 -- backend/documents/migrations/0001_initial.py | 33 ----------- .../migrations/0002_auto_20220818_1156.py | 30 ---------- backend/documents/migrations/__init__.py | 0 backend/documents/models.py | 31 ---------- backend/documents/serializers.py | 57 ------------------- backend/documents/tests/__init__.py | 0 backend/documents/urls.py | 8 --- backend/documents/views.py | 43 -------------- backend/reviews/models.py | 8 ++- 20 files changed, 140 insertions(+), 244 deletions(-) rename backend/{ => course_evaluations/tests}/documents/__init__.py (100%) rename backend/{documents/tests => course_evaluations/tests/documents}/tests_crud.py (99%) rename backend/{documents/tests => course_evaluations/tests/documents}/tests_permissions.py (94%) delete mode 100644 backend/documents/admin.py delete mode 100644 backend/documents/apps.py delete mode 100644 backend/documents/migrations/0001_initial.py delete mode 100644 backend/documents/migrations/0002_auto_20220818_1156.py delete mode 100644 backend/documents/migrations/__init__.py delete mode 100644 backend/documents/models.py delete mode 100644 backend/documents/serializers.py delete mode 100644 backend/documents/tests/__init__.py delete mode 100644 backend/documents/urls.py delete mode 100644 backend/documents/views.py diff --git a/backend/config/settings/base.py b/backend/config/settings/base.py index aaeaa9f..38eb00f 100644 --- a/backend/config/settings/base.py +++ b/backend/config/settings/base.py @@ -67,7 +67,6 @@ "commands", "course_evaluations", "reviews", - "documents", # Authentication "rest_framework", "rest_framework.authtoken", diff --git a/backend/conftest.py b/backend/conftest.py index e248610..5514e4f 100644 --- a/backend/conftest.py +++ b/backend/conftest.py @@ -6,8 +6,7 @@ from django.core.management import call_command from rest_framework.test import APIClient -from course_evaluations.models import CourseEvaluation, EOCSet -from documents.models import Document +from course_evaluations.models import CourseEvaluation, Document, EOCSet from reviews.models import Review diff --git a/backend/course_evaluations/models.py b/backend/course_evaluations/models.py index 945f081..85f54ba 100644 --- a/backend/course_evaluations/models.py +++ b/backend/course_evaluations/models.py @@ -109,3 +109,28 @@ class CourseEvaluationJustification(models.Model): def __str__(self): eoc_specifics = ", ".join([eoc_specific.general_and_specific_eoc() for eoc_specific in self.eoc_specifics.all()]) return f"Course Evaluation Justification ({self.course_evaluation.unit_code}) - {eoc_specifics}" + + +class Document(models.Model): + """ + A document is a url to a resource that a reviewer will look at + to judge the quality of a course + """ + + id = models.UUIDField(primary_key=True, default=uuid.uuid4, editable=False) + + # Main relations + course_evaluation = models.ForeignKey(CourseEvaluation, on_delete=models.CASCADE, related_name="documents") + + # Information about the document + name = models.CharField(max_length=50, null=False, blank=False) + description = models.TextField(null=False, blank=True) + url = models.URLField() + + # Tags Equivalence + is_introduction = models.BooleanField() + eoc_generals = models.ManyToManyField(EOCGeneral, blank=True) + eoc_specifics = models.ManyToManyField(EOCSpecific, blank=True) + + created_at = models.DateTimeField(auto_now_add=True) + updated_at = models.DateTimeField(auto_now=True) diff --git a/backend/course_evaluations/serializers.py b/backend/course_evaluations/serializers.py index 44ebdf1..2275ea7 100644 --- a/backend/course_evaluations/serializers.py +++ b/backend/course_evaluations/serializers.py @@ -4,11 +4,61 @@ from course_evaluations.models import ( CourseEvaluation, CourseEvaluationJustification, + Document, EOCGeneral, EOCSet, EOCSpecific, ) -from documents.serializers import DocumentReadOnlySerializer + + +class DocumentWriteSerializer(serializers.ModelSerializer): + """ + Note: It is important to understand that this serializer is only used for write operations for the most parts. + + This means that `eoc_generals` and `eoc_specifics` are expecting the an id (not the EOC number) + """ + + # Note: `read_only=False` is important to do patching and creations + eoc_generals = serializers.PrimaryKeyRelatedField(many=True, read_only=False, queryset=EOCGeneral.objects.all()) + eoc_specifics = serializers.PrimaryKeyRelatedField(many=True, read_only=False, queryset=EOCSpecific.objects.all()) + + # This is not required as we will force this to a value, see `documents.views,py` for `perform_create` and `perform_update` + course_evaluation = serializers.CharField(write_only=True, required=False) + + class Meta: + model = Document + fields = "__all__" + + +class EOCSpecificSerializerReadOnly(serializers.ModelSerializer): + class Meta: + model = EOCSpecific + fields = ("id", "number", "general_and_specific_eoc") + + +class EOCGeneralSerializerReadOnly(serializers.ModelSerializer): + class Meta: + model = EOCGeneral + fields = ("id", "number") + + +class DocumentReadOnlySerializer(serializers.ModelSerializer): + """ + Read only serializer for the Document model. + """ + + eoc_generals = EOCGeneralSerializerReadOnly( + many=True, + read_only=True, + ) + eoc_specifics = EOCSpecificSerializerReadOnly( + many=True, + read_only=True, + ) + + class Meta: + model = Document + fields = "__all__" class UserSerializer(serializers.ModelSerializer): diff --git a/backend/documents/__init__.py b/backend/course_evaluations/tests/documents/__init__.py similarity index 100% rename from backend/documents/__init__.py rename to backend/course_evaluations/tests/documents/__init__.py diff --git a/backend/documents/tests/tests_crud.py b/backend/course_evaluations/tests/documents/tests_crud.py similarity index 99% rename from backend/documents/tests/tests_crud.py rename to backend/course_evaluations/tests/documents/tests_crud.py index 4027a7c..de39344 100644 --- a/backend/documents/tests/tests_crud.py +++ b/backend/course_evaluations/tests/documents/tests_crud.py @@ -6,7 +6,7 @@ from django.urls import reverse from rest_framework import status -from documents.models import Document +from course_evaluations.models import Document def test_list_view_course_evaluation_document_as_coordinator( diff --git a/backend/documents/tests/tests_permissions.py b/backend/course_evaluations/tests/documents/tests_permissions.py similarity index 94% rename from backend/documents/tests/tests_permissions.py rename to backend/course_evaluations/tests/documents/tests_permissions.py index abc8f49..07dbb8a 100644 --- a/backend/documents/tests/tests_permissions.py +++ b/backend/course_evaluations/tests/documents/tests_permissions.py @@ -5,8 +5,7 @@ from django.urls import reverse from rest_framework import status -from course_evaluations.models import CourseEvaluation -from documents.models import Document +from course_evaluations.models import CourseEvaluation, Document @pytest.mark.django_db @@ -116,7 +115,10 @@ def test_create_view_course_evaluation_documents_reviewer(api_client_with_creden @pytest.mark.django_db def test_update_view_course_evaluation_documents_reviewer( - api_client_with_credentials_return_user, make_course_evaluation_document, make_course_evaluation, make_course_review + api_client_with_credentials_return_user, + make_course_evaluation_document, + make_course_evaluation, + make_course_review, ): """ GIVEN: The user is authenticated and is a reviewer @@ -142,7 +144,10 @@ def test_update_view_course_evaluation_documents_reviewer( @pytest.mark.django_db def test_delete_view_course_evaluation_documents_reviewer( - api_client_with_credentials_return_user, make_course_evaluation_document, make_course_evaluation, make_course_review + api_client_with_credentials_return_user, + make_course_evaluation_document, + make_course_evaluation, + make_course_review, ): """ GIVEN: The user is authenticated and is a reviewer diff --git a/backend/course_evaluations/urls.py b/backend/course_evaluations/urls.py index 025e645..a31ae60 100644 --- a/backend/course_evaluations/urls.py +++ b/backend/course_evaluations/urls.py @@ -1,8 +1,10 @@ from django.urls import include, path from rest_framework.routers import DefaultRouter -from course_evaluations.views import CourseEvaluationViewSet -from documents.views import CourseEvaluationDocumentViewSet +from course_evaluations.views import ( + CourseEvaluationDocumentViewSet, + CourseEvaluationViewSet, +) # Create a router and register our viewsets with it. router = DefaultRouter() diff --git a/backend/course_evaluations/views.py b/backend/course_evaluations/views.py index 6e54d1c..d969b50 100644 --- a/backend/course_evaluations/views.py +++ b/backend/course_evaluations/views.py @@ -2,11 +2,16 @@ from rest_framework.exceptions import ValidationError from rest_framework.response import Response -from course_evaluations.models import CourseEvaluation -from course_evaluations.permissions import CourseEvaluationIsCoordinatorAllowAll +from course_evaluations.models import CourseEvaluation, Document +from course_evaluations.permissions import ( + CourseEvaluationIsCoordinatorAllowAll, + CourseEvaluationIsCoordinatorAllowAllViaObjectReference, +) from course_evaluations.serializers import ( CourseEvaluationDetailSerializer, CourseEvaluationListSerializer, + DocumentReadOnlySerializer, + DocumentWriteSerializer, EOCSet, ) @@ -66,3 +71,39 @@ def destroy(self, request, *args, **kwargs): """ self.get_object() # this is to kick off the usual object level permission return self.http_method_not_allowed(request, *args, **kwargs) + + +class CourseEvaluationDocumentViewSet(viewsets.ModelViewSet): + """ + Viewset that handles documents + + Permissions: + - Coordinator (DETAIL, CREATE, UPDATE, DELETE). Essentially for management of the documents for a particular course_evaluation + + Note: A reviewer should only see documents as part of their specific endpoint. See `reviews/serializers.py` or `reviews/views.py` + """ + + permission_classes = [ + permissions.IsAuthenticated, + CourseEvaluationIsCoordinatorAllowAllViaObjectReference, + ] + + def get_serializer_class(self): + if self.request.method == "GET": + return DocumentReadOnlySerializer + else: + return DocumentWriteSerializer + + def get_queryset(self): + return Document.objects.all().filter(course_evaluation=self.kwargs["course_evaluation_id"]) + + """ + For CREATE and UPDATE, we have to force the value of + `course_evaluation_id` to the url parameters (disregarding what the actual payload was) + """ + + def perform_create(self, serializer): + serializer.save(course_evaluation_id=self.kwargs["course_evaluation_id"]) + + def perform_update(self, serializer): + serializer.save(course_evaluation_id=self.kwargs["course_evaluation_id"]) diff --git a/backend/documents/admin.py b/backend/documents/admin.py deleted file mode 100644 index df358c2..0000000 --- a/backend/documents/admin.py +++ /dev/null @@ -1,21 +0,0 @@ -from django.contrib import admin - -from documents.models import Document - -# Register your models here. - - -@admin.register(Document) -class DocumentAdmin(admin.ModelAdmin): - list_display = ("id", "course_evaluation", "name", "url", "is_introduction") - search_fields = ("id", "name", "description", "justification") - - list_filter = ( - "course_evaluation", - "is_introduction", - ) - - filter_horizontal = ( - "eoc_generals", - "eoc_specifics", - ) diff --git a/backend/documents/apps.py b/backend/documents/apps.py deleted file mode 100644 index 37ce729..0000000 --- a/backend/documents/apps.py +++ /dev/null @@ -1,6 +0,0 @@ -from django.apps import AppConfig - - -class DocumentsConfig(AppConfig): - default_auto_field = "django.db.models.BigAutoField" - name = "documents" diff --git a/backend/documents/migrations/0001_initial.py b/backend/documents/migrations/0001_initial.py deleted file mode 100644 index a0611b6..0000000 --- a/backend/documents/migrations/0001_initial.py +++ /dev/null @@ -1,33 +0,0 @@ -# Generated by Django 3.2.13 on 2022-08-16 06:32 - -import uuid - -import django.db.models.deletion -from django.db import migrations, models - - -class Migration(migrations.Migration): - - initial = True - - dependencies = [ - ("course_evaluations", "0002_courseevaluationjustification"), - ] - - operations = [ - migrations.CreateModel( - name="Document", - fields=[ - ("id", models.UUIDField(default=uuid.uuid4, editable=False, primary_key=True, serialize=False)), - ("name", models.CharField(max_length=50)), - ("description", models.TextField(blank=True)), - ("url", models.URLField()), - ("is_introduction", models.BooleanField()), - ("created_at", models.DateTimeField(auto_now_add=True)), - ("updated_at", models.DateTimeField(auto_now=True)), - ("course_evaluation", models.ForeignKey(on_delete=django.db.models.deletion.CASCADE, to="course_evaluations.courseevaluation")), - ("eoc_generals", models.ManyToManyField(to="course_evaluations.EOCGeneral")), - ("eoc_specifics", models.ManyToManyField(to="course_evaluations.EOCSpecific")), - ], - ), - ] diff --git a/backend/documents/migrations/0002_auto_20220818_1156.py b/backend/documents/migrations/0002_auto_20220818_1156.py deleted file mode 100644 index ede77a2..0000000 --- a/backend/documents/migrations/0002_auto_20220818_1156.py +++ /dev/null @@ -1,30 +0,0 @@ -# Generated by Django 3.2.13 on 2022-08-18 03:56 - -import django.db.models.deletion -from django.db import migrations, models - - -class Migration(migrations.Migration): - - dependencies = [ - ("course_evaluations", "0002_courseevaluationjustification"), - ("documents", "0001_initial"), - ] - - operations = [ - migrations.AlterField( - model_name="document", - name="course_evaluation", - field=models.ForeignKey(on_delete=django.db.models.deletion.CASCADE, related_name="documents", to="course_evaluations.courseevaluation"), - ), - migrations.AlterField( - model_name="document", - name="eoc_generals", - field=models.ManyToManyField(blank=True, to="course_evaluations.EOCGeneral"), - ), - migrations.AlterField( - model_name="document", - name="eoc_specifics", - field=models.ManyToManyField(blank=True, to="course_evaluations.EOCSpecific"), - ), - ] diff --git a/backend/documents/migrations/__init__.py b/backend/documents/migrations/__init__.py deleted file mode 100644 index e69de29..0000000 diff --git a/backend/documents/models.py b/backend/documents/models.py deleted file mode 100644 index 37ef302..0000000 --- a/backend/documents/models.py +++ /dev/null @@ -1,31 +0,0 @@ -import uuid - -from django.db import models - -# Create your models here. -from course_evaluations.models import CourseEvaluation, EOCGeneral, EOCSpecific - - -class Document(models.Model): - """ - A document is a url to a resource that a reviewer will look at - to judge the quality of a course - """ - - id = models.UUIDField(primary_key=True, default=uuid.uuid4, editable=False) - - # Main relations - course_evaluation = models.ForeignKey(CourseEvaluation, on_delete=models.CASCADE, related_name="documents") - - # Information about the document - name = models.CharField(max_length=50, null=False, blank=False) - description = models.TextField(null=False, blank=True) - url = models.URLField() - - # Tags Equivalence - is_introduction = models.BooleanField() - eoc_generals = models.ManyToManyField(EOCGeneral, blank=True) - eoc_specifics = models.ManyToManyField(EOCSpecific, blank=True) - - created_at = models.DateTimeField(auto_now_add=True) - updated_at = models.DateTimeField(auto_now=True) diff --git a/backend/documents/serializers.py b/backend/documents/serializers.py deleted file mode 100644 index 883aae6..0000000 --- a/backend/documents/serializers.py +++ /dev/null @@ -1,57 +0,0 @@ -from rest_framework import serializers - -from course_evaluations.models import ( - EOCGeneral, - EOCSpecific, -) -from documents.models import Document - - -class DocumentWriteSerializer(serializers.ModelSerializer): - """ - Note: It is important to understand that this serializer is only used for write operations for the most parts. - - This means that `eoc_generals` and `eoc_specifics` are expecting the an id (not the EOC number) - """ - - # Note: `read_only=False` is important to do patching and creations - eoc_generals = serializers.PrimaryKeyRelatedField(many=True, read_only=False, queryset=EOCGeneral.objects.all()) - eoc_specifics = serializers.PrimaryKeyRelatedField(many=True, read_only=False, queryset=EOCSpecific.objects.all()) - - # This is not required as we will force this to a value, see `documents.views,py` for `perform_create` and `perform_update` - course_evaluation = serializers.CharField(write_only=True, required=False) - - class Meta: - model = Document - fields = "__all__" - - -class EOCSpecificSerializerReadOnly(serializers.ModelSerializer): - class Meta: - model = EOCSpecific - fields = ("id", "number", "general_and_specific_eoc") - - -class EOCGeneralSerializerReadOnly(serializers.ModelSerializer): - class Meta: - model = EOCGeneral - fields = ("id", "number") - - -class DocumentReadOnlySerializer(serializers.ModelSerializer): - """ - Read only serializer for the Document model. - """ - - eoc_generals = EOCGeneralSerializerReadOnly( - many=True, - read_only=True, - ) - eoc_specifics = EOCSpecificSerializerReadOnly( - many=True, - read_only=True, - ) - - class Meta: - model = Document - fields = "__all__" diff --git a/backend/documents/tests/__init__.py b/backend/documents/tests/__init__.py deleted file mode 100644 index e69de29..0000000 diff --git a/backend/documents/urls.py b/backend/documents/urls.py deleted file mode 100644 index 697d4b1..0000000 --- a/backend/documents/urls.py +++ /dev/null @@ -1,8 +0,0 @@ -""" -Note: This is intentionally left out. -Reason: This resource is crossed between the CourseEvaluation and the Document models. - -See: -- `course_evaluations/urls.py` -- `reviews/urls.py` -""" diff --git a/backend/documents/views.py b/backend/documents/views.py deleted file mode 100644 index 7962c8b..0000000 --- a/backend/documents/views.py +++ /dev/null @@ -1,43 +0,0 @@ -from rest_framework import permissions, viewsets - -from course_evaluations.permissions import ( - CourseEvaluationIsCoordinatorAllowAllViaObjectReference, -) -from documents.models import Document -from documents.serializers import DocumentReadOnlySerializer, DocumentWriteSerializer - - -class CourseEvaluationDocumentViewSet(viewsets.ModelViewSet): - """ - Viewset that handles documents - - Permissions: - - Coordinator (DETAIL, CREATE, UPDATE, DELETE). Essentially for management of the documents for a particular course_evaluation - - Note: A reviewer should only see documents as part of their specific endpoint. See `reviews/serializers.py` or `reviews/views.py` - """ - - permission_classes = [ - permissions.IsAuthenticated, - CourseEvaluationIsCoordinatorAllowAllViaObjectReference, - ] - - def get_serializer_class(self): - if self.request.method == "GET": - return DocumentReadOnlySerializer - else: - return DocumentWriteSerializer - - def get_queryset(self): - return Document.objects.all().filter(course_evaluation=self.kwargs["course_evaluation_id"]) - - """ - For CREATE and UPDATE, we have to force the value of - `course_evaluation_id` to the url parameters (disregarding what the actual payload was) - """ - - def perform_create(self, serializer): - serializer.save(course_evaluation_id=self.kwargs["course_evaluation_id"]) - - def perform_update(self, serializer): - serializer.save(course_evaluation_id=self.kwargs["course_evaluation_id"]) diff --git a/backend/reviews/models.py b/backend/reviews/models.py index 2d499e0..4531822 100755 --- a/backend/reviews/models.py +++ b/backend/reviews/models.py @@ -2,8 +2,12 @@ from django.db import models -from course_evaluations.models import CourseEvaluation, DevelopmentLevels, EOCSpecific -from documents.models import Document +from course_evaluations.models import ( + CourseEvaluation, + DevelopmentLevels, + Document, + EOCSpecific, +) class Review(models.Model): From 7a5bd6a72e849fde616d214a18c9d37938ee1a10 Mon Sep 17 00:00:00 2001 From: Frinze Erin Lapuz Date: Thu, 18 Aug 2022 14:53:49 +0800 Subject: [PATCH 86/89] renew migrations with the document models --- .../migrations/0003_document.py | 36 +++++++++++++++++++ backend/reviews/migrations/0001_initial.py | 17 +++++---- .../migrations/0002_auto_20220818_1039.py | 23 ------------ .../0003_alter_review_unique_together.py | 20 ----------- 4 files changed, 47 insertions(+), 49 deletions(-) create mode 100644 backend/course_evaluations/migrations/0003_document.py delete mode 100644 backend/reviews/migrations/0002_auto_20220818_1039.py delete mode 100644 backend/reviews/migrations/0003_alter_review_unique_together.py diff --git a/backend/course_evaluations/migrations/0003_document.py b/backend/course_evaluations/migrations/0003_document.py new file mode 100644 index 0000000..d7a999b --- /dev/null +++ b/backend/course_evaluations/migrations/0003_document.py @@ -0,0 +1,36 @@ +# Generated by Django 3.2.13 on 2022-08-18 06:53 + +import uuid + +import django.db.models.deletion +from django.db import migrations, models + + +class Migration(migrations.Migration): + + dependencies = [ + ("course_evaluations", "0002_courseevaluationjustification"), + ] + + operations = [ + migrations.CreateModel( + name="Document", + fields=[ + ("id", models.UUIDField(default=uuid.uuid4, editable=False, primary_key=True, serialize=False)), + ("name", models.CharField(max_length=50)), + ("description", models.TextField(blank=True)), + ("url", models.URLField()), + ("is_introduction", models.BooleanField()), + ("created_at", models.DateTimeField(auto_now_add=True)), + ("updated_at", models.DateTimeField(auto_now=True)), + ( + "course_evaluation", + models.ForeignKey( + on_delete=django.db.models.deletion.CASCADE, related_name="documents", to="course_evaluations.courseevaluation" + ), + ), + ("eoc_generals", models.ManyToManyField(blank=True, to="course_evaluations.EOCGeneral")), + ("eoc_specifics", models.ManyToManyField(blank=True, to="course_evaluations.EOCSpecific")), + ], + ), + ] diff --git a/backend/reviews/migrations/0001_initial.py b/backend/reviews/migrations/0001_initial.py index c1e13cf..10873d3 100644 --- a/backend/reviews/migrations/0001_initial.py +++ b/backend/reviews/migrations/0001_initial.py @@ -1,4 +1,4 @@ -# Generated by Django 3.2.13 on 2022-08-16 06:32 +# Generated by Django 3.2.13 on 2022-08-18 06:53 import uuid @@ -12,9 +12,8 @@ class Migration(migrations.Migration): initial = True dependencies = [ + ("course_evaluations", "0003_document"), migrations.swappable_dependency(settings.AUTH_USER_MODEL), - ("documents", "0001_initial"), - ("course_evaluations", "0002_courseevaluationjustification"), ] operations = [ @@ -23,12 +22,15 @@ class Migration(migrations.Migration): fields=[ ("id", models.UUIDField(default=uuid.uuid4, editable=False, primary_key=True, serialize=False)), ("final_comment", models.TextField(blank=True)), - ("date_submitted", models.DateTimeField()), + ("date_submitted", models.DateTimeField(blank=True, null=True)), ("created_at", models.DateTimeField(auto_now_add=True)), ("updated_at", models.DateTimeField(auto_now=True)), - ("coordinators", models.ForeignKey(on_delete=django.db.models.deletion.CASCADE, related_name="reviews", to=settings.AUTH_USER_MODEL)), ("course_evaluation", models.ForeignKey(on_delete=django.db.models.deletion.CASCADE, to="course_evaluations.courseevaluation")), + ("reviewer", models.ForeignKey(on_delete=django.db.models.deletion.CASCADE, related_name="reviews", to=settings.AUTH_USER_MODEL)), ], + options={ + "unique_together": {("course_evaluation", "reviewer")}, + }, ), migrations.CreateModel( name="ReviewEocSpecific", @@ -59,7 +61,10 @@ class Migration(migrations.Migration): ("comment", models.TextField()), ("created_at", models.DateTimeField(auto_now_add=True)), ("updated_at", models.DateTimeField(auto_now=True)), - ("document", models.ForeignKey(on_delete=django.db.models.deletion.CASCADE, related_name="review_messages", to="documents.document")), + ( + "document", + models.ForeignKey(on_delete=django.db.models.deletion.CASCADE, related_name="review_messages", to="course_evaluations.document"), + ), ("review", models.ForeignKey(on_delete=django.db.models.deletion.CASCADE, related_name="documents", to="reviews.review")), ], ), diff --git a/backend/reviews/migrations/0002_auto_20220818_1039.py b/backend/reviews/migrations/0002_auto_20220818_1039.py deleted file mode 100644 index ce15067..0000000 --- a/backend/reviews/migrations/0002_auto_20220818_1039.py +++ /dev/null @@ -1,23 +0,0 @@ -# Generated by Django 3.2.13 on 2022-08-18 02:39 - -from django.db import migrations, models - - -class Migration(migrations.Migration): - - dependencies = [ - ("reviews", "0001_initial"), - ] - - operations = [ - migrations.RenameField( - model_name="review", - old_name="coordinators", - new_name="reviewer", - ), - migrations.AlterField( - model_name="review", - name="date_submitted", - field=models.DateTimeField(blank=True, null=True), - ), - ] diff --git a/backend/reviews/migrations/0003_alter_review_unique_together.py b/backend/reviews/migrations/0003_alter_review_unique_together.py deleted file mode 100644 index ce45f28..0000000 --- a/backend/reviews/migrations/0003_alter_review_unique_together.py +++ /dev/null @@ -1,20 +0,0 @@ -# Generated by Django 3.2.13 on 2022-08-18 04:18 - -from django.conf import settings -from django.db import migrations - - -class Migration(migrations.Migration): - - dependencies = [ - ("course_evaluations", "0002_courseevaluationjustification"), - migrations.swappable_dependency(settings.AUTH_USER_MODEL), - ("reviews", "0002_auto_20220818_1039"), - ] - - operations = [ - migrations.AlterUniqueTogether( - name="review", - unique_together={("course_evaluation", "reviewer")}, - ), - ] From 4d6e3df8a57d7261f8f45a42cc01a8f91a245c7a Mon Sep 17 00:00:00 2001 From: Frinze Erin Lapuz Date: Thu, 18 Aug 2022 14:58:49 +0800 Subject: [PATCH 87/89] fix tests failing due to non-permission of reviewer --- backend/course_evaluations/permissions.py | 15 +++++++++------ 1 file changed, 9 insertions(+), 6 deletions(-) diff --git a/backend/course_evaluations/permissions.py b/backend/course_evaluations/permissions.py index 701b9f1..596fd6a 100644 --- a/backend/course_evaluations/permissions.py +++ b/backend/course_evaluations/permissions.py @@ -16,12 +16,15 @@ def has_object_permission(self, request, view, obj): return request.user in obj.coordinators.all() -class CourseEvaluationIsCoordinatorAllowAllViaObjectReference(CourseEvaluationIsCoordinatorAllowAll): +class CourseEvaluationIsCoordinatorAllowAllViaObjectReference(permissions.BasePermission): """ - Custom permission to only allow coordinators the API (via Object Reference) - Similar to CourseEvaluationIsCoordinatorAllowAll + Custom permission to only allow coordinators the API based on the query parameters in the URL """ - def has_object_permission(self, request, view, obj): - # See that the difference is that we are going to be using the course_evaluation_id in the url - return request.user in obj.course_evaluation.coordinators.all() + def has_permission(self, request, view): + course_evaluation_id = view.kwargs["course_evaluation_id"] + + # Check whether the user has a course_evaluation that is desired + course_evaluations = request.user.course_evaluation_coordinator.all() + if course_evaluations.filter(id=course_evaluation_id).exists(): + return True \ No newline at end of file From 7d7865d246b01967c4454faa8c21e85fae4691b1 Mon Sep 17 00:00:00 2001 From: Frinze Erin Lapuz Date: Thu, 18 Aug 2022 15:05:30 +0800 Subject: [PATCH 88/89] flake8 fix --- backend/course_evaluations/permissions.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/backend/course_evaluations/permissions.py b/backend/course_evaluations/permissions.py index 596fd6a..13a50b6 100644 --- a/backend/course_evaluations/permissions.py +++ b/backend/course_evaluations/permissions.py @@ -27,4 +27,4 @@ def has_permission(self, request, view): # Check whether the user has a course_evaluation that is desired course_evaluations = request.user.course_evaluation_coordinator.all() if course_evaluations.filter(id=course_evaluation_id).exists(): - return True \ No newline at end of file + return True From 67b7b526795642f1a7b3c322729a6c91ba668dcf Mon Sep 17 00:00:00 2001 From: Frinze Erin Lapuz Date: Fri, 19 Aug 2022 11:41:12 +0800 Subject: [PATCH 89/89] remove unused comment --- backend/reviews/views.py | 2 -- 1 file changed, 2 deletions(-) diff --git a/backend/reviews/views.py b/backend/reviews/views.py index 185fdc7..f206fcb 100755 --- a/backend/reviews/views.py +++ b/backend/reviews/views.py @@ -7,8 +7,6 @@ ReviewSerializer, ) -# Create your views here. - class ReviewsViewSet(viewsets.ModelViewSet): """