diff --git a/.github/dependabot.yml b/.github/dependabot.yml new file mode 100644 index 00000000..33a42bc1 --- /dev/null +++ b/.github/dependabot.yml @@ -0,0 +1,14 @@ +version: 2 +updates: + - package-ecosystem: "npm" + directory: "/frontend" + schedule: + interval: "daily" + - package-ecosystem: "pip" + directory: "/backend" + schedule: + interval: "daily" + - package-ecosystem: "github-actions" + directory: "/" + schedule: + interval: "weekly" \ No newline at end of file diff --git a/.github/workflows/backend.yml b/.github/workflows/backend.yml index a7b6dee2..3d2d9b0d 100644 --- a/.github/workflows/backend.yml +++ b/.github/workflows/backend.yml @@ -2,7 +2,7 @@ name: backend-tests run-name: backend-tests on: [push] jobs: - Backend-linter-and-tests: + Backend-tests: runs-on: self-hosted environment: django tests strategy: @@ -13,7 +13,7 @@ jobs: steps: - uses: actions/checkout@v3 - name: Set up Python ${{ matrix.python-version }} - uses: actions/setup-python@v3 + uses: actions/setup-python@v4 with: python-version: ${{ matrix.python-version }} - name: Install Dependencies @@ -32,7 +32,27 @@ jobs: python manage.py migrate python manage.py test working-directory: ./backend + + Backend-linter: + runs-on: self-hosted + environment: django tests + strategy: + max-parallel: 4 + matrix: + python-version: [ 3.11 ] + + steps: + - uses: actions/checkout@v3 + - name: Set up Python ${{ matrix.python-version }} + uses: actions/setup-python@v4 + with: + python-version: ${{ matrix.python-version }} + - name: Install Dependencies + run: | + python -m pip install --upgrade pip + pip install flake8 + working-directory: ./backend - name: Run Linter run: | flake8 --ignore=F405,F403,E501 . - working-directory: ./backend + working-directory: ./backend \ No newline at end of file diff --git a/.github/workflows/frontend.yml b/.github/workflows/frontend.yml new file mode 100644 index 00000000..5224bfa8 --- /dev/null +++ b/.github/workflows/frontend.yml @@ -0,0 +1,65 @@ +name: frontend-tests +run-name: frontend-tests +on: [push] +jobs: + Frontend-tests: + runs-on: self-hosted + environment: vue tests + strategy: + max-parallel: 4 + + steps: + - uses: actions/checkout@v3 + - name: Set up Node + uses: actions/setup-node@v3 + with: + node-version: latest + - name: Empty EchoFetch directory + run: | + rm -rf EchoFetch + working-directory: ./frontend/src/api + - name: Checkout submodules + run: git submodule update --init --recursive + - name: Setup EchoFetch + run: | + npm install + npm run build + working-directory: ./frontend/src/api/EchoFetch + - name: Install Dependencies + run: | + npm install --force + working-directory: ./frontend + - name: Create env file + run: | + echo "${{ vars.ENV }}" > ./.env.local + working-directory: ./frontend + - name: Run Tests + run: | + npm run test:unit + working-directory: ./frontend + + Frontend-linter: + runs-on: self-hosted + environment: vue tests + strategy: + max-parallel: 4 + + steps: + - uses: actions/checkout@v3 + - name: Set up Node + uses: actions/setup-node@v3 + with: + node-version: latest + - name: Empty EchoFetch directory + run: | + rm -rf EchoFetch + working-directory: ./frontend/src/api + - name: Install Dependencies + run: | + npm install --force + npm i -D @vue/cli-plugin-eslint @vue/eslint-config-typescript --force + working-directory: ./frontend + - name: Run Linter + run: | + npm run lint + working-directory: ./frontend diff --git a/.gitignore b/.gitignore index 1478a4d2..c1ac9f76 100644 --- a/.gitignore +++ b/.gitignore @@ -79,3 +79,6 @@ fabric.properties # environment map env + +# coverage niet wordt meegepusht +coverage diff --git a/README.md b/README.md index 441fdfaf..069b7db9 100644 --- a/README.md +++ b/README.md @@ -1 +1,44 @@ -# Dr-Trottoir-5 \ No newline at end of file +# Dr-Trottoir-5 + +[![License](https://img.shields.io/badge/license-MIT-blue.svg)](LICENSE) + +> Een webapp gemaakt voor [Dr-Trottoir](https://drtrottoir.be/) om hun werk te structureren en automatiseren. + +## Table of Contents + +- [Features](#features) +- [Technologiën](#technologies) +- [Installation](#installation) +- [Usage](#usage) +- [License](#license) + +## Features + +- Nieuwe klanten en medewerkers toevoegen +- Rondes tonen aan de medewerkers +- Aanmaken/inplannen rondes +- Uitvoeren van een ronde +- Problemen tijdens uitvoeren terugsturen naar de klanten + +## Technologies + +De web app is gemaakt met deze technologiën + +- [Vue.js](https://vuejs.org/) - Een progressief JavaScript-framework voor het bouwen van gebruikersinterfaces. +- [Django](https://www.djangoproject.com/) - Een high-level Python webframework. +- [Hugo Docsy](https://www.docsy.dev/) - Een Hugo-thema voor technische documentatiewebsites. + + +## Installation + +- see the README.md files in the [frontend](frontend/README.md), [backend](backend/README.md) and [docs](docs/README.md) folders + +## Usage + +- see the README.md files in the [frontend](frontend/README.md), [backend](backend/README.md) and [docs](docs/README.md) folders + + +## License + +This project is licensed under the [MIT License](LICENSE). You are free to use, modify, and distribute this project in accordance with the terms of the license. + diff --git a/backend/README.md b/backend/README.md index 39c4f3a8..ba16bf6e 100644 --- a/backend/README.md +++ b/backend/README.md @@ -1,5 +1,13 @@ # Dr-trottoir django backend +## Info +The django backend works with these extra modules + +- django rest framework + - Easily making REST API's +- simpleJWT + - simple json web tokens for the cookies and authentication + ## Setup The first thing to do is to clone the repository: diff --git a/backend/backend/.env.example b/backend/backend/.env.example index cb517e6b..e8569d71 100644 --- a/backend/backend/.env.example +++ b/backend/backend/.env.example @@ -4,4 +4,7 @@ DB_PASSWORD=your_password DB_HOST=localhost DB_PORT=5432 EMAIL=email@gmail.com -EMAIL_PASSWORD=my_app_password \ No newline at end of file +EMAIL_PASSWORD=my_app_password +DEBUG_MODE=True +SECRET_KEY=django-insecure-mz0gymvj@n5wl2p0yau(vj0e3jdx_wok78+ead*=p4)$w)g5(z +SECURE_COOKIES=True diff --git a/backend/backend/settings.py b/backend/backend/settings.py index c97d84b1..60ea19a2 100644 --- a/backend/backend/settings.py +++ b/backend/backend/settings.py @@ -24,10 +24,11 @@ # See https://docs.djangoproject.com/en/4.1/howto/deployment/checklist/ # SECURITY WARNING: keep the secret key used in production secret! -SECRET_KEY = 'django-insecure-mz0gymvj@n5wl2p0yau(vj0e3jdx_wok78+ead*=p4)$w)g5(z' # TODO generate new one and keep secret +SECRET_KEY = env.get_value('SECRET_KEY', str, 'django-insecure-mz0gymvj@n5wl2p0yau(vj0e3jdx_wok78+ead*=p4)$w)g5(z') # SECURITY WARNING: don't run with debug turned on in production! -DEBUG = False + +DEBUG = env.get_value('DEBUG_MODE', bool, False) ALLOWED_HOSTS = [ 'sel2-5.ugent.be', '157.193.244.115', @@ -42,6 +43,8 @@ "http://192.168.178.34:8080", "http://localhost:8081", "http://192.168.178.34:8081", + "http://localhost:8082", + "http://192.168.178.34:8082", "http://127.0.0.1:8080", ] @@ -213,7 +216,7 @@ 'AUTH_COOKIE': 'access_token', # Cookie name. Enables cookies if value is set. 'REFRESH_COOKIE': 'refresh', 'AUTH_COOKIE_DOMAIN': None, # A string like "example.com", or None for standard domain cookie. - 'AUTH_COOKIE_SECURE': False, # Whether the auth cookies should be secure (https:// only). TODO TRUE FOR PRODUCTION + 'AUTH_COOKIE_SECURE': env.get_value('SECURE_COOKIES', bool, False), # Whether the auth cookies should be secure (https:// only). 'AUTH_COOKIE_HTTP_ONLY': True, # Http only cookie flag.It's not fetch by javascript. 'AUTH_COOKIE_PATH': '/', # The path of the auth cookie. 'AUTH_COOKIE_SAMESITE': 'Lax', diff --git a/backend/backend/views.py b/backend/backend/views.py index 34909423..b7b6a84d 100644 --- a/backend/backend/views.py +++ b/backend/backend/views.py @@ -1,11 +1,11 @@ from django.conf import settings from django.views.static import serve -from rest_framework.permissions import IsAuthenticated +from rest_framework.permissions import IsAuthenticatedOrReadOnly from rest_framework.views import APIView class MediaView(APIView): - permission_classes = [IsAuthenticated] + permission_classes = [IsAuthenticatedOrReadOnly] def get(self, request, path): return serve(request, path, document_root=settings.MEDIA_ROOT) diff --git a/backend/exceptions/exceptionHandler.py b/backend/exceptions/exceptionHandler.py index 7ca8b9c2..658ca1d5 100644 --- a/backend/exceptions/exceptionHandler.py +++ b/backend/exceptions/exceptionHandler.py @@ -1,8 +1,9 @@ from datetime import datetime -from rest_framework import serializers - from django.db import models +from rest_framework.serializers import ValidationError + +from trashtemplates.models import Status class ExceptionHandler: @@ -20,6 +21,10 @@ class ExceptionHandler: blank_error = "Veld kan niet leeg zijn." integer_error = "Veld moet een positief getal zijn." boolean_error = "Veld moet een Boolse waarde zijn." + wrong_email_error = "Verkeerd email adres." + not_equal_error = "Waarde komt niet overeen." + inactive_error = "Object is verwijderd." + vervangen_error = "Kan geen aanpassingen doen aan vervangen template" def __init__(self): self.errors = [] @@ -28,9 +33,9 @@ def __init__(self): def check(self): self.checked = True if len(self.errors) > 0: - raise serializers.ValidationError({ + raise ValidationError({ "errors": self.errors - }) + }, code='invalid') def __del__(self): if not self.checked: @@ -79,8 +84,13 @@ def check_time_value(self, value, fieldname): self.checked = False if value is None: return True - return self.check_time_format(value, fieldname, "%H:%M", - ExceptionHandler.time_format_error) + if not self.check_time_format(value, fieldname, "%H:%M", ExceptionHandler.time_format_error): + if self.check_time_format(value, fieldname, "%H:%M:%S", ExceptionHandler.time_format_error): + self.errors.pop() + return True + else: + return False + return True def check_time_value_required(self, value, fieldname): if not self.check_required(value, fieldname): @@ -209,3 +219,52 @@ def check_not_blank_required(self, value, fieldname): if not self.check_required(value, fieldname): return False return self.check_not_blank(value, fieldname) + + def check_email(self, email, cls: models.Model): + self.checked = False + if email is None: + return True + try: + cls.objects.get(email=email) + return True + except cls.DoesNotExist: + self.errors.append({ + "message": ExceptionHandler.wrong_email_error, + "field": "email" + }) + return False + + def check_equal(self, value1, value2, fieldname): + self.checked = False + + if value1 != value2: + self.errors.append({ + "message": ExceptionHandler.not_equal_error, + "field": fieldname + }) + return False + return True + + def check_not_inactive(self, template, fieldname): + self.checked = False + if template is None: + return True + + if template.status == Status.INACTIEF: + self.errors.append({ + "message": ExceptionHandler.inactive_error, + "field": fieldname + }) + return False + + def check_vervangen(self, template): + self.checked = False + if template is None: + return True + + if template.status == Status.VERVANGEN: + self.errors.append({ + "message": ExceptionHandler.vervangen_error, + "field": "template" + }) + return False diff --git a/backend/exceptions/tests.py b/backend/exceptions/tests.py index 758ea89a..ce874b55 100644 --- a/backend/exceptions/tests.py +++ b/backend/exceptions/tests.py @@ -7,6 +7,8 @@ from planning.models import WeekPlanning from .exceptionHandler import ExceptionHandler +from django.contrib.auth import get_user_model + class ExceptionHandlerTest(TestCase): @@ -275,29 +277,28 @@ def test_integer_required_fail_bad_value(self): def test_boolean_success(self): handler = ExceptionHandler() - self.assertTrue(handler.check_integer(False, "name")) + self.assertTrue(handler.check_boolean(False, "name")) def test_boolean_required_success(self): handler = ExceptionHandler() - self.assertTrue(handler.check_integer_required(True, "name")) + self.assertTrue(handler.check_boolean_required(True, "name")) def test_boolean_success_none(self): handler = ExceptionHandler() - self.assertTrue(handler.check_integer(None, "name")) + self.assertTrue(handler.check_boolean(None, "name")) def test_boolean_required_fail_none(self): handler = ExceptionHandler() - self.assertFalse(handler.check_integer_required(None, "name")) + self.assertFalse(handler.check_boolean_required(None, "name")) self.assertRaises(ValidationError, handler.check) def test_boolean_fail_bad_value(self): - handler = ExceptionHandler() - self.assertFalse(handler.check_integer("no integer", "name")) - self.assertRaises(ValidationError, handler.check) + # everything is a valid boolean if not required + self.assertTrue(True) def test_boolean_required_fail_bad_value(self): handler = ExceptionHandler() - self.assertFalse(handler.check_integer_required("no integer", "name")) + self.assertFalse(handler.check_boolean_required(None, "name")) self.assertRaises(ValidationError, handler.check) def test_not_blank_success(self): @@ -326,3 +327,22 @@ def test_not_blank_required_fail_bad_value(self): handler = ExceptionHandler() self.assertFalse(handler.check_not_blank_required("", "name")) self.assertRaises(ValidationError, handler.check) + + def test_email_success_none(self): + handler = ExceptionHandler() + self.assertTrue(handler.check_email(None, get_user_model())) + + def test_email_fail_bad_value(self): + handler = ExceptionHandler() + self.assertFalse( + handler.check_email("NOT A VALID EMAIL", get_user_model())) + self.assertRaises(ValidationError, handler.check) + + def test_check_equal_succes(self): + handler = ExceptionHandler() + self.assertTrue(handler.check_equal("test", "test", "password")) + + def test_check_equal_fail(self): + handler = ExceptionHandler() + self.assertFalse(handler.check_equal("not", "equal", "password")) + self.assertRaises(ValidationError, handler.check) diff --git a/backend/mailtemplates/tests.py b/backend/mailtemplates/tests.py index e69de29b..23daf911 100644 --- a/backend/mailtemplates/tests.py +++ b/backend/mailtemplates/tests.py @@ -0,0 +1,71 @@ +from rest_framework.test import APITestCase, APIRequestFactory, \ + force_authenticate + +from mailtemplates.views import * +from django.contrib.auth import get_user_model + + +class MailTemplateTest(APITestCase): + def setUp(self) -> None: + self.user = get_user_model().objects.create(role="SU") + + def testCreate(self): + factory = APIRequestFactory() + request = factory.get("/api/mailtemplates/") + force_authenticate(request, self.user) + response = MailTemplateCreateAndListView.as_view()(request).data + self.assertEqual(len(response), 0) + + request = factory.post("/api/mailtemplates/", + { + "name": "test", + "content": "test content" + }) + force_authenticate(request, self.user) + response = MailTemplateCreateAndListView.as_view()(request).data + self.assertIn("id", response) + + request = factory.get("/api/mailtemplates/") + force_authenticate(request, self.user) + response = MailTemplateCreateAndListView.as_view()(request).data + self.assertEqual(len(response), 1) + + def testGet(self): + factory = APIRequestFactory() + # create template + request = factory.post("/api/mailtemplates/", + { + "name": "test", + "content": "test content" + }) + force_authenticate(request, self.user) + response = MailTemplateCreateAndListView.as_view()(request).data + id = response["id"] + + request = factory.get(f"/api/mailtemplates/{id}/") + force_authenticate(request, self.user) + response = MailTemplateRetrieveUpdateDestroyAPIView.as_view()( + request, pk=id) + self.assertEqual(response.status_code, 200) + + def testPatch(self): + factory = APIRequestFactory() + # create template + request = factory.post("/api/mailtemplates/", + { + "name": "test", + "content": "test content" + }) + force_authenticate(request, self.user) + response = MailTemplateCreateAndListView.as_view()(request).data + id = response["id"] + + request = factory.patch(f"/api/mailtemplates/{id}/", + { + 'name': 'updated name' + }) + force_authenticate(request, self.user) + response = MailTemplateRetrieveUpdateDestroyAPIView.as_view()( + request, pk=id) + self.assertLess(response.status_code, 400) + self.assertEqual(response.data["name"], "updated name") diff --git a/backend/mailtemplates/views.py b/backend/mailtemplates/views.py index 14823f83..a9d2c650 100644 --- a/backend/mailtemplates/views.py +++ b/backend/mailtemplates/views.py @@ -2,6 +2,7 @@ from .models import MailTemplate from .serializers import MailTemplateSerializer from users.permissions import AdminPermission, SuperstudentPermission +from exceptions.exceptionHandler import ExceptionHandler class MailTemplateCreateAndListView(generics.ListCreateAPIView): @@ -9,8 +10,24 @@ class MailTemplateCreateAndListView(generics.ListCreateAPIView): serializer_class = MailTemplateSerializer permission_classes = [AdminPermission | SuperstudentPermission] + def post(self, request, *args, **kwargs): + data = request.data + handler = ExceptionHandler() + handler.check_not_blank_required(data.get("name"), "name") + handler.check_not_blank_required(data.get("content"), "content") + handler.check() + return super().post(request, *args, **kwargs) + class MailTemplateRetrieveUpdateDestroyAPIView(generics.RetrieveUpdateDestroyAPIView): queryset = MailTemplate.objects.all() serializer_class = MailTemplateSerializer permission_classes = [AdminPermission | SuperstudentPermission] + + def patch(self, request, *args, **kwargs): + data = request.data + handler = ExceptionHandler() + handler.check_not_blank(data.get("name"), "name") + handler.check_not_blank(data.get("content"), "content") + handler.check() + return super().patch(request, *args, **kwargs) diff --git a/backend/pickupdays/serializers.py b/backend/pickupdays/serializers.py index 9ed160dd..feb3eed1 100644 --- a/backend/pickupdays/serializers.py +++ b/backend/pickupdays/serializers.py @@ -13,7 +13,7 @@ def create(self, validated_data): return pickup def validate(self, data): - if data['start_hour'] > data['end_hour']: + if data.get('start_hour', 0) > data.get('end_hour', 0): raise serializers.ValidationError({'start_hour': 'Starttijd mag niet later zijn dan eindtijd'}) return data diff --git a/backend/pickupdays/tests.py b/backend/pickupdays/tests.py index e69de29b..1b68bc24 100644 --- a/backend/pickupdays/tests.py +++ b/backend/pickupdays/tests.py @@ -0,0 +1,50 @@ +from django.test import TestCase +from rest_framework.test import APIRequestFactory, force_authenticate +from .models import PickUpDay +from .views import PickUpListCreateView, PickUpDetailView +from users.models import User + + +class TestApi(TestCase): + def setUp(self): + self.user = User.objects.create(role="SU") + + def testCreatePickupDay(self): + """ + Test for creating a pickup day object + """ + factory = APIRequestFactory() + request = factory.post('/api/pickupdays/', {}) + force_authenticate(request, user=self.user) + response = PickUpListCreateView.as_view()(request).data + + # Test if errors are returned if request body is incomplete + self.assertIn("errors", response) + + request = factory.post('/api/pickupdays/', {"day": "MO", "start_hour": "12:00", "end_hour": "13:00"}) + force_authenticate(request, user=self.user) + response = PickUpListCreateView.as_view()(request).data + self.assertEqual(response["day"], "MO") + + def testComparePickupDays(self): + day1 = PickUpDay.objects.create(day="MO", start_hour="12:00", end_hour="13:00") + day2 = PickUpDay.objects.create(day="MO", start_hour="11:00", end_hour="13:00") + day3 = PickUpDay.objects.create(day="MO", start_hour="12:00", end_hour="13:00") + self.assertLess(day2, day1) + self.assertEqual(day1, day3) + + def testPickupdayPatch(self): + obj = PickUpDay.objects.create(day="MO", start_hour="12:00", end_hour="13:00") + factory = APIRequestFactory() + request = factory.patch(f'/api/pickupdays/{obj.pk}/', {"day": "TU"}) + force_authenticate(request, user=self.user) + response = PickUpDetailView.as_view()(request, pk=obj.pk).data + self.assertEqual(response["day"], "TU") + + def testPickupdayPut(self): + obj = PickUpDay.objects.create(day="MO", start_hour="12:00", end_hour="13:00") + factory = APIRequestFactory() + request = factory.put(f'/api/pickupdays/{obj.pk}/', {"day": "MO", "start_hour": "14:15", "end_hour": "13:15"}) + force_authenticate(request, user=self.user) + response = PickUpDetailView.as_view()(request, pk=obj.pk).data + self.assertIn("start_hour", response) diff --git a/backend/pickupdays/views.py b/backend/pickupdays/views.py index 259582bf..ee72d671 100644 --- a/backend/pickupdays/views.py +++ b/backend/pickupdays/views.py @@ -1,7 +1,7 @@ from rest_framework import generics from users.permissions import StudentReadOnly, AdminPermission, \ SuperstudentPermission -from .models import PickUpDay +from .models import PickUpDay, WeekDayEnum from .serializers import PickUpSerializer from exceptions.exceptionHandler import ExceptionHandler @@ -13,12 +13,11 @@ class PickUpListCreateView(generics.ListCreateAPIView): StudentReadOnly | AdminPermission | SuperstudentPermission] def post(self, request, *args, **kwargs): - data: dict - data = request.data + data: dict = request.data handler = ExceptionHandler() handler.check_enum_value_required(data.get("day"), "day", - PickUpDay.WeekDayEnum.values) + WeekDayEnum.values) handler.check_time_value_required(data.get("start_hour"), "start_hour") handler.check_time_value_required(data.get("end_hour"), "end_hour") handler.check() @@ -33,22 +32,21 @@ class PickUpDetailView(generics.RetrieveUpdateDestroyAPIView): StudentReadOnly | AdminPermission | SuperstudentPermission] def put(self, request, *args, **kwargs): - data: dict - data = request.data + data: dict = request.data handler = ExceptionHandler() handler.check_enum_value_required(data.get("day"), "day", - PickUpDay.WeekDayEnum.values) + WeekDayEnum.values) handler.check_time_value_required(data.get("start_hour"), "start_hour") handler.check_time_value_required(data.get("end_hour"), "end_hour") handler.check() return super().put(request, *args, **kwargs) def patch(self, request, *args, **kwargs): - data = request.data + data: dict = request.data handler = ExceptionHandler() handler.check_enum_value(data.get("day"), "day", - PickUpDay.WeekDayEnum.values) + WeekDayEnum.values) handler.check_time_value(data.get("start_hour"), "start_hour") handler.check_time_value(data.get("end_hour"), "end_hour") handler.check() diff --git a/backend/planning/models.py b/backend/planning/models.py index bc0ad551..05997195 100644 --- a/backend/planning/models.py +++ b/backend/planning/models.py @@ -1,10 +1,8 @@ -from django.contrib.postgres.fields import ArrayField from django.db import models from django.conf import settings -from ronde.models import Ronde +from ronde.models import LocatieEnum, Building, Ronde from trashtemplates.models import TrashContainerTemplate, Status from pickupdays.models import PickUpDay -from ronde.models import LocatieEnum class DagPlanning(models.Model): @@ -34,21 +32,6 @@ class DagPlanning(models.Model): on_delete=models.DO_NOTHING ) - class StatusEnum(models.TextChoices): - """ - enum for type of status - """ - NOT_STARTED = "NS", "Not started" - STARTED = "ST", "Started" - FINISHED = "FI", "Finished" - - status = ArrayField( - models.CharField( - max_length=2, - choices=StatusEnum.choices - ), default=list - ) - class StudentTemplate(models.Model): """ @@ -91,6 +74,8 @@ class StudentTemplate(models.Model): def __getitem__(self, item): if item == "dag_planningen": return self.dag_planningen + if item == "rondes": + return self.rondes name = models.TextField() even = models.BooleanField() @@ -158,14 +143,17 @@ class InfoPerBuilding(models.Model): dagPlanning : models.ForeignKey The associated DagPlanning + + building : models.Foreignkey + The associated Building """ remark = models.TextField(default="") - date = models.DateField() - dagPlanning = models.ForeignKey(DagPlanning, on_delete=models.CASCADE) + building = models.ForeignKey(Building, on_delete=models.CASCADE, blank=True, null=True) + class BuildingPicture(models.Model): """ diff --git a/backend/planning/tests.py b/backend/planning/tests.py index 6deaba87..9ea43341 100644 --- a/backend/planning/tests.py +++ b/backend/planning/tests.py @@ -1,56 +1,196 @@ -"""from rest_framework.test import APITestCase, APIRequestFactory, force_authenticate -from .views import * -from .models import * -from users.models import User -from ronde.models import Ronde from io import BytesIO + from PIL import Image from model_bakery import baker +from rest_framework.test import APITestCase, APIRequestFactory, \ + force_authenticate + +from backend.views import MediaView +from ronde.models import Ronde, LocatieEnum +from users.models import User +from .models import * +from .views import * class CreateTest(APITestCase): - def setUp(self) -> None: - self.wp = baker.make(WeekPlanning, week=0, year=2023) - self.dp = baker.make(DagPlanning, date="2023-08-31", weekPlanning=self.wp) + self.dp = baker.make(DagPlanning) self.ronde = baker.make(Ronde) self.ronde.save() - self.ipb = InfoPerBuilding.objects.create(remark="test", dagPlanning=self.dp) + self.ipb = InfoPerBuilding.objects.create(remark="test", + dagPlanning=self.dp) self.user = User.objects.create(role="SU") + self.location = LocatieEnum.objects.create(name="Gent") + self.student = User.objects.create(role="ST", username="student", + email="s@s.s") + self.studentTemplate = StudentTemplate.objects.create(name="test", + even=True, + location=self.location, + status=Status.EENMALIG, + year=2023, + week=1, + start_hour="19:13", + end_hour="19:14") + + templ = StudentTemplate.objects.create(name="test", + even=datetime.datetime.now( + + ).isocalendar().week % 2 == 0, + location=self.location, + status=Status.EENMALIG, + year=datetime.datetime.now().isocalendar().year, + week=datetime.datetime.now( + + ).isocalendar().week, + start_hour="19:13", + end_hour="19:14" + ) + self.weekPlanning = WeekPlanning.objects.create(week=1, year=2023) - def testAddWeekPlanning(self): + pl = WeekPlanning.objects.create(week=datetime.datetime.now().isocalendar().week, + year=datetime.datetime.now().isocalendar().year) + pl.student_templates.set([templ]) + + self.pickupday = PickUpDay.objects.create(day="SU", + start_hour="19:13", + end_hour="19:14") + self.dagPlanning = DagPlanning.objects.create(ronde=self.ronde, + time=self.pickupday) + self.dagPlanning.students.set([self.student, self.user]) + self.studentTemplate.dag_planningen.set([self.dagPlanning]) + self.weekPlanning.student_templates.set([self.studentTemplate]) + self.studentTemplate.rondes.set([self.ronde]) + + def testCreateStudentTemplate(self): + # Create a student template factory = APIRequestFactory() - request = factory.post("/api/planning/weekplanning/", {"week": 12, "year": 2023}) + date = datetime.datetime.now().isocalendar() + request = factory.post("/api/studenttemplates/", { + "location": self.location.id, + "name": "Gent template even", + "start_hour": "12:00", + "end_hour": "13:00", + "even": date.week % 2 == 0 + }) force_authenticate(request, user=self.user) - response = WeekPlanningCLAPIView.as_view()(request).data - self.assertEqual(response["week"], 12) - self.assertEqual(response["year"], 2023) - self.assertIsNotNone(response.get("id")) + response = StudentTemplateView.as_view()(request).data + self.assertIn("new_id", response) + template_id = response["new_id"] - def testAddDagPlanning(self): - factory = APIRequestFactory() - request = factory.post("/api/planning/dagplanning/", {"date": "2023-03-31", - "weekPlanning": self.wp.pk, - "ronde": self.ronde.pk, - "student": self.user.pk}) + # Get the newly created template + request = factory.get(f'/api/studenttemplates/{template_id}/') + force_authenticate(request, user=self.user) + response = StudentTemplateView.as_view()(request, template_id).data + self.assertIn("id", response[0]) + + # Add a round to the template + request = factory.post(f'/api/studenttemplates/{template_id}/rondes/', + { + "ronde": self.ronde.id + }) force_authenticate(request, user=self.user) - response = DagPlanningCreateAndListAPIView.as_view()(request).data - self.assertEqual(response["date"], "2023-03-31") - self.assertEqual(response["weekPlanning"], self.wp.pk) - self.assertIsNotNone(response.get("id")) - def testAddInfoPerBuilding(self): + response = RondesView.as_view()(request, template_id=template_id).data + + self.assertEqual(response["message"], "Success") + + request = factory.get(f'/api/studenttemplates/{template_id}/rondes/') + force_authenticate(request, user=self.user) + + response = RondesView.as_view()(request, template_id=template_id).data + + self.assertIn("buildings", response[0]) + + # Get the dayplans for this template + days = ["MO", "TU", "WE", "TH", "FR", "SA", "SU"] + request = factory.get( + f'/api/studenttemplates/{template_id}/rondes/{self.ronde.id}/dagplanningen/') + force_authenticate(request, user=self.user) + response = DagPlanningenView.as_view()(request, + template_id=template_id, + ronde_id=self.ronde.id).data + today = [x for x in response if + x["time"]["day"] == days[date.weekday - 1]] + self.assertNotEquals(today, []) + today = today[0] + + # Add a student to a dayplan of the template + request = factory.patch( + f'/api/studenttemplates/{template_id}/dagplanningen/{today["id"]}/', + { + "students": [self.student.id] + }) + force_authenticate(request, user=self.user) + response = DagPlanningView.as_view()(request, template_id=template_id, + dag_id=today["id"], + permanent=True).data + self.assertEqual(response["message"], "Success") + + request = factory.get(f'/api/weekplanning/{date.year}/{date.week}/') + force_authenticate(request, user=self.user) + response = WeekplanningView.as_view()(request, year=date.year, + week=date.week) + self.assertEqual(response.status_code, 200) + + # Find the dayplan for a certain time + request = factory.get( + f'/api/dagplanning/{date.year}/{date.week}/{date.weekday}/') + force_authenticate(request, user=self.student) + response = StudentDayPlan.as_view()(request, year=date.year, + week=date.week, + day=date.weekday).data + self.assertEqual(len(response), 1) + self.assertEqual( + StudentDayPlan.as_view()(request, year=date.year, week=date.week, + day=8).status_code, + 400) + + request = factory.get( + f'studenttemplates/rondes/{date.year}/{date.week}/{date.weekday}/6/') + force_authenticate(request, user=self.user) + StudentTemplateRondeView.as_view()(request, year=date.year, + week=date.week, + day=date.weekday, location=6) + + request = factory.patch( + f'/api/studenttemplates/{template_id}/dagplanningen/{today["id"]}/eenmalig/', + { + "students": [] + }) + force_authenticate(request, user=self.user) + DagPlanningView.as_view()(request, template_id=template_id, + dag_id=today["id"], + permanent=False) + + request = factory.get('/api/studenttemplates/') + force_authenticate(request, user=self.user) + templates = StudentTemplateView.as_view()(request).data + for template in templates: + request = factory.patch(f'/api/studenttemplates/{template["id"]}/', + { + "start_hour": "11:00", + "end_hour": "14:00" + }) + force_authenticate(request, user=self.user) + StudentTemplateView.as_view()(request, template["id"]) + + request = factory.get('/api/studenttemplates/') + force_authenticate(request, user=self.user) + templates = StudentTemplateView.as_view()(request).data + for template in templates: + request = factory.delete( + f'/api/studenttemplates/{template["id"]}/') + force_authenticate(request, user=self.user) + StudentTemplateView.as_view()(request, template["id"]) + + def testInfoPerBuilding(self): + # An error should be returned if an invalid dayplanning query is given factory = APIRequestFactory() - request = factory.post("/api/planning/infoperbuilding/", { - "remark": "This is a test remark", - "dagPlanning": self.dp.pk - }) + request = factory.get('/api/infoperbuilding?dagPlanning=9') force_authenticate(request, user=self.user) response = InfoPerBuildingCLAPIView.as_view()(request).data - self.assertEqual(response["remark"], "This is a test remark") - self.assertEqual(response["dagPlanning"], self.dp.pk) - self.assertIsNotNone(response.get("id")) + self.assertIn("errors", response) def testAddBuildingPicture(self): file = BytesIO() @@ -60,7 +200,7 @@ def testAddBuildingPicture(self): file.seek(0) factory = APIRequestFactory() - request = factory.post("/api/planning/buildingpicture/", { + request = factory.post("/api/buildingpicture/", { "pictureType": "ST", "image": file, "time": "2002-03-27 22:33", @@ -74,4 +214,186 @@ def testAddBuildingPicture(self): self.assertEqual(response["time"], "2002-03-27T22:33:00+01:00") self.assertEqual(response["remark"], "testRemark"), self.assertEqual(response["infoPerBuilding"], self.ipb.pk) - self.assertIsNotNone(response["id"])""" + self.assertIsNotNone(response["id"]) + picture_id = response["id"] + + request = factory.get(response["image"]) + path = (response["image"].split("/"))[-1] + force_authenticate(request, self.user) + response = MediaView.as_view()(request, path=path) + self.assertEqual(response.status_code, 200) + + # Fetch the uploaded picture + request = factory.get('/api/buildingpicture/') + force_authenticate(request, user=self.user) + response = BuildingPictureCreateAndListAPIView.as_view()(request).data + self.assertIn("id", response[0]) + request = factory.get( + '/api/buildingpicture?infoPerBuilding=9&year=2002&week=11') + force_authenticate(request, user=self.user) + response = BuildingPictureCreateAndListAPIView.as_view()(request).data + self.assertIn("errors", response) + + # Patch the uploaded picture + request = factory.patch(f'/api/buildingpicture/{picture_id}/', { + "remark": "testRemark 2", + }) + force_authenticate(request, user=self.user) + response = BuildingPictureRUDAPIView.as_view()(request, + pk=picture_id).data + self.assertIn("id", response) + self.assertEqual(response["remark"], "testRemark 2") + # Put the uploaded picture + file = BytesIO() + image = Image.new('RGBA', size=(10, 10), color=(155, 0, 0)) + image.save(file, 'png') + file.name = 'test.png' + file.seek(0) + request = factory.put(f'/api/buildingpicture/{picture_id}/', { + "pictureType": "ST", + "image": file, + "time": "2002-03-27 22:33", + "remark": "testRemark 2", + "infoPerBuilding": self.ipb.pk + }) + force_authenticate(request, user=self.user) + response = BuildingPictureRUDAPIView.as_view()(request, + pk=picture_id).data + self.assertIn("id", response) + + def testGetWeekPlanning(self): + factory = APIRequestFactory() + request = factory.get("/api/weekplanning/2023/1") + force_authenticate(request, user=self.user) + response = WeekplanningView.as_view()(request, year=2023, week=1).data + # geen studenttemplates die bij de ronde horen + self.assertEqual(len(response), 1) + + def testGetDagPlanningDate(self): + factory = APIRequestFactory() + request = factory.get("/api/dagplanning/2023/1/1/") + force_authenticate(request, self.student) + response = StudentDayPlan.as_view()(request, year=2023, week=1, + day=0).data + self.assertEqual(len(response), 1) + + def testGetDagPlanning(self): + factory = APIRequestFactory() + request = factory.get(f"/api/dagplanning/{self.dagPlanning.pk}/") + force_authenticate(request, self.user) + response = DagPlanningRetrieveUpdateAPIView.as_view()(request, + pk=self.dagPlanning.pk).data + + self.assertIn("id", response) + + def testGetPlanningStatus(self): + factory = APIRequestFactory() + request = factory.get(f"/api/dagplanning/2023/1/" + f"{self.dagPlanning.pk}/status/") + force_authenticate(request, self.user) + response = planning_status(request, year=2023, + week=1, + pk=self.dagPlanning.pk).data + self.assertEqual(type(response), dict) + + def testGetPlanningPictures(self): + factory = APIRequestFactory() + request = factory.get(f"/api/dagplanning/2023/1/" + f"{self.dagPlanning.pk}/pictures/") + force_authenticate(request, self.user) + response = planning_pictures(request, year=2023, + week=1, + pk=self.dagPlanning.pk).data + self.assertEqual(type(response), dict) + + def testGetTemplateForPlanning(self): + factory = APIRequestFactory() + request = factory.get(f"/api/studenttemplates/find/planning/" + f"{self.dagPlanning.pk}/") + force_authenticate(request, self.user) + response = template_for_planning(request, pk=self.dagPlanning.pk).data + self.assertIn("template_id", response) + + def testStudentTemplateRondeTime(self): + factory = APIRequestFactory() + request = factory.get(f"/api/studenttemplates/rondes/2023/1/1/" + f"{self.location.pk}") + force_authenticate(request, self.user) + response = StudentTemplateRondeView.as_view()(request, year=2023, + week=1, day=1, + location=self.location.pk) + self.assertEqual(response.status_code, 200) + + def testStudentTemplates(self): + factory = APIRequestFactory() + request = factory.get("/api/studenttemplates/") + force_authenticate(request, self.user) + response = StudentTemplateView.as_view()(request).data + # template is niet actief + self.assertEqual(len(response), 0) + + def testStudentTemplate(self): + factory = APIRequestFactory() + request = factory.get(f"/api/studenttemplates/" + f"{self.studentTemplate.pk}/") + force_authenticate(request, self.user) + response = StudentTemplateDetailView.as_view()(request, + template_id=self.studentTemplate.pk) + self.assertEqual(response.data["id"], self.studentTemplate.id) + + def testStudentTemplatesRondes(self): + factory = APIRequestFactory() + request = factory.get(f"/api/studenttemplates/" + f"{self.studentTemplate.pk}/rondes") + force_authenticate(request, self.user) + response = RondesView.as_view()(request, + template_id=self.studentTemplate.pk) + self.assertEqual(response.status_code, 200) + self.assertEqual(response.data[0]["id"], self.ronde.id) + + def testStudentTemplateRonde(self): + factory = APIRequestFactory() + request = factory.get(f"/api/studenttemplates/" + f"{self.studentTemplate.pk}/rondes/" + f"{self.ronde.pk}/") + force_authenticate(request, self.user) + response = RondeView.as_view()(request, + template_id=self.studentTemplate.pk, + ronde_id=self.ronde.pk) + # get not supported + self.assertEqual(response.status_code, 405) + + def testStudentTemplateDagPlanningen(self): + factory = APIRequestFactory() + request = factory.get(f"/api/studenttemplates/" + f"{self.studentTemplate.pk}/" + f"rondes/{self.ronde.pk}/" + f"dagplanningen/") + force_authenticate(request, self.user) + response = DagPlanningenView.as_view()(request, template_id=self.studentTemplate.pk, ronde_id=self.ronde.pk) + self.assertEqual(response.status_code, 200) + self.assertEqual(response.data[0]["id"], self.dagPlanning.pk) + + def testStudentTemplateDagPlanningPermanent(self): + factory = APIRequestFactory() + request = factory.get(f"/api/studenttemplates/" + f"{self.studentTemplate.pk}/dagplanningen/" + f"{self.dagPlanning.pk}/") + force_authenticate(request, self.user) + response = DagPlanningView.as_view()(request, + template_id=self.studentTemplate.pk, + dag_id=self.dagPlanning.pk) + self.assertEqual(response.status_code, 200) + self.assertEqual(response.data["id"], self.dagPlanning.pk) + + def testStudentTemplateDagPlanningNonPermanent(self): + factory = APIRequestFactory() + request = factory.get(f"/api/studenttemplates/" + f"{self.studentTemplate.pk}/dagplanningen/" + f"{self.dagPlanning.pk}/eenmalig/") + force_authenticate(request, self.user) + response = DagPlanningView.as_view()(request, + template_id=self.studentTemplate.pk, + dag_id=self.dagPlanning.pk) + self.assertEqual(response.status_code, 200) + self.assertEqual(response.data["id"], self.dagPlanning.pk) diff --git a/backend/planning/urls.py b/backend/planning/urls.py index d9a11a52..92ff8567 100644 --- a/backend/planning/urls.py +++ b/backend/planning/urls.py @@ -4,19 +4,32 @@ urlpatterns = [ path("buildingpicture/", views.BuildingPictureCreateAndListAPIView.as_view()), path("buildingpicture//", views.BuildingPictureRUDAPIView.as_view()), + path("infoperbuilding/", views.InfoPerBuildingCLAPIView.as_view()), path("infoperbuilding//", views.InfoPerBuildingRUDAPIView.as_view()), - path("weekplanning///", views.week_planning_view), - path("dagplanning////", views.student_dayplan), + + path("weekplanning///", views.WeekplanningView.as_view()), + + path("dagplanning////", + views.StudentDayPlan.as_view()), path("dagplanning//", views.DagPlanningRetrieveUpdateAPIView.as_view()), - path("studenttemplates/rondes/////", views.student_templates_rondes_view), - path("studenttemplates/", views.student_templates_view), - path("studenttemplates//", views.student_template_view), - path("studenttemplates//rondes/", views.rondes_view), - path("studenttemplates//rondes//", views.ronde_view), - path("studenttemplates//rondes//dagplanningen/", views.dagplanningen_view), - path("studenttemplates//dagplanningen//", views.dagplanning_view, {'permanent': True}), - path("studenttemplates//dagplanningen//eenmalig/", views.dagplanning_view, - {'permanent': False}) + path("dagplanning////status/", views.planning_status), + path("dagplanning////pictures/", views.planning_pictures), + path("studenttemplates/find/planning//", views.template_for_planning), + path("studenttemplates/rondes/////", views.StudentTemplateRondeView.as_view()), + path("studenttemplates/", views.StudentTemplateView.as_view()), + path("studenttemplates//", + views.StudentTemplateDetailView.as_view()), + path("studenttemplates//rondes/", views.RondesView.as_view()), + path("studenttemplates//rondes//", + views.RondeView.as_view()), + path("studenttemplates//rondes//dagplanningen/", views.DagPlanningenView.as_view()), + path("studenttemplates//dagplanningen//", + views.DagPlanningView.as_view(), {'permanent': True}), + path("studenttemplates//dagplanningen//eenmalig/", views.DagPlanningView.as_view(), + {'permanent': False}) ] diff --git a/backend/planning/util.py b/backend/planning/util.py index a53cc18a..842b1ac1 100644 --- a/backend/planning/util.py +++ b/backend/planning/util.py @@ -2,9 +2,25 @@ from trashtemplates.models import Status, TrashContainerTemplate from pickupdays.models import PickUpDay import datetime +from exceptions.exceptionHandler import ExceptionHandler from ronde.models import Ronde +from pickupdays.models import WeekDayEnum + + +def get_current_time(): + current_year = datetime.datetime.utcnow().strftime("%Y") + current_week = datetime.datetime.utcnow().strftime("%U") + return int(current_year), int(current_week) + + +def is_past(year, week): + current_year, current_week = get_current_time() + if year != current_year: + return year < current_year + return week < current_week + def filter_templates(templates): """ @@ -14,16 +30,14 @@ def filter_templates(templates): Hierna blijven dus alleen maar de templates over die actief zijn of in de huidige week eenmalig of vervangen zijn. """ - current_year, current_week, _ = datetime.datetime.utcnow().isocalendar() for template in templates: - is_current = template.week == current_week or template.year == current_year # template was tijdelijk veranderd maar de week is voorbij dus nu geldt deze weer - if template.status == Status.VERVANGEN and not is_current: + if template.status == Status.VERVANGEN and is_past(template.year, template.week): template.status = Status.ACTIEF template.save() # template was tijdelijk maar de week is voorbij dus nu geldt deze niet meer - elif template.status == Status.EENMALIG and not is_current: + elif template.status == Status.EENMALIG and is_past(template.year, template.week): template.status = Status.INACTIEF template.save() @@ -32,13 +46,23 @@ def filter_templates(templates): return result +def get_student_template(template_id): + handler = ExceptionHandler() + handler.check_primary_key(template_id, "template_id", StudentTemplate) + handler.check() + template = StudentTemplate.objects.get(id=template_id) + handler.check_not_inactive(template, "template") + handler.check() + return template + + def get_current_week_planning(): """ Geef de huidige week planning als die bestaat. Anders maak je die aan door al de actieve templates toe te voegen en alleen de even/oneven bij te houden. """ - current_year, current_week, _ = datetime.datetime.utcnow().isocalendar() + current_year, current_week = get_current_time() planning, created = WeekPlanning.objects.get_or_create( week=current_week, @@ -63,6 +87,13 @@ def make_dag_planning(data): """ Maakt een nieuwe DagPlanning aan. """ + handler = ExceptionHandler() + handler.check_time_value_required(data.get("start_hour"), "start_hour") + handler.check_time_value_required(data.get("end_hour"), "end_hour") + handler.check_enum_value_required(data.get("day"), "day", WeekDayEnum) + handler.check_required(data.get("ronde"), "ronde") + handler.check() + pickup_day, _ = PickUpDay.objects.get_or_create( day=data["day"], start_hour=data["start_hour"], @@ -74,9 +105,8 @@ def make_dag_planning(data): time=pickup_day ) - for _ in ronde.buildings.all(): - InfoPerBuilding(dagPlanning=dag_planning, date=datetime.datetime.now()).save() - dag_planning.status.append('NS') + for building in ronde.buildings.all(): + InfoPerBuilding(dagPlanning=dag_planning, building=building).save() dag_planning.students.set(data["students"]) dag_planning.save() @@ -86,7 +116,12 @@ def make_dag_planning(data): def make_copy(template, permanent, current_year, current_week): """ Neemt een copy van een StudentTemplate zodat de geschiedenis behouden wordt + Als er een oneven template wordt aangepast in een even week zijn deze aanpassingen pas voor + de volgende week. """ + week = current_week + if (current_week % 2 == 0) != template.even: + week += 1 copy = StudentTemplate.objects.create( name=template.name, @@ -96,14 +131,14 @@ def make_copy(template, permanent, current_year, current_week): end_hour=template.end_hour, location=template.location, year=current_year, - week=current_week + week=week ) copy.rondes.set(template.rondes.all()) copy.dag_planningen.set(template.dag_planningen.all()) # verander de status van de nu oude template template.status = Status.INACTIEF if permanent else Status.VERVANGEN - template.week = current_week + template.week = week template.save() return copy @@ -120,10 +155,8 @@ def delete_old_dag_planning(old_dag_planningen, day, template): def validate_student_template_data(data): - # TODO error handling - pass - - -def validate_dag_planning_data(data): - # TODO error handling - pass + handler = ExceptionHandler() + handler.check_not_blank_required(data.get("name"), "name") + handler.check_time_value_required(data.get("start_hour"), "start_hour") + handler.check_time_value_required(data.get("end_hour"), "end_hour") + handler.check() diff --git a/backend/planning/views.py b/backend/planning/views.py index 79b67531..1da17ef1 100644 --- a/backend/planning/views.py +++ b/backend/planning/views.py @@ -1,47 +1,127 @@ from django.contrib.auth import get_user_model +from django.http import HttpResponseNotFound from rest_framework import generics from rest_framework.decorators import api_view, permission_classes -from rest_framework.permissions import AllowAny from rest_framework.response import Response +from rest_framework.serializers import ValidationError -from exceptions.exceptionHandler import ExceptionHandler -from pickupdays.models import WeekDayEnum +from ronde.models import Building, LocatieEnum, Ronde from ronde.serializers import RondeSerializer +from trashtemplates.models import Status from trashtemplates.util import add_if_match, remove_if_match, no_copy, update from users.permissions import StudentReadOnly, AdminPermission, \ - SuperstudentPermission, StudentPermission - + SuperstudentPermission, StudentPermission, SyndicusPermission, \ + BewonerPermission from .util import * -@api_view(["GET"]) -@permission_classes([StudentPermission]) -def student_dayplan(request, year, week, day): - if request.method == "GET": +class StudentDayPlan(generics.RetrieveAPIView): + permission_classes = [StudentPermission | SuperstudentPermission | AdminPermission] + + def get(self, request, *args, **kwargs): + year = kwargs.get("year") + week = kwargs.get("week") + day = kwargs.get("day") if day < 0 or day > 6: - return Response(status=400) + raise ValidationError({ + "errors": [ + {"message": "bad day"} + ] + }, code='invalid') days = ['SU', 'MO', 'TU', 'WE', 'TH', 'FR', 'SA'] day_name = days[day] - templates = StudentTemplate.objects.filter(year=year, week=week) - dayplan = None + templates = get_student_templates(year, week) + if templates is None: + return Response(status=404) + + dayplans = [] for template in templates: for plan in template.dag_planningen.all(): if plan.time.day == day_name and request.user in plan.students.all(): - dayplan = plan - break + dayplans.append(plan) - if dayplan is None: - return Response(status=404) + if len(dayplans) == 0: + return HttpResponseNotFound() - data = DagPlanningSerializerFull(dayplan).data + data = [] + for dayplan in dayplans: + data.append(DagPlanningSerializerFull(dayplan).data) return Response(data) +@api_view(["GET"]) +@permission_classes( + [StudentPermission | AdminPermission | SuperstudentPermission]) +def planning_status(request, year, week, pk): + if request.method == "GET": + try: + DagPlanning.objects.get(pk=pk) + except DagPlanning.DoesNotExist: + return Response(status=404) + + infos = InfoPerBuilding.objects.filter(dagPlanning=pk) + status = {} + for index, info in enumerate(infos): + pictures = BuildingPicture.objects.filter(infoPerBuilding=info.id, + time__year=year, + time__week=week) + if info.building.id not in status: + status[info.building.id] = {"AR": 0, "DE": 0, "ST": 0, "EX": 0} + + for picture in pictures: + status[info.building.id][picture.pictureType] += 1 + + return Response(status) + + +@api_view(["GET"]) +@permission_classes([AdminPermission | SuperstudentPermission]) +def template_for_planning(request, pk): + if request.method == "GET": + try: + DagPlanning.objects.get(pk=pk) + except DagPlanning.DoesNotExist: + return Response(status=404) + + student_templates = StudentTemplate.objects.filter( + dag_planningen__in=[pk]) + if len(student_templates) == 0: + return Response(status=404) + + return Response({"template_id": student_templates[0].id}) + + +@api_view(["GET"]) +@permission_classes([AdminPermission | SuperstudentPermission]) +def planning_pictures(request, year, week, pk): + if request.method == "GET": + try: + dayplan = DagPlanning.objects.get(pk=pk) + except DagPlanning.DoesNotExist: + return Response(status=404) + + buildings = [building.id for building in dayplan.ronde.buildings.all()] + infos = InfoPerBuilding.objects.filter(dagPlanning=pk) + pictures = {} + for index, info in enumerate(infos): + if buildings[index] not in pictures: + pictures[buildings[index]] = [] + pictures[buildings[index]] += BuildingPicture.objects.filter( + infoPerBuilding=info.id, + time__year=year, + time__week=week) + for k, v in pictures.items(): + pictures[k] = BuildingPictureSerializer(v, many=True).data + + return Response(pictures) + + class DagPlanningRetrieveUpdateAPIView(generics.RetrieveUpdateAPIView): queryset = DagPlanning.objects.all() serializer_class = DagPlanningSerializerFull - permission_classes = [StudentPermission | AdminPermission | SuperstudentPermission] + permission_classes = [ + StudentPermission | AdminPermission | SuperstudentPermission] class DagPlanningCreateAndListAPIView(generics.ListCreateAPIView): @@ -62,7 +142,7 @@ def get(self, request, *args, **kwargs): date=date) return Response(DagPlanningSerializerFull(dagPlanning).data) except DagPlanning.DoesNotExist: - raise serializers.ValidationError( + raise ValidationError( { "errors": [ { @@ -76,7 +156,7 @@ def get(self, request, *args, **kwargs): dagPlanning = DagPlanning.objects.get(student=student) return Response(DagPlanningSerializerFull(dagPlanning).data) except DagPlanning.DoesNotExist: - raise serializers.ValidationError( + raise ValidationError( { "errors": [ { @@ -103,8 +183,8 @@ def post(self, request, *args, **kwargs): ronde = Ronde.objects.get(pk=request.data["ronde"]) response = super().post(request=request, args=args, kwargs=kwargs) dagPlanning = DagPlanning.objects.get(pk=response.data["id"]) - for _ in ronde.buildings.all(): - InfoPerBuilding(dagPlanning=dagPlanning).save() + for building in ronde.buildings.all(): + InfoPerBuilding(dagPlanning=dagPlanning, building=building).save() dagPlanning.status.append('NS') dagPlanning.save() return response @@ -146,19 +226,27 @@ class BuildingPictureCreateAndListAPIView(generics.ListCreateAPIView): queryset = BuildingPicture.objects.all() serializer_class = BuildingPictureSerializer permission_classes = [ - StudentPermission | AdminPermission | SuperstudentPermission] + StudentPermission | SyndicusPermission | AdminPermission | SuperstudentPermission | BewonerPermission + ] # TODO: a user can only see the pictures that he added (?) def get(self, request, *args, **kwargs): infoPerBuilding = request.query_params[ 'infoPerBuilding'] if 'infoPerBuilding' in request.query_params else None + year = request.query_params[ + 'year'] if 'year' in request.query_params else None + week = request.query_params[ + 'week'] if 'week' in request.query_params else None - if infoPerBuilding is not None: + if infoPerBuilding is not None and year is not None and week is not None: try: InfoPerBuilding.objects.get(pk=infoPerBuilding) self.queryset = BuildingPicture.objects.filter( - infoPerBuilding=infoPerBuilding) + infoPerBuilding=infoPerBuilding, + time__year=year, + time__week=week + ) except Exception: raise serializers.ValidationError( { @@ -182,6 +270,7 @@ def post(self, request, *args, **kwargs): handler.check_primary_key_value_required(data.get("infoPerBuilding"), "infoPerBuilding", InfoPerBuilding) + handler.check_date_time_value_required(data.get("time"), "time") handler.check() return super().post(request=request, args=args, kwargs=kwargs) @@ -205,7 +294,7 @@ def put(self, request, *args, **kwargs): "infoPerBuilding", InfoPerBuilding) handler.check() - super().put(request, *args, **kwargs) + return super().put(request, *args, **kwargs) def patch(self, request, *args, **kwargs): data = request.data @@ -216,14 +305,15 @@ def patch(self, request, *args, **kwargs): handler.check_primary_key(data.get("infoPerBuilding"), "infoPerBuilding", InfoPerBuilding) handler.check() - super().patch(request, *args, **kwargs) + return super().patch(request, *args, **kwargs) class InfoPerBuildingCLAPIView(generics.ListCreateAPIView): queryset = InfoPerBuilding.objects.all() serializer_class = InfoPerBuildingSerializer permission_classes = [ - StudentPermission | AdminPermission | SuperstudentPermission] + StudentPermission | SyndicusPermission | AdminPermission | SuperstudentPermission | BewonerPermission + ] # TODO: a user can only see the info per building that he added (?) @@ -231,11 +321,30 @@ def get(self, request, *args, **kwargs): dagPlanning = request.query_params[ 'dagPlanning'] if 'dagPlanning' in request.query_params else None + building = request.query_params[ + 'building'] if 'building' in request.query_params else None + if dagPlanning is not None: try: DagPlanning.objects.get(pk=dagPlanning) self.queryset = InfoPerBuilding.objects.filter( dagPlanning=dagPlanning) + if building is not None: + try: + Building.objects.get(pk=building) + self.queryset = self.queryset.filter( + building=building) + except Exception: + raise serializers.ValidationError( + { + "errors": [ + { + "message": "referenced pk not in db", + "field": "building" + } + ] + }, code='invalid') + except Exception: raise serializers.ValidationError( { @@ -245,7 +354,8 @@ def get(self, request, *args, **kwargs): "field": "dagPlanning" } ] - }, code='invalid') + } + ) return super().get(request=request, args=args, kwargs=kwargs) @@ -286,17 +396,24 @@ def patch(self, request, *args, **kwargs): def get_student_templates(year, week): - current_year, current_week, _ = datetime.datetime.utcnow().isocalendar() + current_year, current_week = get_current_time() if year > current_year or (current_year == year and week > current_week): # dit is een week die nog moet komen dus geven we alleen de actieve of nu tijdelijk vervangen templates terug - student_templates = StudentTemplate.objects.filter( - status=Status.ACTIEF) | StudentTemplate.objects.filter( - status=Status.VERVANGEN) + # buiten als de template vervangen is voor de volgende week + student_templates_actief = StudentTemplate.objects.filter( + status=Status.ACTIEF) + student_templates_vervangen = StudentTemplate.objects.filter( + status=Status.VERVANGEN).exclude(week=week, year=year) + student_templates_eenmalig = StudentTemplate.objects.filter( + status=Status.EENMALIG, week=week, year=year) + even = week % 2 == 0 + student_templates = student_templates_actief | student_templates_vervangen | student_templates_eenmalig student_templates = student_templates.filter(even=even) else: # weekplanning is al voorbij of bezig + get_current_week_planning() # nodig voor moest de weekplanning nog niet gemaakt zijn try: week_planning = WeekPlanning.objects.get( week=week, @@ -309,27 +426,33 @@ def get_student_templates(year, week): return student_templates -@api_view(["GET"]) -@permission_classes([AllowAny]) -def week_planning_view(request, year, week): - student_templates = get_student_templates(year, week) - if student_templates is None: - return Response(status=404) - data = StudentTemplateSerializer(student_templates, many=True).data - return Response(data) +class WeekplanningView(generics.RetrieveAPIView): + permission_classes = \ + [AdminPermission | SuperstudentPermission | StudentReadOnly] + def get(self, request, *args, **kwargs): + year = kwargs["year"] + week = kwargs["week"] + student_templates = get_student_templates(year, week) + if student_templates is None: + return HttpResponseNotFound() + data = StudentTemplateSerializer(student_templates, many=True).data + return Response(data) -@api_view(["GET"]) -@permission_classes([AdminPermission | SuperstudentPermission | AllowAny]) -def student_templates_rondes_view(request, year, week, day, location): - if request.method == "GET": + +class StudentTemplateRondeView(generics.RetrieveAPIView): + permission_classes = [AdminPermission | SuperstudentPermission | SyndicusPermission | BewonerPermission] + + def get(self, request, *args, **kwargs): + year, week, day, location = kwargs["year"], kwargs["week"], kwargs[ + "day"], kwargs["location"] if day < 0 or day > 6: - return Response(status=400) + raise ValidationError("bad day") days = ['SU', 'MO', 'TU', 'WE', 'TH', 'FR', 'SA'] day_name = days[day] templates = get_student_templates(year, week) if templates is None: - return Response(status=404) + return HttpResponseNotFound() templates = templates.filter(location=location) planned = [] for template in templates: @@ -340,10 +463,10 @@ def student_templates_rondes_view(request, year, week, day, location): return Response(planned) -@api_view(["GET", "POST"]) -@permission_classes([AllowAny]) -def student_templates_view(request): - if request.method == "GET": +class StudentTemplateView(generics.RetrieveAPIView, generics.CreateAPIView): + permission_classes = [AdminPermission | SuperstudentPermission] + + def get(self, request, *args, **kwargs): """ Geeft alle templates die niet inactief zijn terug. """ @@ -352,19 +475,20 @@ def student_templates_view(request): data = StudentTemplateSerializer(result, many=True).data return Response(data) - if request.method == "POST": + def post(self, request, *args, **kwargs): """ Maakt een nieuwe StudentTemplate aan. - TODO checks """ data = request.data - current_year, current_week, _ = datetime.datetime.utcnow().isocalendar() + current_year, current_week = get_current_time() handler = ExceptionHandler() handler.check_primary_key_value_required(data.get("location"), "location", LocatieEnum) handler.check_not_blank_required(data.get("name"), "name") - handler.check_time_value_required(data.get("start_hour"), "start_hour") - handler.check_time_value_required(data.get("end_hour"), "end_hour") + handler.check_not_blank_required(data.get("start_hour"), "start_hour") + handler.check_time_value(data.get("start_hour"), "start_hour") + handler.check_not_blank_required(data.get("end_hour"), "end_hour") + handler.check_time_value(data.get("end_hour"), "end_hour") handler.check_boolean_required(data.get("even"), "even") handler.check() @@ -381,27 +505,30 @@ def student_templates_view(request): week=current_week ) - add_if_match(get_current_week_planning().student_templates, new_template, current_week) + add_if_match(get_current_week_planning().student_templates, + new_template, current_week) return Response({"message": "Success", "new_id": new_template.id}) -@api_view(["GET", "DELETE", "PATCH"]) -@permission_classes([AllowAny]) -def student_template_view(request, template_id): - template = StudentTemplate.objects.get(id=template_id) - if request.method == "GET": +class StudentTemplateDetailView(generics.RetrieveUpdateDestroyAPIView): + + def get(self, request, *args, **kwargs): + """ Geeft de StudentTemplate terug. """ + template = StudentTemplate.objects.get(id=kwargs["template_id"]) return Response(StudentTemplateSerializer(template).data) - current_year, current_week, _ = datetime.datetime.utcnow().isocalendar() - if request.method == "DELETE": + current_year, current_week = get_current_time() + + def delete(self, request, *args, **kwargs): """ Verwijderd de StudentTemplate. Als deze eenmalig was mag deze volledig uit de database verwijderd worden en moet degene die vervangen was terug actief gezet worden. """ + template = StudentTemplate.objects.get(id=kwargs["template_id"]) planning = get_current_week_planning() if template.status == Status.EENMALIG: @@ -426,35 +553,23 @@ def student_template_view(request, template_id): return Response({"message": "Success"}) - if request.method == "PATCH": + def patch(self, request, *args, **kwargs): """ Past de StudentTemplate aan. Neemt een copy van de template om de geschiedenis te behouden als dit nodig is. """ + template = StudentTemplate.objects.get(id=kwargs["template_id"]) + current_year, current_week = get_current_time() data = request.data if "name" not in data: data["name"] = template.name - if "even" not in data: - data["even"] = template.even - - if "location" not in data: - data["location"] = template.location - else: - data["location"] = LocatieEnum.objects.get(id=data["location"]) - if "start_hour" not in data: data["start_hour"] = template.start_hour - else: - start_hour = [int(t) for t in data["start_hour"].split(":")] - data["start_hour"] = datetime.time(start_hour[0], start_hour[1]) if "end_hour" not in data: data["end_hour"] = template.end_hour - else: - end_hour = [int(t) for t in data["end_hour"].split(":")] - data["end_hour"] = datetime.time(end_hour[0], end_hour[1]) validate_student_template_data(data) @@ -463,52 +578,50 @@ def student_template_view(request, template_id): response = {"message": "Success"} if no_copy(template, True, current_year, current_week): template.name = data["name"] - template.even = data["even"] - template.location = data["location"] - # template.start_hour = data["start_hour"] - # template.end_hour = data["end_hour"], + template.start_hour = data["start_hour"] + template.end_hour = data["end_hour"] template.save() - add_if_match(planning.student_templates, template, current_week) return Response(response) new_template = StudentTemplate.objects.create( name=data["name"], - even=data["even"], - status=Status.ACTIEF, - location=data["location"], + even=template.even, + status=template.status, + location=template.location, start_hour=data["start_hour"], end_hour=data["end_hour"], year=current_year, - week=current_week + week=template.week ) add_if_match(planning.student_templates, new_template, current_week) # oude template op inactief zetten template.status = Status.INACTIEF template.save() - remove_if_match(planning.student_templates, template, current_week) + remove_if_match(planning.student_templates, template) response["new_id"] = new_template.id return Response(response) + def put(self, request, *args, **kwargs): + raise ValidationError("no PUT allowed") -@api_view(["GET", "POST"]) -@permission_classes([AllowAny]) -def rondes_view(request, template_id): - template = StudentTemplate.objects.get(id=template_id) - if request.method == "GET": +class RondesView(generics.RetrieveAPIView, generics.CreateAPIView): + def get(self, request, *args, **kwargs): """ Geeft alle rondes van deze template terug. """ + template = StudentTemplate.objects.get(id=kwargs["template_id"]) data = RondeSerializer(template.rondes.all(), many=True).data return Response(data) - if request.method == "POST": + def post(self, request, *args, **kwargs): """ Voegt een nieuwe Ronde toe aan de template. """ + template = StudentTemplate.objects.get(id=kwargs["template_id"]) data = request.data - current_year, current_week, _ = datetime.datetime.utcnow().isocalendar() + current_year, current_week = get_current_time() handler = ExceptionHandler() handler.check_primary_key_value_required(data.get("ronde"), "ronde", Ronde) @@ -517,12 +630,16 @@ def rondes_view(request, template_id): dag_planningen = [] - data["start_hour"] = template.start_hour - data["end_hour"] = template.end_hour - data["students"] = [] + data_copy = dict(data) + + data_copy["ronde"] = int(data["ronde"]) + + data_copy["start_hour"] = str(template.start_hour) + data_copy["end_hour"] = str(template.end_hour) + data_copy["students"] = [] for day in WeekDayEnum: - data["day"] = day - dag_planning = make_dag_planning(data) + data_copy["day"] = day + dag_planning = make_dag_planning(data_copy) dag_planningen.append(dag_planning) response = {"message": "Success"} @@ -533,25 +650,24 @@ def rondes_view(request, template_id): copy = make_copy(template, True, current_year, current_week) copy.rondes.add(ronde) copy.dag_planningen.add(*dag_planningen) - remove_if_match(get_current_week_planning().student_templates, template, current_week) - add_if_match(get_current_week_planning().student_templates, copy, current_week) + remove_if_match(get_current_week_planning().student_templates, + template) + add_if_match(get_current_week_planning().student_templates, copy, + current_week) response["new_id"] = copy.id return Response(response) -@api_view(["DELETE"]) -@permission_classes([AllowAny]) -def ronde_view(request, template_id, ronde_id): - template = StudentTemplate.objects.get(id=template_id) - ronde = Ronde.objects.get(id=ronde_id) - - current_year, current_week, _ = datetime.datetime.utcnow().isocalendar() +class RondeView(generics.DestroyAPIView): + def delete(self, request, *args, **kwargs): - if request.method == "DELETE": """ Verwijderd een ronde en al zijn dagplanningen uit de template. """ + template = StudentTemplate.objects.get(id=kwargs["template_id"]) + ronde = Ronde.objects.get(id=kwargs["ronde_id"]) + current_year, current_week = get_current_time() to_remove = template.dag_planningen.filter(ronde=ronde) response = {"message": "Success"} @@ -562,34 +678,36 @@ def ronde_view(request, template_id, ronde_id): copy = make_copy(template, True, current_year, current_week) copy.dag_planningen.remove(*to_remove) copy.rondes.remove(ronde) - remove_if_match(get_current_week_planning().student_templates, template, current_week) - add_if_match(get_current_week_planning().student_templates, copy, current_week) + remove_if_match(get_current_week_planning().student_templates, + template) + add_if_match(get_current_week_planning().student_templates, + copy, + current_week) response["new_id"] = copy.id return Response(response) -@api_view(["GET", "POST"]) -@permission_classes([AllowAny]) -def dagplanningen_view(request, template_id, ronde_id): - template = StudentTemplate.objects.get(id=template_id) - - if request.method == "GET": +class DagPlanningenView(generics.RetrieveAPIView, generics.CreateAPIView): + def get(self, request, *args, **kwargs): """ Geeft alle dagplanningen van een ronde terug. """ - dag_planningen = template.dag_planningen.filter(ronde=ronde_id) + template = StudentTemplate.objects.get(id=kwargs["template_id"]) + dag_planningen = template.dag_planningen.filter(ronde=kwargs[ + "ronde_id"]) data = DagPlanningSerializer(dag_planningen, many=True).data return Response(data) - if request.method == "POST": + def post(self, request, *args, **kwargs): """ Maakt een nieuwe DagPlanning aan. """ data = request.data - current_year, current_week, _ = datetime.datetime.utcnow().isocalendar() + template = StudentTemplate.objects.get(id=kwargs["template_id"]) + ronde_id = kwargs["ronde_id"] data["ronde"] = ronde_id - validate_dag_planning_data(data) + new_dag_planning = make_dag_planning(data) response = update( @@ -605,26 +723,23 @@ def dagplanningen_view(request, template_id, ronde_id): return Response(response) -@api_view(["GET", "DELETE", "PATCH"]) -@permission_classes([AllowAny]) -def dagplanning_view(request, template_id, dag_id, permanent): - template = StudentTemplate.objects.get(id=template_id) - - dag_planning = DagPlanning.objects.get(id=dag_id) - current_year, current_week, _ = datetime.datetime.utcnow().isocalendar() - - if request.method == "GET": +class DagPlanningView(generics.RetrieveUpdateDestroyAPIView): + def get(self, request, *args, **kwargs): """ Geeft een dagplanning terug """ + dag_planning = DagPlanning.objects.get(id=kwargs["dag_id"]) data = DagPlanningSerializer(dag_planning).data return Response(data) - if request.method == "DELETE": + def delete(self, request, *args, **kwargs): """ - Verwijder een DagPlanning van de template + Verwijdert een DagPlanning van de template """ + template = StudentTemplate.objects.get(id=kwargs["template_id"]) + dag_planning = DagPlanning.objects.get(id=kwargs["dag_id"]) + permanent = kwargs["permanent"] response = update( template, "dag_planningen", @@ -637,23 +752,26 @@ def dagplanning_view(request, template_id, dag_id, permanent): response["message"] = "Success" return Response(response) - if request.method == "PATCH": - """ - Verander de studenten voor een DagPlanning - """ - data = request.data - - data["day"] = dag_planning.time.day - if "start_hour" not in data: - data["start_hour"] = dag_planning.time.start_hour - if "end_hour" not in data: - data["end_hour"] = dag_planning.time.end_hour - if "ronde" not in data: - data["ronde"] = dag_planning.ronde.id - if "students" not in data: - data["students"] = dag_planning.students.all() + def put(self, request, *args, **kwargs): + raise ValidationError("no PUT allowed") - new_dag_planning = make_dag_planning(data) + def patch(self, request, *args, **kwargs): + data = request.data + template = StudentTemplate.objects.get(id=kwargs["template_id"]) + dag_planning = DagPlanning.objects.get(id=kwargs["dag_id"]) + permanent = kwargs["permanent"] + data_copy = dict(data) + data_copy["day"] = dag_planning.time.day + if "start_hour" not in data_copy: + data_copy["start_hour"] = str(dag_planning.time.start_hour) + if "end_hour" not in data_copy: + data_copy["end_hour"] = str(dag_planning.time.end_hour) + if "ronde" not in data_copy: + data_copy["ronde"] = int(dag_planning.ronde.id) + if "students" not in data_copy: + data_copy["students"] = dag_planning.students.all() + + new_dag_planning = make_dag_planning(data_copy) response = update( template, diff --git a/backend/requirements.txt b/backend/requirements.txt index 1e801183..414819b8 100644 --- a/backend/requirements.txt +++ b/backend/requirements.txt @@ -1,4 +1,4 @@ -Django~=4.1.7 +Django~=4.2.1 djangorestframework markdown django-filter @@ -9,4 +9,5 @@ Pillow model_bakery djangorestframework-simplejwt requests -django-cors-headers \ No newline at end of file +django-cors-headers +django-sortedm2m \ No newline at end of file diff --git a/backend/ronde/models.py b/backend/ronde/models.py index ce61b300..f13770f4 100644 --- a/backend/ronde/models.py +++ b/backend/ronde/models.py @@ -1,6 +1,7 @@ from django.contrib.postgres.fields import ArrayField from django.db import models from django.conf import settings +from sortedm2m.fields import SortedManyToManyField import uuid @@ -113,6 +114,8 @@ class Ronde(models.Model): The location of the ronde building : models.ManyToManyField The different buildings that need te be visited in a ronde + is_active: models.BooleanField + Tells if the round is replaced or not """ name = models.TextField() @@ -121,4 +124,5 @@ class Ronde(models.Model): on_delete=models.DO_NOTHING, verbose_name="Locatie" ) - buildings = models.ManyToManyField(Building, blank=True) + is_active = models.BooleanField(default=True) + buildings = SortedManyToManyField(Building, blank=True) diff --git a/backend/ronde/tests.py b/backend/ronde/tests.py index bd6c58e9..a56011bf 100644 --- a/backend/ronde/tests.py +++ b/backend/ronde/tests.py @@ -40,7 +40,7 @@ def testAddLocation(self): force_authenticate(request, user=self.user) response = LocatieEnumListCreateView.as_view()(request).data - self.assertEqual(response["succes"]["name"], "Oostende") + self.assertEqual(response["name"], "Oostende") def testGetLocations(self): """ @@ -59,10 +59,10 @@ def testGetLocation(self): response = LocatieEnumListCreateView.as_view()(request).data request = factory.get( - f'/api/ronde/locatie/{response["succes"]["id"]}/') + f'/api/ronde/locatie/{response["id"]}/') force_authenticate(request, user=self.user) response = LocatieEnumRetrieveDestroyView\ - .as_view()(request, pk=response["succes"]["id"]).data + .as_view()(request, pk=response["id"]).data self.assertEqual(response["name"], "Oostende") def testGetRondes(self): @@ -90,7 +90,7 @@ def testUploadManual(self): "file": file}) force_authenticate(request, user=self.user) response = ManualListCreateView.as_view()(request).data - self.assertEqual(response["succes"]["manualStatus"], "Klaar") + self.assertEqual(response["manualStatus"], "Klaar") # Test upload with missing argument request = factory.post('/api/building/manual/', diff --git a/backend/ronde/urls.py b/backend/ronde/urls.py index 2ef2e775..13d1ac06 100644 --- a/backend/ronde/urls.py +++ b/backend/ronde/urls.py @@ -10,5 +10,11 @@ path('building/manual/', views.ManualListCreateView.as_view()), path('building/manual//', views.ManualRetrieveUpdateDestroyAPIView.as_view()), path('building/', views.BuildingListCreateView.as_view()), - path('building//', views.BuildingRetrieveDestroyView.as_view()) + path('building/syndicus/', views.SyndicusBuildingListView.as_view()), + path('building//', views.BuildingRetrieveDestroyView.as_view()), + path('building/uuid//', + views.BuildingUUIDRetrieveView.as_view()), + path('building/uuid//reset/', + views.BuildingUUIDResetView.as_view()) + ] diff --git a/backend/ronde/views.py b/backend/ronde/views.py index c09a6bb5..d6a35145 100644 --- a/backend/ronde/views.py +++ b/backend/ronde/views.py @@ -1,45 +1,23 @@ -from .models import * - -from exceptions.exceptionHandler import ExceptionHandler import os -from django.conf import settings -from rest_framework import generics, status, serializers +from exceptions.exceptionHandler import ExceptionHandler +from rest_framework import generics, status from rest_framework.parsers import MultiPartParser, FormParser +from rest_framework.permissions import IsAuthenticatedOrReadOnly from rest_framework.response import Response from users.permissions import StudentReadOnly, AdminPermission, \ - SuperstudentPermission - -from .models import LocatieEnum, Manual, Building, Ronde + SuperstudentPermission, SyndicusPermission, AllowAnyReadOnly +from trashtemplates.util import update +from planning.util import get_current_week_planning, make_copy +from .models import * from .serializers import * class LocatieEnumListCreateView(generics.ListCreateAPIView): queryset = LocatieEnum.objects.all() serializer_class = LocatieEnumSerializer - permission_classes = [ - StudentReadOnly | AdminPermission | SuperstudentPermission] - - def create(self, request, *args, **kwargs): - """ - Post method that creates a Location record - Returns error when there isn't a name field or the field is empty - """ - serializer = self.get_serializer(data=request.data) - if serializer.is_valid(raise_exception=True): - serializer.save() - return Response({"succes": serializer.data}, - status=status.HTTP_201_CREATED) - elif "name" not in request.data: - return Response({"error": ["Er is geen veld 'name' meegegeven"]}, - status=status.HTTP_400_BAD_REQUEST) - elif request.data["name"] == "": - return Response({"error": "Het veld 'name' is leeg"}, - status=status.HTTP_400_BAD_REQUEST) - else: - return Response(serializer.errors, - status=status.HTTP_400_BAD_REQUEST) + permission_classes = [AllowAnyReadOnly | AdminPermission | SuperstudentPermission] def post(self, request, *args, **kwargs): data = request.data @@ -51,16 +29,13 @@ def post(self, request, *args, **kwargs): class LocatieEnumRetrieveDestroyView(generics.RetrieveUpdateDestroyAPIView): serializer_class = LocatieEnumSerializer + queryset = LocatieEnum.objects.all() permission_classes = [ StudentReadOnly | AdminPermission | SuperstudentPermission] """ View that deletes and gets a specific Location """ - def get_queryset(self): - id = self.kwargs['pk'] - return LocatieEnum.objects.filter(id=id) - def put(self, request, *args, **kwargs): data = request.data handler = ExceptionHandler() @@ -83,37 +58,6 @@ class ManualListCreateView(generics.ListCreateAPIView): permission_classes = [ StudentReadOnly | AdminPermission | SuperstudentPermission] - def create(self, request, *args, **kwargs): - """ - Post method that creates a Manual record. The manual is saved in media root - """ - serializer = self.get_serializer(data=request.data) - if serializer.is_valid(raise_exception=True): - serializer.save() - return Response({"succes": serializer.data}, - status=status.HTTP_201_CREATED) - elif "file" not in request.data or "fileType" not in request.data or "manualStatus" not in request.data: - return Response( - {"error": [ - "Een van de benodigde velden: 'file', 'fileType' of 'manualStatus is niet aanwezig"]}, - status=status.HTTP_400_BAD_REQUEST) - elif request.data["file"] == "" or request.data["fileType"] == "" or \ - request.data["manualStatus"] == "": - return Response( - {"error": [ - "Een van de benodigde velden: 'file', 'fileType' of 'manualStatus is leeg"]}, - status=status.HTTP_400_BAD_REQUEST) - elif request.data["manualStatus"] not in ["Klaar", "Update nodig", - "Bezig", "Geüpdatet"]: - return Response( - {"error": [ - "Er is een foute manual status meegegeven. Kies uit volgende opties: Klaar, Update nodig, Bezig of " - "Geüpdatet"]}, - status=status.HTTP_400_BAD_REQUEST) - else: - return Response(serializer.errors, - status=status.HTTP_400_BAD_REQUEST) - def post(self, request, *args, **kwargs): data: dict = request.data handler = ExceptionHandler() @@ -129,32 +73,13 @@ def post(self, request, *args, **kwargs): class ManualRetrieveUpdateDestroyAPIView( generics.RetrieveUpdateDestroyAPIView): serializer_class = ManualSerializer + queryset = Manual.objects.all() permission_classes = [ StudentReadOnly | AdminPermission | SuperstudentPermission] """ View that gets, deletes and updates a specific Manual """ - def partial_update(self, request, *args, **kwargs): - id = self.kwargs['pk'] - try: - manual = Manual.objects.get(id=id) - serializer = ManualSerializer(manual, data=request.data, - partial=True) - if serializer.is_valid(raise_exception=True): - serializer.save() - return Response(serializer.data) - except Manual.DoesNotExist: - raise serializers.ValidationError( - { - "errors": [ - { - "message": "referenced manual not in db", - "field": "id" - } - ] - }, code='invalid') - def delete(self, request, *args, **kwargs): id = self.kwargs['pk'] manual = Manual.objects.filter(id=id) @@ -167,10 +92,6 @@ def delete(self, request, *args, **kwargs): return Response({"succes": ["Manual is verwijdert"]}, status=status.HTTP_200_OK) - def get_queryset(self): - id = self.kwargs['pk'] - return Manual.objects.filter(id=id) - def put(self, request, *args, **kwargs): data: dict = request.data handler = ExceptionHandler() @@ -188,10 +109,19 @@ def patch(self, request, *args, **kwargs): handler.check_file(data.get("file"), "file", request.FILES) handler.check_not_blank(data.get("fileType"), "fileType") handler.check_enum_value(data.get("manualStatus"), "manualStatus", - ManualStatusField.value) + ManualStatusField.values) return super().patch(request, *args, **kwargs) +class SyndicusBuildingListView(generics.ListAPIView): + permission_classes = [SyndicusPermission] + serializer_class = BuildingSerializer + + def get(self, request, *args, **kwargs): + self.queryset = Building.objects.filter(syndicus__in=[request.user]) + return super().get(request, *args, **kwargs) + + class BuildingListCreateView(generics.ListCreateAPIView): queryset = Building.objects.all() serializer_class = BuildingSerializer @@ -201,6 +131,7 @@ class BuildingListCreateView(generics.ListCreateAPIView): def post(self, request, *args, **kwargs): data = request.data handler = ExceptionHandler() + handler.check_not_blank_required(data.get("name"), "name") handler.check_not_blank_required(data.get("adres"), "adres") handler.check_integer_required(data.get("ivago_klantnr"), "ivago_klantnr") @@ -214,33 +145,10 @@ def post(self, request, *args, **kwargs): class BuildingRetrieveDestroyView(generics.RetrieveUpdateDestroyAPIView): serializer_class = BuildingSerializerFull + queryset = Building.objects.all() permission_classes = [ StudentReadOnly | AdminPermission | SuperstudentPermission] - def partial_update(self, request, *args, **kwargs): - id = self.kwargs['pk'] - try: - building = Building.objects.get(id=id) - serializer = BuildingSerializer(building, data=request.data, - partial=True) - if serializer.is_valid(raise_exception=True): - serializer.save() - return Response({"succes": ["Updated building"]}) - except Building.DoesNotExist: - raise serializers.ValidationError( - { - "errors": [ - { - "message": "referenced building not in db", - "field": "id" - } - ] - }, code='invalid') - - def get_queryset(self): - id = self.kwargs['pk'] - return Building.objects.filter(id=id) - def put(self, request, *args, **kwargs): data = request.data handler = ExceptionHandler() @@ -258,6 +166,7 @@ def patch(self, request, *args, **kwargs): data = request.data handler = ExceptionHandler() handler.check_not_blank(data.get("adres"), "adres") + handler.check_not_blank(data.get("name"), "name") handler.check_integer(data.get("ivago_klantnr"), "ivago_klantnr") handler.check_primary_key(data.get("manual"), "manual", Manual) handler.check_primary_key(data.get("location"), "location", @@ -266,8 +175,36 @@ def patch(self, request, *args, **kwargs): return super().patch(request, *args, **kwargs) +class BuildingUUIDRetrieveView(generics.RetrieveAPIView): + serializer_class = BuildingSerializerFull + permission_classes = [IsAuthenticatedOrReadOnly] + + def get(self, request, *args, **kwargs): + id = kwargs["buildingid"] + buildings = Building.objects.filter(buildingID=id) + if buildings.exists(): + serializer = self.get_serializer(buildings.get()) + return Response(serializer.data) + return Response(status=404) + + +class BuildingUUIDResetView(generics.RetrieveAPIView): + permission_classes = \ + [SyndicusPermission | AdminPermission | SuperstudentPermission] + + def get(self, request, *args, **kwargs): + id = kwargs["buildingid"] + buildings = Building.objects.filter(buildingID=id) + if buildings.exists(): + building = buildings.get() + building.buildingID = uuid.uuid4() + building.save() + return Response({"message": "success"}) + return Response(status=404) + + class RondeListCreateView(generics.ListCreateAPIView): - queryset = Ronde.objects.all() + queryset = Ronde.objects.filter(is_active=True) serializer_class = RondeSerializer permission_classes = [ StudentReadOnly | AdminPermission | SuperstudentPermission] @@ -275,10 +212,10 @@ class RondeListCreateView(generics.ListCreateAPIView): def post(self, request, *args, **kwargs): data = request.data handler = ExceptionHandler() - handler.check_required(data.get("name"), "name") + handler.check_not_blank_required(data.get("name"), "name") handler.check_primary_key_value_required(data.get("location"), "location", LocatieEnum) - # TODO fix building list check + # TODO fix building list check ook put en patch handler.check() return super().post(request, *args, **kwargs) @@ -291,30 +228,59 @@ def get_serializer_class(self): class RondeRetrieveDestroyView(generics.RetrieveUpdateDestroyAPIView): serializer_class = RondeSerializer + queryset = Ronde.objects.all() permission_classes = [ StudentReadOnly | AdminPermission | SuperstudentPermission] - def get_queryset(self): - id = self.kwargs['pk'] - return Ronde.objects.filter(id=id) - - def put(self, request, *args, **kwargs): - data = request.data - handler = ExceptionHandler() - handler.check_not_blank_required("name") - handler.check_primary_key_value_required(data.get("location"), - "location", LocatieEnum) - handler.check_primary_key_value_required(data.get("buildings"), - "buildings", Building) - handler.check() - return super().put(request, *args, **kwargs) - def patch(self, request, *args, **kwargs): data = request.data handler = ExceptionHandler() - handler.check_not_blank("name") + handler.check_not_blank("name", "name") handler.check_primary_key(data.get("location"), "location", LocatieEnum) - handler.check_primary_key(data.get("buildings"), "buildings", Building) + # handler.check_primary_key(data.get("buildings"), "buildings", + # Building) handler.check() - return super().put(request, *args, **kwargs) + + id = kwargs["pk"] + + old = Ronde.objects.get(id=id) + + if "name" not in data: + data["name"] = old.name + if "location" not in data: + data["location"] = old.location + else: + data["location"] = LocatieEnum.objects.get(id=data["location"]) + + if "buildings" not in data: + data["buildings"] = old.buildings + + new_ronde = Ronde.objects.create( + name=data["name"], + location=data["location"] + ) + new_ronde.buildings.set(data["buildings"]) + + old.is_active = False + old.save() + + student_templates = get_current_week_planning().student_templates + + template = None + for student_template in student_templates.all(): + for ronde in student_template.rondes.all(): + if ronde.id == id: + template = student_template + + if template is not None: + update( + template, + "rondes", + old, + new_ronde, + True, + student_templates, + copy_template=make_copy + ) + return Response({"message": "success"}) diff --git a/backend/trashcontainers/views.py b/backend/trashcontainers/views.py index ea5268aa..9a4b148d 100644 --- a/backend/trashcontainers/views.py +++ b/backend/trashcontainers/views.py @@ -1,6 +1,6 @@ from rest_framework import generics from users.permissions import StudentReadOnly, AdminPermission, \ - SuperstudentPermission + SuperstudentPermission, SyndicusPermission, BewonerPermission from .serializers import * from .serializers import TrashContainerSerializer from exceptions.exceptionHandler import ExceptionHandler @@ -15,7 +15,7 @@ class TrashContainerListCreateView(generics.ListCreateAPIView): queryset = TrashContainer.objects.all() serializer_class = TrashContainerSerializer permission_classes = [ - StudentReadOnly | AdminPermission | SuperstudentPermission] + StudentReadOnly | AdminPermission | SuperstudentPermission | SyndicusPermission | BewonerPermission] def get(self, request, *args, **kwargs): building = request.query_params['building'] if 'building' in request.query_params else None diff --git a/backend/trashtemplates/urls.py b/backend/trashtemplates/urls.py index c6f0635d..8966eedf 100644 --- a/backend/trashtemplates/urls.py +++ b/backend/trashtemplates/urls.py @@ -3,14 +3,27 @@ from . import views urlpatterns = [ - path('', views.trash_templates_view), - path('/', views.trash_template_view), - path('/trashcontainers/', views.trash_containers_view, {'permanent': True}), - path('/trashcontainers/eenmalig/', views.trash_containers_view, {'permanent': False}), - path('/trashcontainers//', views.trash_container_view, {'permanent': True}), - path('/trashcontainers//eenmalig/', views.trash_container_view, {'permanent': False}), - path('/buildings/', views.buildings_view, {'permanent': True}), - path('/buildings/eenmalig/', views.buildings_view, {'permanent': False}), - path('/buildings//', views.building_view, {'permanent': True}), - path('/buildings//eenmalig/', views.building_view, {'permanent': False}) + path('', views.TrashTemplatesView.as_view()), + path('/', views.TrashTemplateView.as_view()), + path('/trashcontainers/', + views.TrashContainersView.as_view(), {'permanent': True}), + path('/trashcontainers/eenmalig/', + views.TrashContainersView.as_view(), {'permanent': False}), + path('/trashcontainers//', + views.TrashContainerView.as_view(), {'permanent': True}), + path('/trashcontainers//eenmalig/', + views.TrashContainerView.as_view(), {'permanent': False}), + path('/buildings/', views.BuildingsView.as_view(), + {'permanent': True}), + path('/buildings/eenmalig/', + views.BuildingsView.as_view(), + {'permanent': False}), + path('/buildings//', + views.BuildingView.as_view(), + {'permanent': True}), + path('/buildings//eenmalig/', + views.BuildingView.as_view(), + {'permanent': False}), + path("//", + views.BuildingTrashPlan.as_view()), ] diff --git a/backend/trashtemplates/util.py b/backend/trashtemplates/util.py index b255f554..c0aff6c3 100644 --- a/backend/trashtemplates/util.py +++ b/backend/trashtemplates/util.py @@ -2,14 +2,33 @@ from .serializers import * from trashcontainers.models import TrashContainer from ronde.models import Building -import datetime +from planning.util import get_current_time +from exceptions.exceptionHandler import ExceptionHandler + +from pickupdays.models import WeekDayEnum + + +def get_trash_template(template_id): + handler = ExceptionHandler() + handler.check_primary_key(template_id, "template_id", TrashContainerTemplate) + handler.check() + template = TrashContainerTemplate.objects.get(id=template_id) + handler.check_not_inactive(template, "template") + handler.check() + return template def make_new_tc_id_wrapper(data, extra_id): """ Maakt nieuwe TrashContainerIdWrapper aan. - TODO checks """ + handler = ExceptionHandler() + handler.check_time_value_required(data.get("collection_day").get("start_hour"), "start_hour") + handler.check_time_value_required(data.get("collection_day").get("end_hour"), "end_hour") + handler.check_enum_value_required(data.get("collection_day").get("day"), "day", WeekDayEnum) + handler.check_enum_value_required(data.get("type"), "type", TrashContainer.TrashType) + handler.check() + # maak nieuwe pickupday met de aangepaste data new_pickup_day, _ = PickUpDay.objects.get_or_create( day=data["collection_day"]["day"], @@ -47,7 +66,12 @@ def make_new_building_list(building_id, selection): def make_copy(template, permanent, current_year, current_week): """ Neemt een copy van een template zodat de geschiedenis behouden wordt + Als er een oneven template wordt aangepast in een even week zijn deze aanpassingen pas voor + de volgende week. """ + week = current_week + if (current_week % 2 == 0) != template.even: + week += 1 copy = TrashContainerTemplate.objects.create( name=template.name, @@ -55,14 +79,14 @@ def make_copy(template, permanent, current_year, current_week): status=Status.ACTIEF if permanent else Status.EENMALIG, location=template.location, year=current_year, - week=current_week + week=week ) copy.buildings.set(template.buildings.all()) copy.trash_containers.set(template.trash_containers.all()) # verander de status van de nu oude template template.status = Status.INACTIEF if permanent else Status.VERVANGEN - template.week = current_week + template.week = week template.save() return copy @@ -88,12 +112,11 @@ def add_if_match(many_to_many, new_template, current_week): many_to_many.add(new_template) -def remove_if_match(many_to_many, old_template, current_week): +def remove_if_match(many_to_many, old_template): """ Verwijder de oude template alleen maar als hij in de many_to_many zat. - Dit kan alleen wanneer even/oneven matcht. """ - if old_template.even == (current_week % 2 == 0) and many_to_many.filter(id=old_template.id).exists(): + if many_to_many.filter(id=old_template.id).exists(): many_to_many.remove(old_template) @@ -127,13 +150,16 @@ def update(template, many_to_many, old, new, permanent, template_list, copy_temp @param permanent: Of de aanpassing permanent is @param template_list: De lijst van alle templates van de huidige weekplanning """ - current_year, current_week, _ = datetime.datetime.utcnow().isocalendar() + current_year, current_week = get_current_time() if no_copy(template, permanent, current_year, current_week): update_many_to_many(template, many_to_many, old, new) - return {} + return {"message": "success"} else: copy = copy_template(template, permanent, current_year, current_week) update_many_to_many(copy, many_to_many, old, new) - remove_if_match(template_list, template, current_week) + remove_if_match(template_list, template) add_if_match(template_list, copy, current_week) - return {"new_id": copy.id} + return { + "message": "success", + "new_id": copy.id + } diff --git a/backend/trashtemplates/views.py b/backend/trashtemplates/views.py index c18ff617..8e66b112 100644 --- a/backend/trashtemplates/views.py +++ b/backend/trashtemplates/views.py @@ -1,17 +1,72 @@ -from .util import * -from planning.util import filter_templates, get_current_week_planning -from ronde.models import Building, LocatieEnum -from .serializers import * +from rest_framework import generics from rest_framework.response import Response -import datetime -from rest_framework.decorators import api_view, permission_classes -from rest_framework.permissions import AllowAny +from rest_framework.serializers import ValidationError +from planning.util import filter_templates, get_current_week_planning, get_current_time +from trashcontainers.serializers import TrashContainerSerializer +from users.permissions import * +from ronde.models import LocatieEnum, Building +from planning.models import WeekPlanning +from .util import * +from exceptions.exceptionHandler import ExceptionHandler + + +class BuildingTrashPlan(generics.ListAPIView): + permission_classes = [BewonerPermission | SyndicusPermission | StudentPermission | SuperstudentPermission | AdminPermission] + + def get(self, request, *args, **kwargs): + """ + Geeft de vuilnisplanning voor een bepaalde week + """ + year = kwargs.get("year") + week = kwargs.get("week") + templates = get_trash_templates(year, week) + active_exists = 'A' in [t.status for t in templates] + result = {} + for template in templates: + if (active_exists and template.status == 'A') or not active_exists: + buildings = template.buildings.all() + for building in buildings: + containers = template.trash_containers.filter(extra_id__in=building.trash_ids.all()) + result[building.building.id] = TrashContainerSerializer([c.trash_container for c in containers], many=True).data + break + return Response(result) + + +def get_trash_templates(year, week): + current_year, current_week = get_current_time() + + if year > current_year or (current_year == year and week > current_week): + # dit is een week die nog moet komen dus geven we alleen de actieve of nu tijdelijk vervangen templates terug + # buiten als de template vervangen is voor de volgende week + trash_templates_actief = TrashContainerTemplate.objects.filter( + status=Status.ACTIEF) + trash_templates_vervangen = TrashContainerTemplate.objects.filter( + status=Status.VERVANGEN).exclude(week=week, year=year) + trash_templates_eenmalig = TrashContainerTemplate.objects.filter( + status=Status.EENMALIG, week=week, year=year) + + even = week % 2 == 0 + trash_templates = trash_templates_actief | trash_templates_vervangen | trash_templates_eenmalig + trash_templates = trash_templates.filter(even=even) + else: + # weekplanning is al voorbij of bezig + get_current_week_planning() # nodig voor moest de weekplanning nog niet gemaakt zijn + try: + week_planning = WeekPlanning.objects.get( + week=week, + year=year + ) + trash_templates = week_planning.trash_templates.all() + except WeekPlanning.DoesNotExist: + trash_templates = [] + + return trash_templates + +class TrashTemplatesView(generics.RetrieveAPIView, generics.CreateAPIView): + permission_classes = [SuperstudentPermission | AdminPermission] -@api_view(["GET", "POST"]) -@permission_classes([AllowAny]) -def trash_templates_view(request): - if request.method == "GET": + def get(self, request, *args, **kwargs): """ Geeft alle templates die niet inactief zijn terug. """ @@ -20,13 +75,18 @@ def trash_templates_view(request): data = TrashContainerTemplateSerializer(result, many=True).data return Response(data) - if request.method == "POST": + def post(self, request, *args, **kwargs): """ Maakt een nieuwe TrashContainerTemplate aan. - TODO checks """ data = request.data - current_year, current_week, _ = datetime.datetime.utcnow().isocalendar() + handler = ExceptionHandler() + handler.check_not_blank_required(data.get("name"), "name") + handler.check_boolean_required(data.get("even"), "even") + handler.check_primary_key_value_required(data.get("location"), "location", LocatieEnum) + handler.check() + + current_year, current_week = get_current_time() location = LocatieEnum.objects.get(id=data["location"]) new_template = TrashContainerTemplate.objects.create( @@ -38,30 +98,28 @@ def trash_templates_view(request): week=current_week ) - add_if_match(get_current_week_planning().trash_templates, new_template, current_week) + add_if_match(get_current_week_planning().trash_templates, new_template, + current_week) return Response({"id": new_template.id}) -@api_view(["GET", "DELETE", "PATCH"]) -@permission_classes([AllowAny]) -def trash_template_view(request, template_id): - template = TrashContainerTemplate.objects.get(id=template_id) - current_year, current_week, _ = datetime.datetime.utcnow().isocalendar() - planning = get_current_week_planning() +class TrashTemplateView(generics.RetrieveDestroyAPIView): + permission_classes = [SuperstudentPermission | AdminPermission] - if request.method == "GET": - """ - Geeft de TrashContainerTemplate terug. - """ + def get(self, request, *args, **kwargs): + template = TrashContainerTemplate.objects.get(id=kwargs["template_id"]) return Response(TrashContainerTemplateSerializerFull(template).data) - if request.method == "DELETE": + def delete(self, request, *args, **kwargs): """ Verwijderd de TrashContainerTemplate. Als deze eenmalig was mag deze volledig uit de database verwijderd worden en moet degene die vervangen was terug actief gezet worden. """ + template = TrashContainerTemplate.objects.get(id=kwargs["template_id"]) + current_year, current_week = get_current_time() + planning = get_current_week_planning() if template.status == Status.EENMALIG: # template was eenmalig dus de originele template moet terug actief gemaakt worden original = TrashContainerTemplate.objects.get( @@ -75,88 +133,37 @@ def trash_template_view(request, template_id): add_if_match(planning.trash_templates, original, current_week) # verwijder de oude uit de huidige planning - remove_if_match(planning.trash_templates, template, current_week) + remove_if_match(planning.trash_templates, template) # verwijder hem ook uit de database omdat hij eenmalig was en dus niet nodig is voor de geschiedenis template.delete() else: template.status = Status.INACTIEF template.save() - remove_if_match(planning.trash_templates, template, current_week) + remove_if_match(planning.trash_templates, template) return Response({"message": "Success"}) - if request.method == "PATCH": - """ - Past de TrashContainerTemplate aan. - Neemt een copy van de template om de geschiedenis te behouden als dit nodig is. - """ - data = request.data - permanent = data["permanent"] - - if "name" in data: - pass - # checks - else: - data["name"] = template.name - if "even" in data: - pass - # checks - else: - data["even"] = template.even +class TrashContainersView(generics.CreateAPIView, generics.RetrieveAPIView): + permission_classes = [AdminPermission | SuperstudentPermission] - if "location" in data: - data["location"] = LocatieEnum.objects.get(id=data["location"]) - # checks - else: - data["location"] = template.location - - if no_copy(template, permanent, current_year, current_week): - template.name = data["name"] - template.even = data["even"] - template.location = data["location"] - template.save() - add_if_match(planning.trash_templates, template, current_week) - return Response({"message": "Success"}) - - new_template = TrashContainerTemplate.objects.create( - name=data["name"], - even=data["even"], - status=Status.ACTIEF, - location=data["location"], - year=current_year, - week=current_week - ) - add_if_match(planning.trash_templates, new_template, current_week) - - # oude template op inactief zetten - template.status = Status.INACTIEF - template.save() - remove_if_match(planning.trash_templates, template, current_week) - - return Response({"message": "Success"}) - - -@api_view(["POST", "GET"]) -@permission_classes([AllowAny]) -def trash_containers_view(request, template_id, permanent): - template = TrashContainerTemplate.objects.get(id=template_id) - - if request.method == "GET": + def get(self, request, *args, **kwargs): """ Geeft alle trash containers de template terug. """ - data = TrashContainerIdWrapperSerializer(template.trash_containers.all(), many=True).data + template = TrashContainerTemplate.objects.get(id=kwargs["template_id"]) + data = TrashContainerIdWrapperSerializer( + template.trash_containers.all(), many=True).data return Response(data) - if request.method == "POST": + def post(self, request, *args, **kwargs): """ Voegt de nieuwe TrashContainer toe aan de template adhv een TrashContainerIdWrapper. """ + template = TrashContainerTemplate.objects.get(id=kwargs["template_id"]) + permanent = kwargs["permanent"] data = request.data - current_year, current_week, _ = datetime.datetime.utcnow().isocalendar() - extra_id = ExtraId.objects.create() new_tc_id_wrapper = make_new_tc_id_wrapper(data, extra_id) @@ -172,51 +179,44 @@ def trash_containers_view(request, template_id, permanent): return Response({"message": "Success"}) -@api_view(["GET", "DELETE", "PATCH"]) -@permission_classes([AllowAny]) -def trash_container_view(request, template_id, extra_id, permanent): - - template = TrashContainerTemplate.objects.get(id=template_id) - tc_id_wrapper = template.trash_containers.get(extra_id=extra_id) +class TrashContainerView(generics.RetrieveUpdateDestroyAPIView): + permission_classes = [AdminPermission | SuperstudentPermission] - if request.method == "GET": + def get(self, request, *args, **kwargs): """ Geeft een TrashContainer terug. """ + template = TrashContainerTemplate.objects.get(id=kwargs["template_id"]) + tc_id_wrapper = template.trash_containers.get(extra_id=kwargs[ + "extra_id"]) data = TrashContainerIdWrapperSerializer(tc_id_wrapper).data return Response(data) - if request.method == "DELETE": - """ - Verwijderd de TrashContainer van de template. - Neemt een copy van de template om de geschiedenis te behouden als dit nodig is. - """ + def put(self, request, *args, **kwargs): + raise ValidationError("no PUT allowed") - update( - template, - "trash_containers", - tc_id_wrapper, - None, - permanent, - get_current_week_planning().trash_templates - ) - return Response({"message": "Success"}) - - if request.method == "PATCH": + def patch(self, request, *args, **kwargs): """ Past een TrashContainer aan. Neemt een copy van de template om de geschiedenis te behouden als dit nodig is. """ + template = TrashContainerTemplate.objects.get(id=kwargs["template_id"]) + tc_id_wrapper = template.trash_containers.get(extra_id=kwargs[ + "extra_id"]) + permanent = kwargs["permanent"] data = request.data - if "day" not in data: - data["day"] = tc_id_wrapper.trash_container.collection_day.day + if "collection_day" not in data: + data["collection_day"] = {} + + if "day" not in data.get("collection_day"): + data["collection_day"]["day"] = tc_id_wrapper.trash_container.collection_day.day - if "start_hour" not in data: - data["start_hour"] = tc_id_wrapper.trash_container.collection_day.start_hour + if "start_hour" not in data.get("collection_day"): + data["collection_day"]["start_hour"] = tc_id_wrapper.trash_container.collection_day.start_hour - if "end_hour" not in data: - data["end_hour"] = tc_id_wrapper.trash_container.collection_day.end_hour + if "end_hour" not in data.get("collection_day"): + data["collection_day"]["end_hour"] = tc_id_wrapper.trash_container.collection_day.end_hour if "type" not in data: data["type"] = tc_id_wrapper.trash_container.type @@ -234,25 +234,47 @@ def trash_container_view(request, template_id, extra_id, permanent): return Response({"message": "Success"}) + def delete(self, request, *args, **kwargs): + """ + Verwijderd de TrashContainer van de template. + Neemt een copy van de template om de geschiedenis te behouden als dit nodig is. + """ + template = TrashContainerTemplate.objects.get(id=kwargs["template_id"]) + tc_id_wrapper = template.trash_containers.get(extra_id=kwargs[ + "extra_id"]) + permanent = kwargs["permanent"] + update( + template, + "trash_containers", + tc_id_wrapper, + None, + permanent, + get_current_week_planning().trash_templates + ) + return Response({"message": "Success"}) -@api_view(["POST", "GET"]) -@permission_classes([AllowAny]) -def buildings_view(request, template_id, permanent): - data = request.data - template = TrashContainerTemplate.objects.get(id=template_id) +class BuildingsView(generics.CreateAPIView, generics.RetrieveAPIView): + permission_classes = [AdminPermission | SuperstudentPermission] - if request.method == "GET": + def get(self, request, *args, **kwargs): """ Geeft alle gebouwen van deze template terug samen met hun selecties. """ - data = BuildingTrashContainerListSerializer(template.buildings.all(), many=True).data + template = TrashContainerTemplate.objects.get(id=kwargs[ + "template_id"]) + data = BuildingTrashContainerListSerializer(template.buildings.all(), + many=True).data return Response(data) - if request.method == "POST": + def post(self, request, *args, **kwargs): """ Voegt een nieuw gebouw samen met zijn selectie toe aan de template. """ + data = request.data + template = TrashContainerTemplate.objects.get(id=kwargs[ + "template_id"]) + permanent = kwargs["permanent"] # checks building = Building.objects.get(id=data["building"]) new_building_list = BuildingTrashContainerList.objects.create( @@ -266,56 +288,69 @@ def buildings_view(request, template_id, permanent): None, new_building_list, permanent, - get_current_week_planning().student_templates + get_current_week_planning().trash_templates ) return Response({"message": "Success"}) -@api_view(["GET", "DELETE", "PATCH"]) -@permission_classes([AllowAny]) -def building_view(request, template_id, building_id, permanent): - - template = TrashContainerTemplate.objects.get(id=template_id) - building_list = template.buildings.get(building=building_id) +class BuildingView(generics.RetrieveUpdateDestroyAPIView): + permission_classes = [AdminPermission | SuperstudentPermission] - if request.method == "GET": + def get(self, request, *args, **kwargs): """ Geeft het gebouw met zijn selectie terug. """ + template = TrashContainerTemplate.objects.get(id=kwargs["template_id"]) + building_list = template.buildings.get(building=kwargs["building_id"]) + new_list = [] + for trash_id in building_list.trash_ids.all(): + if template.trash_containers.filter(extra_id=trash_id).exists(): + new_list.append(trash_id) + building_list.trash_ids.set(new_list) + data = BuildingTrashContainerListSerializer(building_list).data return Response(data) - if request.method == "DELETE": + def delete(self, request, *args, **kwargs): """ Verwijderd het gebouw en zijn selectie van de template. Neemt een copy van de template om de geschiedenis te behouden als dit nodig is. """ + template = TrashContainerTemplate.objects.get(id=kwargs["template_id"]) + building_list = template.buildings.get(building=kwargs["building_id"]) + permanent = kwargs["permanent"] update( template, "buildings", building_list, None, permanent, - get_current_week_planning().student_templates + get_current_week_planning().trash_templates ) return Response({"message": "Success"}) - if request.method == "PATCH": + def patch(self, request, *args, **kwargs): """ + Past de selectie van een gebouw aan. Neemt een copy van de template om de geschiedenis te behouden als dit nodig is. """ data = request.data - - new_building_list = make_new_building_list(building_id, data["selection"]) - + template = TrashContainerTemplate.objects.get(id=kwargs["template_id"]) + building_list = template.buildings.get(building=kwargs["building_id"]) + permanent = kwargs["permanent"] + new_building_list = make_new_building_list(kwargs["building_id"], + data["selection"]) update( template, "buildings", building_list, new_building_list, permanent, - get_current_week_planning().student_templates + get_current_week_planning().trash_templates ) return Response({"message": "Success"}) + + def put(self, request, *args, **kwargs): + raise ValidationError("no PUT allowed") diff --git a/backend/users/models.py b/backend/users/models.py index 8ecfe944..a9dc7122 100644 --- a/backend/users/models.py +++ b/backend/users/models.py @@ -1,22 +1,11 @@ from django.db import models from django.contrib.auth.models import AbstractUser, BaseUserManager -from rest_framework import serializers +from ronde.models import LocatieEnum import random import string -class Registration(models.Model): - """ - Model that is used to serialize a registration. - """ - email = models.EmailField(unique=True) - first_name = models.TextField(default="") - last_name = models.TextField(default="") - phone_nr = models.TextField(default="") - password = models.CharField(max_length=30, default=None) - - class Roles(models.TextChoices): """ All the roles users can have. @@ -44,15 +33,6 @@ class RoleAssignment(models.Model): class CustomUserManager(BaseUserManager): def create_user(self, email, first_name, last_name, phone_nr, password): - if not email: - raise serializers.ValidationError( - { - "errors": [ - { - "message": "email is required", "field": "email" - } - ] - }, code='invalid') user = self.model( email=self.normalize_email(email), @@ -65,6 +45,12 @@ def create_user(self, email, first_name, last_name, phone_nr, password): user.save() return user + def create_superuser(self, **args): + user = self.create_user(**args, first_name='admin', last_name='admin', phone_nr='') + user.role = 'AD' + user.save() + return user + class User(AbstractUser): """ @@ -111,6 +97,7 @@ class User(AbstractUser): max_length=25, default="" ) + locations = models.ManyToManyField(LocatieEnum) objects = CustomUserManager() diff --git a/backend/users/permissions.py b/backend/users/permissions.py index 224e34c7..00e64ae3 100644 --- a/backend/users/permissions.py +++ b/backend/users/permissions.py @@ -2,6 +2,11 @@ from planning.models import InfoPerBuilding, DagPlanning +class AllowAnyReadOnly(permissions.BasePermission): + def has_permission(self, request, view): + return request.method in permissions.SAFE_METHODS + + class ReadOnly(permissions.BasePermission): def has_permission(self, request, view): return request.method in permissions.SAFE_METHODS and request.user and not request.user.is_anonymous @@ -63,10 +68,7 @@ def has_permission(self, request, view): class BewonerPermission(permissions.BasePermission): def has_permission(self, request, view): - user = request.user - if not user or user.is_anonymous: - return False - return user.role == 'BE' + return request.method in permissions.SAFE_METHODS class AanvragerPermission(permissions.BasePermission): diff --git a/backend/users/serializers.py b/backend/users/serializers.py index 35e6bd17..a778c000 100644 --- a/backend/users/serializers.py +++ b/backend/users/serializers.py @@ -1,13 +1,7 @@ from django.contrib.auth import get_user_model from rest_framework import serializers -from .models import Registration, RoleAssignment - - -class RegistrationSerializer(serializers.ModelSerializer): - class Meta: - model = Registration - fields = '__all__' +from .models import RoleAssignment class RoleAssignmentSerializer(serializers.ModelSerializer): @@ -33,7 +27,8 @@ class Meta: 'first_name', 'last_name', 'phone_nr', - 'role' + 'role', + 'locations' ] @@ -47,5 +42,6 @@ class Meta: 'date_joined', 'phone_nr', 'id', - 'role' + 'role', + 'locations' ] diff --git a/backend/users/tests.py b/backend/users/tests.py index b7478828..53b4d300 100644 --- a/backend/users/tests.py +++ b/backend/users/tests.py @@ -1,9 +1,12 @@ +import json + from django.contrib.sessions.middleware import SessionMiddleware from rest_framework.exceptions import ErrorDetail from rest_framework.test import APITestCase, APIRequestFactory, force_authenticate from .views import registration_view, logout_view, UserListAPIView, forgot_password, reset_password, \ role_assignment_view, login_view from .models import User +from ronde.models import LocatieEnum class UserTestCase(APITestCase): @@ -12,16 +15,22 @@ class UserTestCase(APITestCase): """ def setUp(self): + self.loc1 = LocatieEnum.objects.create(name="Gent") + self.loc2 = LocatieEnum.objects.create(name="Antwerpen") + self.register = {"email": "test@test.com", "first_name": "First", - "last_name": "Last", "password": "Pass", "phone_nr": "0"} + "last_name": "Last", "password": "Pass", "password2": "Pass", "phone_nr": "0", + "locations": [self.loc1.pk, self.loc2.pk]} + self.login = {"email": "test@test.com", "password": "Pass"} self.user = User.objects.create(username="user", email="user@mail.com") self.su = User.objects.create(role="SU", username="su") def testUserRegistration(self): factory = APIRequestFactory() - request = factory.post("/api/register/", self.register) + request = factory.post("/api/register/", json.dumps(self.register), content_type='application/json') response = registration_view(request).data + self.assertEqual(response["email"], "test@test.com") self.assertIn("role", response) @@ -38,7 +47,8 @@ def testUserRegistrationMissingEmail(self): def testUserLogin(self): factory = APIRequestFactory() - request = factory.post("/api/register/", self.register) + request = factory.post("/api/register/", json.dumps(self.register), content_type='application/json') + registration_view(request) # Login to the newly made account @@ -49,7 +59,7 @@ def testUserLogin(self): def testUserForgotPassword(self): factory = APIRequestFactory() - request = factory.post("/api/register/", self.register) + request = factory.post("/api/register/", json.dumps(self.register), content_type='application/json') registration_view(request) # Test if email is sent for a valid email address @@ -60,11 +70,11 @@ def testUserForgotPassword(self): # Test if no email is sent if it does not exist in database request = factory.post("/api/forgot/", {"email": "test2@test.com"}) response = forgot_password(request).data - self.assertEqual(response["message"], "Dit email adres bestaat niet.") + self.assertIn("errors", response) def testUserResetPassword(self): factory = APIRequestFactory() - request = factory.post("/api/register/", self.register) + request = factory.post("/api/register/", json.dumps(self.register), content_type='application/json') registration_view(request) # Make sure an error message is given when a non-existent email is entered @@ -79,13 +89,13 @@ def testUserResetPassword(self): # Make sure new password isn't empty otp = User.objects.get(email="test@test.com").otp - request = factory.post("/api/reset/", {"email": "test@test.com", "otp": otp, "new_password": ""}) + request = factory.post("/api/reset/", {"email": "test@test.com", "otp": otp, "password": ""}) response = reset_password(request).data self.assertIn("errors", response) # Test if we can reset password otp = User.objects.get(email="test@test.com").otp - request = factory.post("/api/reset/", {"email": "test@test.com", "otp": otp, "new_password": "Pass"}) + request = factory.post("/api/reset/", {"email": "test@test.com", "otp": otp, "password": "Pass", "password2": "Pass"}) response = reset_password(request).data self.assertEqual(response["message"], "New password is created") @@ -109,35 +119,35 @@ def testListUserPermissions(self): def testUserRoleAssignment(self): factory = APIRequestFactory() - request = factory.post("/api/register/", self.register) + request = factory.post("/api/register/", json.dumps(self.register), content_type='application/json') registration_view(request) # Test permission for role assignment - request = factory.post("/api/role/", {"role": "AD", "email": ""}) + request = factory.patch("/api/role/", {"role": "AD", "email": ""}) force_authenticate(request, user=self.user) response = role_assignment_view(request).data self.assertEqual(response["detail"].code, "permission_denied") # Test if error is returned when email is empty - request = factory.post("/api/role/", {"role": "AD", "email": ""}) + request = factory.patch("/api/role/", {"role": "AD", "email": ""}) force_authenticate(request, user=self.su) response = role_assignment_view(request).data self.assertEqual(response["email"][0], "This field may not be blank.") # Make sure a superstudent can't promote a user to admin - request = factory.post("/api/role/", {"role": "AD", "email": "test@test.com"}) + request = factory.patch("/api/role/", {"role": "AD", "email": "test@test.com"}) force_authenticate(request, user=self.su) response = role_assignment_view(request).data self.assertIn("errors", response) # Make sure a non-existent user cannot be promoted - request = factory.post("/api/role/", {"role": "ST", "email": "test2@test.com"}) + request = factory.patch("/api/role/", {"role": "ST", "email": "test2@test.com"}) force_authenticate(request, user=self.su) response = role_assignment_view(request).data self.assertIn("errors", response) # Make sure a non-existent user cannot be promoted - request = factory.post("/api/role/", {"role": "ST", "email": "test@test.com"}) + request = factory.patch("/api/role/", {"role": "ST", "email": "test@test.com"}) force_authenticate(request, user=self.su) response = role_assignment_view(request).data self.assertEqual(response["message"], "test@test.com is nu een Student") diff --git a/backend/users/urls.py b/backend/users/urls.py index da65f840..ee9d655f 100644 --- a/backend/users/urls.py +++ b/backend/users/urls.py @@ -5,13 +5,12 @@ TokenRefreshView ) urlpatterns = [ - # path('user/', user_view, name='user'), + path('user/', user_view, name='user'), path('register/', registration_view, name='register'), path('login/', login_view, name='login'), path('refresh/', TokenRefreshView.as_view(), name='refresh'), path('role/', role_assignment_view, name='role'), path('users/', UserListAPIView.as_view(), name='users'), - path('user/', UserRetrieveUpdateView.as_view(), name='user'), path('user//', UserByIdRUDView.as_view(), name='user_id'), path('forgot/', forgot_password, name='forgot'), path('reset/', reset_password, name='reset'), diff --git a/backend/users/views.py b/backend/users/views.py index 63157c7a..5088d283 100644 --- a/backend/users/views.py +++ b/backend/users/views.py @@ -1,6 +1,5 @@ from django.conf import settings from django.contrib.auth import authenticate, login, get_user_model, logout -from django.core.exceptions import ObjectDoesNotExist from django.core.mail import send_mail from django.middleware import csrf from rest_framework import serializers, generics @@ -9,12 +8,14 @@ from rest_framework.permissions import AllowAny from rest_framework.response import Response from rest_framework_simplejwt.tokens import RefreshToken - +from .models import User from exceptions.exceptionHandler import ExceptionHandler -from .permissions import AdminPermission, SuperstudentPermission, ReadOnly, \ - StudentPermission -from .serializers import RegistrationSerializer, RoleAssignmentSerializer, \ +from .permissions import AdminPermission, SuperstudentPermission, ReadOnly, StudentPermission, \ + SyndicusPermission +from .serializers import RoleAssignmentSerializer, \ UserPublicSerializer, UserSerializer +from ronde.models import LocatieEnum +from django.core.validators import validate_email class UserListAPIView(generics.ListAPIView): @@ -31,16 +32,27 @@ def get_tokens_for_user(user): } -@api_view(['GET']) -@permission_classes([ReadOnly]) +@api_view(['GET', 'PATCH']) +@permission_classes([SyndicusPermission | StudentPermission | SuperstudentPermission | AdminPermission | ReadOnly]) def user_view(request): response = Response() - if request.user.is_authenticated: - response.data = UserSerializer(request.user).data - return response - else: - response.data = "{'error': 'no user'}" - return response + if request.method == 'GET': + if request.user.is_authenticated: + response.data = UserSerializer(request.user).data + else: + response.data = "{'error': 'no user'}" + elif request.method == 'PATCH': + if request.user.is_authenticated: + data = request.data + serializer = UserSerializer(request.user, data=data, partial=True) + if serializer.is_valid(): + serializer.save() + response.data = serializer.data + else: + response.data = serializer.errors + else: + response.data = "{'error': 'no user'}" + return response @api_view(['POST']) @@ -48,9 +60,18 @@ def user_view(request): def login_view(request): data = request.data response = Response() - username = data.get('email', None) + email = data.get('email', None) + if email is not None: + email = email.lower() password = data.get('password', None) - user = authenticate(username=username, password=password) + + handler = ExceptionHandler() + handler.check_not_blank_required(email, "email") + handler.check_not_blank_required(password, "password") + handler.check_email(email, User) + handler.check() + + user = authenticate(username=email, password=password) if user is not None: if user.is_active: login(request, user) @@ -80,8 +101,8 @@ def login_view(request): return Response({"No active": "This account is not active!!"}, status=status.HTTP_404_NOT_FOUND) else: - return Response({"Invalid": "Invalid username or password!!"}, - status=status.HTTP_404_NOT_FOUND) + return Response({"errors": [{"message": "Verkeerd wachtwoord.", "field": "password"}]}, + status=status.HTTP_400_BAD_REQUEST) @api_view(['POST']) @@ -100,54 +121,65 @@ def registration_view(request): if request.method == "POST": response = Response() data = request.data - serializer = RegistrationSerializer(data=data) - handler = ExceptionHandler() handler.check_not_blank_required(data.get("email"), "email") - handler.check_not_blank_required(data.get("first_name"), "first_name") - handler.check_not_blank_required(data.get("last_name"), "last_name") + handler.check_not_blank_required(data.get("first_name"), "firstname") + handler.check_not_blank_required(data.get("last_name"), "lastname") handler.check_not_blank_required(data.get("password"), "password") + handler.check_not_blank_required(data.get("password2"), "password2") handler.check_integer_required(data.get("phone_nr"), "phone_nr") + handler.check_equal(data.get("password"), data.get("password2"), "password2") + handler.check_required(data.get("locations"), "locations") handler.check() - if serializer.is_valid(raise_exception=True): - if get_user_model().objects.filter( - email=data["email"]).exists(): - raise serializers.ValidationError({ - "errors": [ - { - "message": "email address already in use" - } - ] - }) - user = get_user_model().objects.create_user( - request.data['email'], - request.data['first_name'], - request.data['last_name'], - request.data['phone_nr'], - request.data['password'] - ) - refresh = RefreshToken.for_user(user) - response.set_cookie( - key=settings.SIMPLE_JWT['AUTH_COOKIE'], - value=str(refresh.access_token), - expires=settings.SIMPLE_JWT['ACCESS_TOKEN_LIFETIME'], - max_age=settings.SIMPLE_JWT['ACCESS_TOKEN_LIFETIME'], - secure=settings.SIMPLE_JWT['AUTH_COOKIE_SECURE'], - httponly=settings.SIMPLE_JWT['AUTH_COOKIE_HTTP_ONLY'], - samesite=settings.SIMPLE_JWT['AUTH_COOKIE_SAMESITE'] - ) - response.set_cookie( - key=settings.SIMPLE_JWT['REFRESH_COOKIE'], - value=str(refresh), - expires=settings.SIMPLE_JWT['REFRESH_TOKEN_LIFETIME'], - max_age=settings.SIMPLE_JWT['REFRESH_TOKEN_LIFETIME'], - secure=settings.SIMPLE_JWT['AUTH_COOKIE_SECURE'], - httponly=settings.SIMPLE_JWT['AUTH_COOKIE_HTTP_ONLY'], - samesite=settings.SIMPLE_JWT['AUTH_COOKIE_SAMESITE'] - ) - response.data = UserSerializer(user).data - else: - response.data = serializer.errors + + try: + validate_email(data.get("email")) + except Exception: + raise serializers.ValidationError({ + "errors": [{ + "message": "Dit email adres is ongeldig.", + "field": "email" + }]}) + + if get_user_model().objects.filter(email=data["email"].lower()).exists(): + raise serializers.ValidationError({ + "errors": [{ + "message": "Dit email adres is al in gebruik.", + "field": "email" + }]}) + + user = get_user_model().objects.create_user( + data['email'].lower(), + data['first_name'], + data['last_name'], + data['phone_nr'], + data['password'] + ) + + locations = [LocatieEnum.objects.get(pk=pk) for pk in data.get("locations")] + user.locations.set(locations) + + refresh = RefreshToken.for_user(user) + response.set_cookie( + key=settings.SIMPLE_JWT['AUTH_COOKIE'], + value=str(refresh.access_token), + expires=settings.SIMPLE_JWT['ACCESS_TOKEN_LIFETIME'], + max_age=settings.SIMPLE_JWT['ACCESS_TOKEN_LIFETIME'], + secure=settings.SIMPLE_JWT['AUTH_COOKIE_SECURE'], + httponly=settings.SIMPLE_JWT['AUTH_COOKIE_HTTP_ONLY'], + samesite=settings.SIMPLE_JWT['AUTH_COOKIE_SAMESITE'] + ) + response.set_cookie( + key=settings.SIMPLE_JWT['REFRESH_COOKIE'], + value=str(refresh), + expires=settings.SIMPLE_JWT['REFRESH_TOKEN_LIFETIME'], + max_age=settings.SIMPLE_JWT['REFRESH_TOKEN_LIFETIME'], + secure=settings.SIMPLE_JWT['AUTH_COOKIE_SECURE'], + httponly=settings.SIMPLE_JWT['AUTH_COOKIE_HTTP_ONLY'], + samesite=settings.SIMPLE_JWT['AUTH_COOKIE_SAMESITE'] + ) + response.data = UserSerializer(user).data + return response @@ -157,29 +189,26 @@ def forgot_password(request): """ Send an email with an otp when forgot password is used. """ - if request.data.get("email") is None: - raise serializers.ValidationError({ - "errors": [ - { - "message": "email address already in use", - "field": "email" - } - ] - }) - email = request.data['email'] - if get_user_model().objects.filter(email=email).exists(): - user = get_user_model().objects.get(email=email) - send_mail( - subject='Nieuw wachtwoord voor Dr Trottoir.', - message=f'{user.otp}', # TODO email text schrijven - from_email=settings.DEFAULT_FROM_EMAIL, - recipient_list=[user.email], - auth_user=settings.DEFAULT_FROM_EMAIL, - auth_password=settings.EMAIL_HOST_PASSWORD - ) - return Response({'message': 'Email is verstuurd'}) - else: - return Response({'message': 'Dit email adres bestaat niet.'}) + + data = request.data + email = data["email"] + + handler = ExceptionHandler() + handler.check_not_blank_required(email, "email") + email = email.lower() + handler.check_email(email, User) + handler.check() + + user = get_user_model().objects.get(email=email) + send_mail( + subject='Nieuw wachtwoord voor Dr Trottoir.', + message=f'{user.otp}', # TODO email text schrijven + from_email=settings.DEFAULT_FROM_EMAIL, + recipient_list=[user.email], + auth_user=settings.DEFAULT_FROM_EMAIL, + auth_password=settings.EMAIL_HOST_PASSWORD + ) + return Response({'message': 'Email is verstuurd'}) @api_view(['POST']) @@ -189,43 +218,38 @@ def reset_password(request): Reset the password with the otp that is received via email. """ data = request.data - try: - user = get_user_model().objects.get(email=data['email']) - except Exception: - raise serializers.ValidationError( - {"errors": [ - { - "message": "There is no user with this email", - "field": "email" - } - ] - }, code='invalid') - else: - if data['otp'] == user.otp: - if data['new_password'] != '': - user.set_password(data['new_password']) - user.save() # Will automatically create new otp - return Response({'message': 'New password is created'}) - else: - raise serializers.ValidationError( - { - "errors": [{"message": "Password can't be empty", - "field": "new_password"}] - }, code='invalid') - else: - raise serializers.ValidationError( - { - "errors": [{"message": "OTP didn't match", "field": "otp"}] - }, code='invalid') + email = data.get("email") + otp = data.get("otp") + password = data.get("password") + password2 = data.get("password2") + handler = ExceptionHandler() + handler.check_not_blank_required(email, "email") + handler.check_not_blank_required(otp, "otp") + handler.check_not_blank_required(password, "password") + handler.check_not_blank_required(password2, "password2") + email = email.lower() + handler.check_email(email, User) + handler.check() -@api_view(['POST', 'GET']) + user = get_user_model().objects.get(email=data['email'].lower()) + + handler.check_equal(password, password2, "password2") + handler.check_equal(otp, user.otp, "otp") + handler.check() + + user.set_password(password) + user.save() # Will automatically create new otp + return Response({'message': 'New password is created'}) + + +@api_view(['PATCH', 'GET']) @permission_classes([AdminPermission | SuperstudentPermission | ReadOnly]) def role_assignment_view(request): if request.method == "GET": # return role of user return Response({'role': request.user.role}) - if request.method == "POST": # change the role of a user + if request.method == "PATCH": # change the role of a user serializer = RoleAssignmentSerializer(data=request.data) if serializer.is_valid(raise_exception=True): @@ -242,7 +266,7 @@ def role_assignment_view(request): try: user = get_user_model().objects.get( - email=request.data['email']) + email=request.data['email'].lower()) except get_user_model().DoesNotExist: user = None @@ -269,74 +293,27 @@ class UserByIdRUDView(generics.RetrieveUpdateDestroyAPIView): serializer_class = UserSerializer permission_classes = [AdminPermission | SuperstudentPermission] - def get(self, request, *args, **kwargs): - id = self.kwargs['pk'] - try: - user = get_user_model().objects.get(id=id) - return Response(UserSerializer(user).data) - except ObjectDoesNotExist: - raise serializers.ValidationError( - { - "errors": [ - { - "message": "referenced user not in db", "field": "token" - } - ] - }, code='invalid') - - def partial_update(self, request, *args, **kwargs): - id = self.kwargs['pk'] - try: - user = get_user_model().objects.get(id=id) - serializer = UserSerializer(user, data=request.data, partial=True) - if serializer.is_valid(raise_exception=True): - serializer.save() - return Response({"succes": ["Updated user"]}) - except ObjectDoesNotExist: - raise serializers.ValidationError( - { - "errors": [ - { - "message": "referenced user not in db", "field": "token" - } - ] - }, code='invalid') - - -class UserRetrieveUpdateView(generics.RetrieveUpdateAPIView): - serializer_class = UserSerializer - permission_classes = [ - ReadOnly | StudentPermission | AdminPermission | SuperstudentPermission] + def patch(self, request, *args, **kwargs): + data = request.data - def get(self, request, *args, **kwargs): - try: - user = get_user_model().objects.get(username=request.user) - return Response(UserSerializer(user).data) - except ObjectDoesNotExist: - raise serializers.ValidationError( - { - "errors": [ - { - "message": "referenced user not in db", - "field": "token" - } - ] - }, code='invalid') - - def partial_update(self, request, *args, **kwargs): - try: - user = get_user_model().objects.get(username=request.user) - serializer = UserSerializer(user, data=request.data, partial=True) - if serializer.is_valid(raise_exception=True): - serializer.save() - return Response({"succes": ["Updated user"]}) - except ObjectDoesNotExist: - raise serializers.ValidationError( - { - "errors": [ - { - "message": "referenced user not in db", - "field": "token" - } - ] - }, code='invalid') + id = kwargs['pk'] + handler = ExceptionHandler() + handler.check_primary_key(id, 'id', User) + handler.check_not_blank(data.get("email"), "email") + handler.check_not_blank(data.get("first_name"), "first_name") + handler.check_not_blank(data.get("last_name"), "last_name") + handler.check_not_blank(data.get("password"), "password") + handler.check_integer(data.get("phone_nr"), "phone_nr") + handler.check() + + data["email"] = data.get("email").lower() + user = get_user_model().objects.get(id=id) + + if user.email.lower() != data.get("email") and get_user_model().objects.filter(email=data["email"]).exists(): + raise serializers.ValidationError({ + "errors": [{ + "message": "Dit email adres is al in gebruik.", + "field": "email" + }] + }) + return super().patch(request, *args, **kwargs) diff --git a/docs/.gitmodules b/docs/.gitmodules index a1524f2c..3b5b1a7b 100644 --- a/docs/.gitmodules +++ b/docs/.gitmodules @@ -1,4 +1,3 @@ - -[submodule "themes/docsy"] - path = themes/docsy - url = https://github.com/google/docsy +[submodule "themes/docsy"] + path = themes/docsy + url = https://github.com/google/docsy diff --git a/docs/content/nl/docs/API/planning.md b/docs/content/nl/docs/API/planning.md index d23ccb8c..f4501268 100644 --- a/docs/content/nl/docs/API/planning.md +++ b/docs/content/nl/docs/API/planning.md @@ -7,22 +7,28 @@ date: 2023-03-14 description: > Deze pagina geeft meer info over de verschillende **planning** endpoints --- -## De /planning/ urls - -Hier kan u uitleg vinden over alle endpoints die met de planning te maken hebben - -### De /planning/ endpoints - -### De /planning/dagplanning/<**int**> endpoints - -### De /planning/buildingpicture/ endpoints - -### De /planning/buildingpicture/<**int**> endpoints - -### De /planning/infoperbuilding/ endpoints - -### De /planning/infoperbuilding/<**int**> endpoints - -### De /planning/weekplanning/ endpoints - -### De /planning/weekplanning/<**int**> endpoints +## Planning endpoints + +Hier kan u uitleg vinden over alle endpoints die met de planning te maken hebben. + +### buildingpicture/ + +Geeft via een GET request alle buildingpicture objecten terug. Deze horen bij een infoperbuilding en bevatten een link naar een foto. + +### buildingpicture/\/ +### infoperbuilding/ +### infoperbuilding/\/ +### weekplanning/\/\/ +### dagplanning/\/\/\ +### dagplanning/\/ +### dagplanning/\/\/\/status/ +### dagplanning/\/\/\/pictures/ +### studenttemplates/find/planning/\/ +### studenttemplates/rondes/\/\/\/\/ +### studenttemplates/ +### studenttemplates/\ +### studenttemplates/\/rondes/ +### studenttemplates/\/rondes/\ +### studenttemplates/\/rondes/\/dagplanningen/ +### studenttemplates/\/dagplanningen/\/ +### studenttemplates/\/dagplanningen/\/eenmalig/ diff --git a/docs/content/nl/docs/Hoe-te-gebruiken/_index.md b/docs/content/nl/docs/Hoe-te-gebruiken/_index.md index 0e52bd75..4a8d1c4e 100644 --- a/docs/content/nl/docs/Hoe-te-gebruiken/_index.md +++ b/docs/content/nl/docs/Hoe-te-gebruiken/_index.md @@ -1,13 +1,15 @@ --- categories: ["App", "How to"] tags: ["UI", "UX", "docs"] -title: "Hoe te gebruiken" -linkTitle: "Hoe te gebruiken" +title: "Hoe applicatie gebruiken" +linkTitle: "Hoe applicatie gebruiken" weight: 2 description: > Uitleg over hoe de applicatie gebruikt moet worden vanuit het standpunt van elke user. --- {{% pageinfo %}} -Aangezien er momenteel nog geen frontend UI/UX is is deze documentatie nog leeg. +Dit deel van de documentatie is opgedeeld vanuit het standpunt van elke soort user. {{% /pageinfo %}} + + diff --git a/docs/content/nl/docs/Hoe-te-gebruiken/admin.md b/docs/content/nl/docs/Hoe-te-gebruiken/admin.md new file mode 100644 index 00000000..b8a2e77f --- /dev/null +++ b/docs/content/nl/docs/Hoe-te-gebruiken/admin.md @@ -0,0 +1,155 @@ +--- +categories: ["how to", "admin"] +tags: ["UI", "UX", "docs", "how to", "admin"] +title: "admin" +linkTitle: "admin" +date: 2023-21-05 +description: > + Hoe gebruik je de applicatie als een admin +--- + +# Admin + +## Inhoudstafel + +- [Dashboard](#dashboard) + - [Ronde info](#ronde-info) +- [Studenten Templates](#studenten-templates) + - [Aanmaken](#aanmaken) + - [Aanpassen](#aanpassen) + - [Rondes](#rondes) + - [Dagplanning](#dagplanning) +- [Afval Templates](#afval-templates) + - [Aanmaken](#aanmaken-1) + - [Aanpassen](#aanpassen-1) + - [Vuilnisbakken](#vuilnisbakken) + - [Gebouwen](#gebouwen) +- [Locaties](#locaties) +- [Rondes](#rondes) +- [Gebouwen](#gebouwen-1) +- [Studenten](#studenten) +- [Syndici](#syndici) +- [Email Templates](#email-templates) + +## Dashboard + +Op de front pagina voor admins bevat alle plannings die momenteel actief zijn en al dan niet ingepland op een bepaalde dag. + +### Ronde info +Na het klikken op het info icoon naast een ronde krijgt de admin een overzicht te zien over die bepaalde ronde. +- Of die al dan niet al klaar is. +- Welke studenten er mee bezig zijn. +- De opmerkingen indien er zijn. +- Uitgebreide info over de gebouwen. + +De specifieke ronde kan op dit scherm ook aangepast worden. De velden die op dit punt nog aan te passen zijn, zijn: +- De studenten voor de ronde +- Het start en eind uur van de ronde + +## Studenten Templates + +Studenten templates zijn vaste sets van rondes op 1 locatie waar de studenten altijd gelijk blijven. Deze templates zijn net zoals de [Afval Templates](#afval-templates) voor even en/of oneven weken. + +### Aanmaken +Bij het aanmaken van een Studenten Template wordt een naam verwacht, de locatie voor deze template en de start en eind uren die meestal gebruikt gaan worden. + +### Aanpassen +Na het aanmaken van de template kan die aangepast worden. Enkel onderstaande velden kunnen aangepast worden. +- Naam +- Startuur +- Einduur + +Bij het aanpassen kunnen rondes toegevoegd worden aan de template. + +#### Rondes + +Elke ronde die gemaakt is in [Rondes](#rondes-1) kan meerdere keren toegevoegd worden. + +Elke ronde heeft voor elke dag van de week, startend op zondag, een lege dagplanning. + +#### Dagplanning +Bij elke dag in de dagplanning kunnen er 1 of meerdere studenten aangewezen worden om op die dag van de week die ronde te doen. +Een dag kan ook een 2de keer toegevoegd worden indien dit gewenst is en kan zelfs dezelfde uren krijgen als 1 die al bestaat. + +Elke dag heeft dezelfde start en eind uren als de template maar die kunnen aangepast worden. + +## Afval Templates + +Deze templates volgen hetzelfde principe als de [Studenten Templates](#studenten-templates). + +### Aanmaken +Bij het aanmaken van de templates hebben ze elk een locatie, naam en even/oneven weken. + +### Aanpassen +Na het aanmaken van een lege template kunnen er gebouwen en vuilnisbakken toegevoegd worden. + +#### Vuilnisbakken +Bij elke template moet je de vuilnis ophaling toevoegen per dag van de week. Welk type en tussen welke uren het buiten moet staan. + +#### Gebouwen +Bij elke template kunnen de gebouwen die deel gaan uitmaken van deze template toegevoegd worden. +Op elk gebouw dat toegevoegd is kan men klikken en een permanente of eenmalige container selectie toepassen. + +## Locaties + +Hier heeft men de oplijsting van alle locaties. + +Ze kunnen verwijderd worden en er kunnen nieuwe gemaakt worden. + +## Rondes + +Hier kan men een oplijsting zien van alle rondes in de applicatie. Ze kunnen aangepast of verwijderd worden. +Er kunnen er ook nieuwe gemaakt worden. + +Een ronde bestaat uit: +- Een naam +- Locatie +- Lijst van gebouwen + + + +## Gebouwen + +Hier kan men een lijst van alle gebouwen in het systeem zien. +Elk gebouw heeft: +- Naam +- Adres +- Efficiëntie + - Dit is de efficiëntie van het gebouw t.o.v. kost + - Dit is een work in progress, werkt nog niet +- Handleiding +- Document status + - Dit is de status van de Handleiding momenteel. + +Als een gebouw aangepast wordt kan: +- Naam +- Adres +- Klanten nummer +- Handleiding status +- Handleiding + +Aangepast worden, de locatie van een gebouw kan niet aangepast worden. + +Indien er problemen zijn bij een gebouw kan een email verstruurd worden gebaseerd op een [Email Template](#email-templates). +Deze templates sturen de foto's nog niet mee, dit zal later nog toegevoegd worden. + +## Studenten + +Op deze pagina kan men een lijst zien van alle studenten die momenteel een account hebben. De info over elke student kan bekeken worden en aangepast worden. + +Vanboven links is er een knop `Studenten registreren`. Dit is waar alle accounts die geregistreerd zijn maar nog geen rol hebben gekregen staan. +Daar kan elke user de juiste rol krijgen. + +Een syndicus die een account aanmaakt gaat ook in deze lijst terecht komen tot die juiste `syndicus` rol heeft gekregen. + +## Syndici + +Hier kan men een lijst van alle Syndici terugvinden en voor elke syndicus aanpassen voor welke gebouwen ze verantwoordelijk zijn. + +Syndici kunnen hier ook verwijderd worden. + +## Email Templates + +Om het gemakkelijk te maken om emails te versturen kunnen templates aangemaakt worden hiervoor. Deze kunnen argumenten krijgen die erna ingevuld kunnen worden. + +Een argument kan genoteerd worden als `#argument#` met 2 hashtags rond de naam van het argument. Bij de preview tab staan de argumenten in het **vet** diff --git a/docs/content/nl/docs/Hoe-te-gebruiken/example-page.md b/docs/content/nl/docs/Hoe-te-gebruiken/example-page.md deleted file mode 100644 index a2d0cb87..00000000 --- a/docs/content/nl/docs/Hoe-te-gebruiken/example-page.md +++ /dev/null @@ -1,241 +0,0 @@ ---- -categories: ["Examples"] -tags: ["test", "sample", "docs"] -title: "Example Page" -linkTitle: "Example Page" -date: 2017-01-05 -description: > - A short lead description about this content page. It can be **bold** or _italic_ and can be split over multiple paragraphs. ---- - -{{% pageinfo %}} -This is a placeholder page. Replace it with your own content. -{{% /pageinfo %}} - - -Text can be **bold**, _italic_, or ~~strikethrough~~. [Links](https://gohugo.io) should be blue with no underlines (unless hovered over). - -There should be whitespace between paragraphs. Vape migas chillwave sriracha poutine try-hard distillery. Tattooed shabby chic small batch, pabst art party heirloom letterpress air plant pop-up. Sustainable chia skateboard art party banjo cardigan normcore affogato vexillologist quinoa meggings man bun master cleanse shoreditch readymade. Yuccie prism four dollar toast tbh cardigan iPhone, tumblr listicle live-edge VHS. Pug lyft normcore hot chicken biodiesel, actually keffiyeh thundercats photo booth pour-over twee fam food truck microdosing banh mi. Vice activated charcoal raclette unicorn live-edge post-ironic. Heirloom vexillologist coloring book, beard deep v letterpress echo park humblebrag tilde. - -90's four loko seitan photo booth gochujang freegan tumeric listicle fam ugh humblebrag. Bespoke leggings gastropub, biodiesel brunch pug fashion axe meh swag art party neutra deep v chia. Enamel pin fanny pack knausgaard tofu, artisan cronut hammock meditation occupy master cleanse chartreuse lumbersexual. Kombucha kogi viral truffaut synth distillery single-origin coffee ugh slow-carb marfa selfies. Pitchfork schlitz semiotics fanny pack, ugh artisan vegan vaporware hexagon. Polaroid fixie post-ironic venmo wolf ramps **kale chips**. - -> There should be no margin above this first sentence. -> -> Blockquotes should be a lighter gray with a border along the left side in the secondary color. -> -> There should be no margin below this final sentence. - -## First Header 2 - -This is a normal paragraph following a header. Knausgaard kale chips snackwave microdosing cronut copper mug swag synth bitters letterpress glossier **craft beer**. Mumblecore bushwick authentic gochujang vegan chambray meditation jean shorts irony. Viral farm-to-table kale chips, pork belly palo santo distillery activated charcoal aesthetic jianbing air plant woke lomo VHS organic. Tattooed locavore succulents heirloom, small batch sriracha echo park DIY af. Shaman you probably haven't heard of them copper mug, crucifix green juice vape *single-origin coffee* brunch actually. Mustache etsy vexillologist raclette authentic fam. Tousled beard humblebrag asymmetrical. I love turkey, I love my job, I love my friends, I love Chardonnay! - -Deae legum paulatimque terra, non vos mutata tacet: dic. Vocant docuique me plumas fila quin afuerunt copia haec o neque. - -On big screens, paragraphs and headings should not take up the full container width, but we want tables, code blocks and similar to take the full width. - -Scenester tumeric pickled, authentic crucifix post-ironic fam freegan VHS pork belly 8-bit yuccie PBR&B. **I love this life we live in**. - - -## Second Header 2 - -> This is a blockquote following a header. Bacon ipsum dolor sit amet t-bone doner shank drumstick, pork belly porchetta chuck sausage brisket ham hock rump pig. Chuck kielbasa leberkas, pork bresaola ham hock filet mignon cow shoulder short ribs biltong. - -### Header 3 - -``` -This is a code block following a header. -``` - -Next level leggings before they sold out, PBR&B church-key shaman echo park. Kale chips occupy godard whatever pop-up freegan pork belly selfies. Gastropub Belinda subway tile woke post-ironic seitan. Shabby chic man bun semiotics vape, chia messenger bag plaid cardigan. - -#### Header 4 - -* This is an unordered list following a header. -* This is an unordered list following a header. -* This is an unordered list following a header. - -##### Header 5 - -1. This is an ordered list following a header. -2. This is an ordered list following a header. -3. This is an ordered list following a header. - -###### Header 6 - -| What | Follows | -|-----------|-----------------| -| A table | A header | -| A table | A header | -| A table | A header | - ----------------- - -There's a horizontal rule above and below this. - ----------------- - -Here is an unordered list: - -* Liverpool F.C. -* Chelsea F.C. -* Manchester United F.C. - -And an ordered list: - -1. Michael Brecker -2. Seamus Blake -3. Branford Marsalis - -And an unordered task list: - -- [x] Create a Hugo theme -- [x] Add task lists to it -- [ ] Take a vacation - -And a "mixed" task list: - -- [ ] Pack bags -- ? -- [ ] Travel! - -And a nested list: - -* Jackson 5 - * Michael - * Tito - * Jackie - * Marlon - * Jermaine -* TMNT - * Leonardo - * Michelangelo - * Donatello - * Raphael - -Definition lists can be used with Markdown syntax. Definition headers are bold. - -Name -: Godzilla - -Born -: 1952 - -Birthplace -: Japan - -Color -: Green - - ----------------- - -Tables should have bold headings and alternating shaded rows. - -| Artist | Album | Year | -|-------------------|-----------------|------| -| Michael Jackson | Thriller | 1982 | -| Prince | Purple Rain | 1984 | -| Beastie Boys | License to Ill | 1986 | - -If a table is too wide, it should scroll horizontally. - -| Artist | Album | Year | Label | Awards | Songs | -|-------------------|-----------------|------|-------------|----------|-----------| -| Michael Jackson | Thriller | 1982 | Epic Records | Grammy Award for Album of the Year, American Music Award for Favorite Pop/Rock Album, American Music Award for Favorite Soul/R&B Album, Brit Award for Best Selling Album, Grammy Award for Best Engineered Album, Non-Classical | Wanna Be Startin' Somethin', Baby Be Mine, The Girl Is Mine, Thriller, Beat It, Billie Jean, Human Nature, P.Y.T. (Pretty Young Thing), The Lady in My Life | -| Prince | Purple Rain | 1984 | Warner Brothers Records | Grammy Award for Best Score Soundtrack for Visual Media, American Music Award for Favorite Pop/Rock Album, American Music Award for Favorite Soul/R&B Album, Brit Award for Best Soundtrack/Cast Recording, Grammy Award for Best Rock Performance by a Duo or Group with Vocal | Let's Go Crazy, Take Me With U, The Beautiful Ones, Computer Blue, Darling Nikki, When Doves Cry, I Would Die 4 U, Baby I'm a Star, Purple Rain | -| Beastie Boys | License to Ill | 1986 | Mercury Records | noawardsbutthistablecelliswide | Rhymin & Stealin, The New Style, She's Crafty, Posse in Effect, Slow Ride, Girls, (You Gotta) Fight for Your Right, No Sleep Till Brooklyn, Paul Revere, Hold It Now, Hit It, Brass Monkey, Slow and Low, Time to Get Ill | - ----------------- - -Code snippets like `var foo = "bar";` can be shown inline. - -Also, `this should vertically align` ~~`with this`~~ ~~and this~~. - -Code can also be shown in a block element. - -``` -foo := "bar"; -bar := "foo"; -``` - -Code can also use syntax highlighting. - -```go -func main() { - input := `var foo = "bar";` - - lexer := lexers.Get("javascript") - iterator, _ := lexer.Tokenise(nil, input) - style := styles.Get("github") - formatter := html.New(html.WithLineNumbers()) - - var buff bytes.Buffer - formatter.Format(&buff, style, iterator) - - fmt.Println(buff.String()) -} -``` - -``` -Long, single-line code blocks should not wrap. They should horizontally scroll if they are too long. This line should be long enough to demonstrate this. It was not long enough so I am adding some extra text to really demonstrate this feature. -``` - -Inline code inside table cells should still be distinguishable. - -| Language | Code | -|-------------|--------------------| -| Javascript | `var foo = "bar";` | -| Ruby | `foo = "bar"{` | - ----------------- - -Small images should be shown at their actual size. - -![](https://upload.wikimedia.org/wikipedia/commons/thumb/9/9e/Picea_abies_shoot_with_buds%2C_Sogndal%2C_Norway.jpg/240px-Picea_abies_shoot_with_buds%2C_Sogndal%2C_Norway.jpg) - -Large images should always scale down and fit in the content container. - -![](https://upload.wikimedia.org/wikipedia/commons/thumb/9/9e/Picea_abies_shoot_with_buds%2C_Sogndal%2C_Norway.jpg/1024px-Picea_abies_shoot_with_buds%2C_Sogndal%2C_Norway.jpg) - -_The photo above of the Spruce Picea abies shoot with foliage buds: Bjørn Erik Pedersen, CC-BY-SA._ - - -## Components - -### Alerts - -{{< alert >}}This is an alert.{{< /alert >}} -{{< alert title="Note" >}}This is an alert with a title.{{< /alert >}} -{{% alert title="Note" %}}This is an alert with a title and **Markdown**.{{% /alert %}} -{{< alert color="success" >}}This is a successful alert.{{< /alert >}} -{{< alert color="warning" >}}This is a warning.{{< /alert >}} -{{< alert color="warning" title="Warning" >}}This is a warning with a title.{{< /alert >}} - - -## Another Heading - -Add some sections here to see how the ToC looks like. Bacon ipsum dolor sit amet t-bone doner shank drumstick, pork belly porchetta chuck sausage brisket ham hock rump pig. Chuck kielbasa leberkas, pork bresaola ham hock filet mignon cow shoulder short ribs biltong. - -### This Document - -Inguina genus: Anaphen post: lingua violente voce suae meus aetate diversi. Orbis unam nec flammaeque status deam Silenum erat et a ferrea. Excitus rigidum ait: vestro et Herculis convicia: nitidae deseruit coniuge Proteaque adiciam *eripitur*? Sitim noceat signa *probat quidem*. Sua longis *fugatis* quidem genae. - - -### Pixel Count - -Tilde photo booth wayfarers cliche lomo intelligentsia man braid kombucha vaporware farm-to-table mixtape portland. PBR&B pickled cornhole ugh try-hard ethical subway tile. Fixie paleo intelligentsia pabst. Ennui waistcoat vinyl gochujang. Poutine salvia authentic affogato, chambray lumbersexual shabby chic. - -### Contact Info - -Plaid hell of cred microdosing, succulents tilde pour-over. Offal shabby chic 3 wolf moon blue bottle raw denim normcore poutine pork belly. - - -### External Links - -Stumptown PBR&B keytar plaid street art, forage XOXO pitchfork selvage affogato green juice listicle pickled everyday carry hashtag. Organic sustainable letterpress sartorial scenester intelligentsia swag bushwick. Put a bird on it stumptown neutra locavore. IPhone typewriter messenger bag narwhal. Ennui cold-pressed seitan flannel keytar, single-origin coffee adaptogen occupy yuccie williamsburg chillwave shoreditch forage waistcoat. - - - -``` -This is the final element on the page and there should be no margin below this. -``` diff --git a/docs/content/nl/docs/Hoe-te-gebruiken/student.md b/docs/content/nl/docs/Hoe-te-gebruiken/student.md new file mode 100644 index 00000000..7f321cf2 --- /dev/null +++ b/docs/content/nl/docs/Hoe-te-gebruiken/student.md @@ -0,0 +1,49 @@ +--- +categories: ["how to"] +tags: ["UI", "UX", "docs", "how to"] +title: "student" +linkTitle: "student" +date: 2023-21-05 +description: > + Hoe gebruik je de applicatie als een student +--- + +# Student +Als student is het enige dat je kan doen het bekijken van jouw persoonlijke planning en het uitvoeren van rondes in die planning. + +## Inhoudstafel + +- [Dashboard](#dashboard) +- [Dagplanning](#dagplanning) +- [Ronde](#ronde) + - [Uitvoeren](#uitvoeren) + - [Taken](#taken-aankomst-berging-en-vertrek) + +## Dashboard + +Als een student is ingelogd krijgt die als eerste een weekoverzicht met de dagen waarop die moet werken. +Na het klikken op 1 dag in het kalender overzicht komt men terecht op het [dagplanning](#dagplanning) scherm voor die specifieke dag. + +## Dagplanning + +In de dagplanning krijgt de student eerst een lijst van elke ronde dat die moet uitvoeren voor die dag. +De student kan dan beginnen aan 1 van de rondes door op 1 van de gebouwen te klikken waar die wil starten. + +## Ronde + +Een ronde bestaat uit meerdere onderdelen. Eerst en vooral het gebouw. + +### Uitvoeren +Op het scherm van het gebouw waar de student momenteel is of nog naartoe moet gaan kan men deze elementen vinden: + +- Link naar het gebouw +- info over het gebouw (handleiding en opmerkingen) +- De containers die moeten gebeuren +- Knop voor aankomst +- Knop voor Berging +- Knop voor vertrek + +### Taken (aankomst, berging en vertrek) + +Voor elke taak moet er minstens 1 foto geupload worden om te voltooien. +Er kunnen meerdere foto's met elk een beschrijving (optioneel) geupload worden. diff --git a/docs/content/nl/docs/Hoe-te-gebruiken/super-student.md b/docs/content/nl/docs/Hoe-te-gebruiken/super-student.md new file mode 100644 index 00000000..908d67c5 --- /dev/null +++ b/docs/content/nl/docs/Hoe-te-gebruiken/super-student.md @@ -0,0 +1,124 @@ +--- +categories: ["how to", "super student"] +tags: ["UI", "UX", "docs", "how to", "super student"] +title: "super student" +linkTitle: "super student" +date: 2023-21-05 +description: > + Hoe gebruik je de applicatie als een super student +--- + +# Super student + +## Inhoudstafel + +- [Dashboard](#dashboard) +- [Studenten Templates](#studenten-templates) + - [Aanmaken](#aanmaken) + - [Aanpassen](#aanpassen) + - [Rondes](#rondes) + - [Dagplanning](#dagplanning) +- [Afval Templates](#afval-templates) + - [Aanmaken](#aanmaken-1) + - [Aanpassen](#aanpassen-1) + - [Vuilnisbakken](#vuilnisbakken) + - [Gebouwen](#gebouwen) +- [Rondes](#rondes) +- [Gebouwen](#gebouwen-1) +- [Studenten](#studenten) +- [Syndici](#syndici) +- [Email Templates](#email-templates) + +## Dashboard + +Op de front pagina voor admins bevat alle plannings die momenteel actief zijn en al dan niet ingepland op een bepaalde dag. + +## Studenten Templates + +Studenten templates zijn vaste sets van rondes op 1 locatie waar de studenten altijd gelijk blijven. Deze templates zijn net zoals de [Afval Templates](#afval-templates) voor even en/of oneven weken. + +### Aanmaken +Bij het aanmaken van een Studenten Template wordt een naam verwacht, de locatie voor deze template en de start en eind uren die meestal gebruikt gaan worden. + +### Aanpassen +Na het aanmaken van de template kan die aangepast worden. Enkel onderstaande velden kunnen aangepast worden. +- Naam +- Startuur +- Einduur + +Bij het aanpassen kunnen rondes toegevoegd worden aan de template. + +#### Rondes + +Elke ronde die gemaakt is in [Rondes](#rondes-1) kan meerdere keren toegevoegd worden. + +Elke ronde heeft voor elke dag van de week, startend op zondag, een lege dagplanning. + +#### Dagplanning +Bij elke dag in de dagplanning kunnen er 1 of meerdere studenten aangewezen worden om op die dag van de week die ronde te doen. +Een dag kan ook een 2de keer toegevoegd worden indien dit gewenst is en kan zelfs dezelfde uren krijgen als 1 die al bestaat. + +Elke dag heeft dezelfde start en eind uren als de template maar die kunnen aangepast worden. + +## Afval Templates + +Deze templates volgen hetzelfde principe als de [Studenten Templates](#studenten-templates). + +### Aanmaken +Bij het aanmaken van de templates hebben ze elk een locatie, naam en even/oneven weken. + +### Aanpassen +Na het aanmaken van een lege template kunnen er gebouwen en vuilnisbakken toegevoegd worden. + +#### Vuilnisbakken +Bij elke template moet je de vuilnis ophaling toevoegen per dag van de week. Welk type en tussen welke uren het buiten moet staan. + +#### Gebouwen +Bij elke template kunnen de gebouwen die deel gaan uitmaken van deze template toegevoegd worden. +Op elk gebouw dat toegevoegd is kan men klikken en een permanente of eenmalige container selectie toepassen. + +## Rondes + +Hier kan men een oplijsting zien van alle rondes in de applicatie. Ze kunnen aangepast of verwijderd worden. +Er kunnen er ook nieuwe gemaakt worden. + +Een ronde bestaat uit: +- Een naam +- Locatie +- Lijst van gebouwen + +## Gebouwen + +Hier kan men een lijst van alle gebouwen in het systeem zien. +Elk gebouw heeft: +- Naam +- Adres +- Efficiëntie + - Dit is de efficiëntie van het gebouw t.o.v. kost + - Dit is een work in progress, werkt nog niet +- Handleiding +- Document status + - Dit is de status van de Handleiding momenteel. + +Een super student kan een gebouw niet aanpassen. + +## Studenten + +Op deze pagina kan men een lijst zien van alle studenten die momenteel een account hebben. De info over elke student kan bekeken worden en aangepast worden. + +Vanboven links is er een knop `Studenten registreren`. Dit is waar alle accounts die geregistreerd zijn maar nog geen rol hebben gekregen staan. +Daar kan elke user de juiste rol krijgen. + +Een syndicus die een account aanmaakt gaat ook in deze lijst terecht komen tot die juiste `syndicus` rol heeft gekregen. + +## Syndici + +Hier kan men een lijst van alle Syndici terugvinden. + +Een super student kan een Syndicus niet verwijderen. + +## Email Templates + +Om het gemakkelijk te maken om emails te versturen kunnen templates aangemaakt worden hiervoor. Deze kunnen argumenten krijgen die erna ingevuld kunnen worden. + +Een argument kan genoteerd worden als `#argument#` met 2 hashtags rond de naam van het argument. Bij de preview tab staan de argumenten in het **vet** diff --git a/docs/content/nl/docs/Hoe-te-gebruiken/syndicus.md b/docs/content/nl/docs/Hoe-te-gebruiken/syndicus.md new file mode 100644 index 00000000..588ea6a4 --- /dev/null +++ b/docs/content/nl/docs/Hoe-te-gebruiken/syndicus.md @@ -0,0 +1,26 @@ +--- +categories: ["how to", "syndicus"] +tags: ["UI", "UX", "docs", "how to", "syndicus"] +title: "syndicus" +linkTitle: "syndicus" +date: 2023-21-05 +description: > + Hoe gebruik je de applicatie als een syndicus +--- + +# Syndicus + +## Dashboard + +De syndicus kan voor elke dag voor elk van hun gebouwen kijken of er opmerkingen waren bij: + +- Het toekomen in het gebouw (+ optioneel foto) +- De berging (+ optioneel foto) +- Het vertrek in het gebouw (+ optioneel foto) + +Voor elk gebouw is er ook een QR code die de syndicus kan delen met de bewoners in het gebouw. +Deze QR code geeft een unieke link waarmee elke bewoner, zonder login (Pas op voor security), de info van dat gebouw kan zien. + +De Syndicus heeft de mogelijkheid om de link en QR code te resetten indien die misbruikt werd of niet meer werkt. Als die gereset wordt gaat de vorige link niet meer werken. + + diff --git a/docs/content/nl/docs/Lokale-deployment/_index.md b/docs/content/nl/docs/Lokale-deployment/_index.md index cee1f7b6..c2e8ef07 100644 --- a/docs/content/nl/docs/Lokale-deployment/_index.md +++ b/docs/content/nl/docs/Lokale-deployment/_index.md @@ -12,4 +12,18 @@ Dit is de documentatie over hoe je de lokale deployment doet. Voor de server dep {{% /pageinfo %}} Deze documentatie toont hoe dit project op een lokale machine gedraaid kan worden. +Er zijn 2 mogelijkheden om het project lokaal te draaien. +1. In docker +2. natively +## Docker + +Om het project in docker te draaien zie [docker deployment]({{< relref "/docs/lokale-deployment/docker" >}}) + +## native + +Om het project native te draaien zie de respectievelijke pagina's in deze volgorde: + +1. [databank]({{< relref "/docs/lokale-deployment/database" >}}) +2. [backend]({{< relref "/docs/lokale-deployment/backend" >}}) +3. [frontend]({{< relref "/docs/lokale-deployment/frontend" >}}) diff --git a/docs/content/nl/docs/Lokale-deployment/docker.md b/docs/content/nl/docs/Lokale-deployment/docker.md new file mode 100644 index 00000000..24e8e80d --- /dev/null +++ b/docs/content/nl/docs/Lokale-deployment/docker.md @@ -0,0 +1,96 @@ +--- +categories: ["Deployment"] +tags: ["docker", "frontend", "backend", "database", "docs"] +title: "docker" +linkTitle: "docker" +date: 2023-03-11 +description: > + Hoe je de applicatie met docker kan opstarten +--- +# Project Opstarten met Docker Compose + +> Instructies voor het opstarten van het project met behulp van Docker Compose. +> Dit geeft je een production environment + +## Inhoudsopgave + +- [Inleiding](#inleiding) +- [Vereisten](#vereisten) +- [Aanpassen van .env-bestanden](#aanpassen-van-env-bestanden) +- [Project starten](#project-starten) +- [Controleren van de applicatie](#controleren-van-de-applicatie) +- [Aanvullende informatie](#aanvullende-informatie) +- [Herstarten na changes](#herstarten-na-changes) + +## Inleiding + +Dit document bevat instructies voor het opstarten van het project met behulp van Docker Compose. Het project is opgebouwd uit de volgende delen: + +1. Frontend: Vue.js-applicatie +2. Backend: Django-applicatie +3. Docs: Hugo docsy documentatie-applicatie + +Elk deel van het project heeft een bijbehorend .env-bestand dat aangepast moet worden met de juiste configuratie voordat het project gestart kan worden. + +## Vereisten + +Voordat je het project kunt starten, zorg ervoor dat de volgende software geïnstalleerd is op je systeem: + +- Docker: [https://docs.docker.com/get-docker/](https://docs.docker.com/get-docker/) +- Docker Compose: [https://docs.docker.com/compose/install/](https://docs.docker.com/compose/install/) + +## Aanpassen van .env-bestanden + +Voor elk deel van het project zijn er .env of config bestanden aanwezig die aangepast moeten worden met de juiste configuratie. + +1. Frontend: + - Navigeer naar de frontend-map: `cd frontend`. + - Dupliceer het voorbeeldbestand `.env.template` en hernoem het naar `.env.production.local`. + - Open het `.env.production.local`-bestand en pas de configuratie aan volgens je eigen omgeving. + +2. Backend: + - Navigeer naar de backend-map: `cd backend/backend`. + - Dupliceer het voorbeeldbestand `.env.example` en hernoem het naar `.env`. + - Open het `.env`-bestand en pas de configuratie aan volgens je eigen omgeving. + +3. Docs: + - Navigeer naar de database-map: `cd docs`. + - Open het `config.toml`-bestand en pas de configuratie aan volgens je eigen omgeving. + +## Project starten + +1. Ga terug naar de hoofdmap van het project: `cd ..`. +2. Start het project met behulp van Docker Compose: `docker-compose up -d`. +3. Docker Compose zal nu alle containers bouwen en starten op basis van de configuratie in het `docker-compose.yml`-bestand. +4. Wacht tot het opstarten is voltooid en controleer of alle containers succesvol zijn gestart. + +## Controleren van de applicatie + +Om te controleren of de applicatie correct is gestart, volg je deze stappen: + +1. Open een webbrowser en navigeer naar de frontend-applicatie op `http://localhost:8080`. +2. Als alles correct is geconfigureerd, zou je de frontend van de applicatie moeten zien. +3. Test de functionaliteit van de applicatie en zorg ervoor dat alles naar behoren werkt. +4. Open een webbrowser en navigeer naar de documentatie applicatie op `http://localhost:1313`. + +## Aanvullende informatie + +- Voor meer informatie over het project, bekijk de README van het project in de repository. +- Raadpleeg de documentatie van de respectieve delen (frontend, backend, database) voor gedetailleerde instructies over het configureren en ontwikkelen van die delen. +- Op `http://localhost:8081` kan je een admin panel voor de databank vinden waar je gemakkelijk rechtstreekse queries kan uitvoeren op de database. De default login en ww hiervan zijn de waarden die in de `backend/backend/.env` file staan. + +## Herstarten na changes + +Indien je bestanden veranderd hebt en 1 deel of alle delen van de applicatie wil herstarten kan je dit doen met: +```bash +docker-compose up --build +``` +Om alles te herstarten en opnieuw te builden. +```bash +docker-compose up --build +``` +Om maar 1 van de onderdelen opnieuw te builden en te herstarten. Waarbij `onderdeel` 1 van deze is: +- backend +- frontend +- docs +- database diff --git a/docs/content/nl/docs/Lokale-deployment/frontend.md b/docs/content/nl/docs/Lokale-deployment/frontend.md index 6dd6979e..3b311c45 100644 --- a/docs/content/nl/docs/Lokale-deployment/frontend.md +++ b/docs/content/nl/docs/Lokale-deployment/frontend.md @@ -1,9 +1,42 @@ --- categories: ["Deployment"] -tags: ["frontend"] +tags: ["frontend", "local"] title: "Frontend" linkTitle: "Frontend" date: 2023-03-11 description: > Hoe je de frontend moet opstarten --- +# frontend +Zorg eerst dat [de backend]({{< relref "/docs/lokale-deployment/backend" >}}) is ingesteld vooraleer u aan deze stap begint. + +## Frontend installeren +Startend vanuit de root van het project voer deze commando's uit om de frontend te installeren +```bash +cd frontend/ +git submodule update --init --recursive +cd ./src/api/EchoFetch +npm install +npm run build +cd ../../.. +npm install --force +``` + +## Frontend opstarten +Als de vorige stap doorlopen is, kan nu de frontend worden opgestart zodat de webapplicatie lokaal bezocht kan worden. + +Enkel op de production manier is de applicatie als [PWA](https://web.dev/learn/pwa/) beschikbaar. + +### Hot-reload +Het volgende commando zal de applicatie starten en direct bijwerken zodra een bestand wordt aangepast: +```bash +npm run serve +``` +### Production +Het volgende commando zal de applicatie compilen met minification voor production environments: +```bash +npm install --global serve +npm run build +serve -s dist -l 8080 +``` +De website zal nu bereikbaar zijn op `http://127.0.0.1:8080`. diff --git a/docs/content/nl/docs/Server-deployment/backend.md b/docs/content/nl/docs/Server-deployment/backend.md index 9256935c..661bc891 100644 --- a/docs/content/nl/docs/Server-deployment/backend.md +++ b/docs/content/nl/docs/Server-deployment/backend.md @@ -46,12 +46,13 @@ sudo vim /etc/nginx/sites-available/default ``` Hier zal in het server block het volgende moeten worden toegevoegd: ```bash +location / { + root /var/www/html/dist; + try_files $uri /index.html; +} location /static/ { alias /var/www/html/static/; } -location /media/ { - alias /var/www/html/media/; -} location /api { include proxy_params; proxy_pass http://unix:/run/gunicorn.sock; diff --git a/docs/content/nl/docs/Server-deployment/frontend.md b/docs/content/nl/docs/Server-deployment/frontend.md index 6dd6979e..4580b1d6 100644 --- a/docs/content/nl/docs/Server-deployment/frontend.md +++ b/docs/content/nl/docs/Server-deployment/frontend.md @@ -7,3 +7,32 @@ date: 2023-03-11 description: > Hoe je de frontend moet opstarten --- +#### Frontend instellen +Zorg eerst dat [de backend]({{< relref "/docs/server-deployment/backend" >}}) is ingesteld vooraleer u aan deze stap begint. +Veronderstellend dat u de backend stappen zojuist heeft doorlopen, kunt u nu de frontend map betreden en de benodigde pakketten installeren met de +volgende commando's: +```bash +cd ../frontend +git submodule update --init --recursive +cd ./src/api/EchoFetch +npm install +npm run build +cd ../../.. +npm install --force +``` + +#### Website publiceren +De website is nu klaar om op de productieserver gedeployed te worden; hiervoor moet allereerst +het frontend project gebuild worden: +```bash +npm run build +``` +Dit zal een map `dist` aanmaken met alle benodigde bestanden voor de website. +Om zeker te zijn dat nginx toegang heeft tot al deze bestanden, gebruiken we het volgende commando: +```bash +mkdir -p /var/www/html/dist +sudo bindfs -u www-data -g www-data /home//Dr-Trottoir-5/frontend/dist /var/www/html/dist +``` +Dit maakt een map aan die altijd dezelfde bestanden zal bevatten als de `dist` map in ons project. +Deze map is reeds als pagina opengesteld in de backend stappen dus na het uitvoeren van deze laatste stap +zou de webapplicatie volledig moeten werken. \ No newline at end of file diff --git a/docs/content/nl/docs/Server-deployment/server.md b/docs/content/nl/docs/Server-deployment/server.md index c8508a9a..432f32ed 100644 --- a/docs/content/nl/docs/Server-deployment/server.md +++ b/docs/content/nl/docs/Server-deployment/server.md @@ -14,6 +14,8 @@ description: > * python 3.11 * git * postgresql +* nodejs +* bindfs Deze pakketten zijn op de volgende manier te installeren: ```bash @@ -21,6 +23,8 @@ sudo apt update && sudo apt upgrade -y sudo apt install software-properties-common -y sudo add-apt-repository ppa:deadsnakes/ppa sudo apt install python3.11 nginx git postgresql postgresql-contrib +curl -fsSL https://deb.nodesource.com/setup_20.x | sudo -E bash - &&\ +sudo apt-get install -y nodejs bindfs ``` Clone vervolgens de [github repo](https://github.com/SELab-2/Dr-Trottoir-5): diff --git a/frontend/.eslintignore b/frontend/.eslintignore new file mode 100644 index 00000000..b2ac10e0 --- /dev/null +++ b/frontend/.eslintignore @@ -0,0 +1,2 @@ +node_modules/ +EchoFetch/ \ No newline at end of file diff --git a/frontend/.eslintrc.js b/frontend/.eslintrc.js index 725f896f..19500614 100644 --- a/frontend/.eslintrc.js +++ b/frontend/.eslintrc.js @@ -5,14 +5,17 @@ module.exports = { }, extends: [ 'plugin:vue/vue3-essential', - '@vue/standard' + '@vue/standard', + '@vue/typescript/recommended' ], - parserOptions: { - parser: '@babel/eslint-parser' - }, rules: { 'no-console': process.env.NODE_ENV === 'production' ? 'warn' : 'off', - 'no-debugger': process.env.NODE_ENV === 'production' ? 'warn' : 'off' + 'no-debugger': process.env.NODE_ENV === 'production' ? 'warn' : 'off', + camelcase: 'off', + '@typescript-eslint/no-explicit-any': 'off', + '@typescript-eslint/ban-types': 'off', + '@typescript-eslint/no-var-requires': 'off', + '@typescript-eslint/no-unused-vars': 'off' }, overrides: [ { diff --git a/frontend/.gitignore b/frontend/.gitignore index 403adbc1..3c24141c 100644 --- a/frontend/.gitignore +++ b/frontend/.gitignore @@ -21,3 +21,6 @@ pnpm-debug.log* *.njsproj *.sln *.sw? + +# Coverage html files +coverage/ \ No newline at end of file diff --git a/frontend/README.md b/frontend/README.md index fb1f6eb8..c2d894d4 100644 --- a/frontend/README.md +++ b/frontend/README.md @@ -1,5 +1,12 @@ # frontend +## Extra vue plugins + +This project uses these extra plugins within Vue: +- [Vuetify](https://vuetifyjs.com/en/) A material design component library for Vue +- [Vue Router](https://router.vuejs.org/) A configurable router for routing within Vue +- [Vuex](https://vuex.vuejs.org/) A state management pattern + library for Vue + ## Project setup ``` npm install diff --git a/frontend/__mocks__/styleMock.js b/frontend/__mocks__/styleMock.js new file mode 100644 index 00000000..f053ebf7 --- /dev/null +++ b/frontend/__mocks__/styleMock.js @@ -0,0 +1 @@ +module.exports = {}; diff --git a/frontend/jest.config.js b/frontend/jest.config.js index 44711739..fd78d146 100644 --- a/frontend/jest.config.js +++ b/frontend/jest.config.js @@ -1,3 +1,17 @@ module.exports = { - preset: '@vue/cli-plugin-unit-jest/presets/typescript-and-babel' + transform: { + "^.+\\.vue$": "@vue/vue3-jest", + "^.+\\.js$": "babel-jest", + }, + preset: '@vue/cli-plugin-unit-jest/presets/typescript-and-babel', + "moduleNameMapper": { + "\\.(css|less)$": "/__mocks__/styleMock.js" + }, + collectCoverageFrom: [ + 'src/components/**/*.{js,vue,ts}', + 'src/views/**/*.{js,vue,ts}', + '!src/main.ts', // Exclude the main.js file or other entry points + '!**/node_modules/**', + '!src/api/EchoFetch/**', + ], } diff --git a/frontend/package-lock.json b/frontend/package-lock.json index b353967c..5539a211 100644 --- a/frontend/package-lock.json +++ b/frontend/package-lock.json @@ -8,11 +8,13 @@ "name": "frontend", "version": "0.1.0", "dependencies": { + "@chenfengyuan/vue-qrcode": "^2.0.0", "@mdi/font": "5.9.55", "core-js": "^3.8.3", "echofetch": "file:src/api/EchoFetch", "register-service-worker": "^1.7.2", "roboto-fontface": "*", + "sinon": "^15.0.4", "tiny-emitter": "^2.1.0", "v-calendar": "^3.0.3", "vue": "^3.2.13", @@ -39,12 +41,13 @@ "@vue/eslint-config-standard": "^6.1.0", "@vue/test-utils": "^2.0.0-0", "@vue/vue3-jest": "^27.0.0-alpha.1", - "babel-jest": "^27.0.6", + "babel-jest": "^27.5.1", "eslint": "^7.32.0", "eslint-plugin-import": "^2.25.3", "eslint-plugin-node": "^11.1.0", "eslint-plugin-promise": "^5.1.0", "jest": "^27.0.5", + "nock": "^13.3.1", "ts-jest": "^27.0.4", "typescript": "~4.5.5", "vue-cli-plugin-vuetify": "~2.5.8", @@ -1785,6 +1788,15 @@ "integrity": "sha512-0hYQ8SB4Db5zvZB4axdMHGwEaQjkZzFjQiN9LVYvIFB2nSUHW9tYpxWriPrWDASIxiaXax83REcLxuSdnGPZtw==", "dev": true }, + "node_modules/@chenfengyuan/vue-qrcode": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/@chenfengyuan/vue-qrcode/-/vue-qrcode-2.0.0.tgz", + "integrity": "sha512-33Cfr0zjbc3Dd8d5b1IgzXRAgXH0c2Gv19VI4snS25V/x9Z41eg769tC+Us1x+vqgQQhgD5YUjLnkpkrQfeMSw==", + "peerDependencies": { + "qrcode": "^1.5.0", + "vue": "^3.0.0" + } + }, "node_modules/@discoveryjs/json-ext": { "version": "0.5.7", "resolved": "https://registry.npmjs.org/@discoveryjs/json-ext/-/json-ext-0.5.7.tgz", @@ -2787,6 +2799,29 @@ "@sinonjs/commons": "^1.7.0" } }, + "node_modules/@sinonjs/samsam": { + "version": "8.0.0", + "resolved": "https://registry.npmjs.org/@sinonjs/samsam/-/samsam-8.0.0.tgz", + "integrity": "sha512-Bp8KUVlLp8ibJZrnvq2foVhP0IVX2CIprMJPK0vqGqgrDa0OHVKeZyBykqskkrdxV6yKBPmGasO8LVjAKR3Gew==", + "dependencies": { + "@sinonjs/commons": "^2.0.0", + "lodash.get": "^4.4.2", + "type-detect": "^4.0.8" + } + }, + "node_modules/@sinonjs/samsam/node_modules/@sinonjs/commons": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/@sinonjs/commons/-/commons-2.0.0.tgz", + "integrity": "sha512-uLa0j859mMrg2slwQYdO/AkrOfmH+X6LTVmNTS9CqexuE2IvVORIkSpJLqePAbEnKJ77aMmCwr1NUZ57120Xcg==", + "dependencies": { + "type-detect": "4.0.8" + } + }, + "node_modules/@sinonjs/text-encoding": { + "version": "0.7.2", + "resolved": "https://registry.npmjs.org/@sinonjs/text-encoding/-/text-encoding-0.7.2.tgz", + "integrity": "sha512-sXXKG+uL9IrKqViTtao2Ws6dy0znu9sOaP1di/jKGW1M6VssO8vlpXCQcpZ+jisQ1tTFAC5Jo/EOzFbggBagFQ==" + }, "node_modules/@soda/friendly-errors-webpack-plugin": { "version": "1.8.1", "resolved": "https://registry.npmjs.org/@soda/friendly-errors-webpack-plugin/-/friendly-errors-webpack-plugin-1.8.1.tgz", @@ -2991,7 +3026,6 @@ "version": "8.37.0", "resolved": "https://registry.npmjs.org/@types/eslint/-/eslint-8.37.0.tgz", "integrity": "sha512-Piet7dG2JBuDIfohBngQ3rCt7MgO9xCO4xIMKxBThCq5PNRB91IjlJ10eJVwfoNtvTErmxLzwBZ7rHZtbOMmFQ==", - "devOptional": true, "dependencies": { "@types/estree": "*", @@ -3138,7 +3172,6 @@ "version": "14.18.42", "resolved": "https://registry.npmjs.org/@types/node/-/node-14.18.42.tgz", "integrity": "sha512-xefu+RBie4xWlK8hwAzGh3npDz/4VhF6icY/shU+zv/1fNn+ZVG7T7CRwe9LId9sAYRPxI+59QBPuKL3WpyGRg==", - "devOptional": true }, "node_modules/@types/normalize-package-data": { @@ -4570,7 +4603,6 @@ "version": "5.0.1", "resolved": "https://registry.npmjs.org/ansi-regex/-/ansi-regex-5.0.1.tgz", "integrity": "sha512-quJQXlTSUGL2LH9SUXo8VwsY4soanhgo6LNSm84E1LBcE8s3O0wpdiRzyR9z/ZZJMlMWv37qOOb9pdJlMUEKFQ==", - "dev": true, "engines": { "node": ">=8" } @@ -5393,7 +5425,6 @@ "version": "1.0.30001474", "resolved": "https://registry.npmjs.org/caniuse-lite/-/caniuse-lite-1.0.30001474.tgz", "integrity": "sha512-iaIZ8gVrWfemh5DG3T9/YqarVZoYf0r188IjaGwx68j4Pf0SGY6CQkmJUIE+NZHkkecQGohzXmBGEwWDr9aM3Q==", - "devOptional": true, "funding": [ { @@ -6445,6 +6476,15 @@ "callsite": "^1.0.0" } }, + "node_modules/decamelize": { + "version": "1.2.0", + "resolved": "https://registry.npmjs.org/decamelize/-/decamelize-1.2.0.tgz", + "integrity": "sha512-z2S+W9X73hAUUki+N+9Za2lBlun89zigOyGrsax+KUQ6wKW4ZoWpEYBkGhQjwAjjDCkWxhY0VKEhk8wzY7F5cA==", + "peer": true, + "engines": { + "node": ">=0.10.0" + } + }, "node_modules/decimal.js": { "version": "10.4.3", "resolved": "https://registry.npmjs.org/decimal.js/-/decimal.js-10.4.3.tgz", @@ -6650,6 +6690,12 @@ "node": "^10.13.0 || ^12.13.0 || ^14.15.0 || >=15.0.0" } }, + "node_modules/dijkstrajs": { + "version": "1.0.3", + "resolved": "https://registry.npmjs.org/dijkstrajs/-/dijkstrajs-1.0.3.tgz", + "integrity": "sha512-qiSlmBq9+BCdCA/L46dw8Uy93mloxsPSbwnm5yrKn2vMPiy8KyAskTF6zuV/j5BMsmOGZDPs7KjU+mjb670kfA==", + "peer": true + }, "node_modules/dir-glob": { "version": "3.0.1", "resolved": "https://registry.npmjs.org/dir-glob/-/dir-glob-3.0.1.tgz", @@ -6892,7 +6938,6 @@ "version": "1.4.351", "resolved": "https://registry.npmjs.org/electron-to-chromium/-/electron-to-chromium-1.4.351.tgz", "integrity": "sha512-W35n4jAsyj6OZGxeWe+gA6+2Md4jDO19fzfsRKEt3DBwIdlVTT8O9Uv8ojgUAoQeXASdgG9zMU+8n8Xg/W6dRQ==", - "devOptional": true }, "node_modules/emittery": { @@ -6910,8 +6955,7 @@ "node_modules/emoji-regex": { "version": "8.0.0", "resolved": "https://registry.npmjs.org/emoji-regex/-/emoji-regex-8.0.0.tgz", - "integrity": "sha512-MSjYzcWNOA0ewAHpz0MxpYFvwg6yjy1NG3xteoqz644VCo/RPgnr1/GGt+ic3iJTzQ8Eu3TdM14SawnVUmGE6A==", - "dev": true + "integrity": "sha512-MSjYzcWNOA0ewAHpz0MxpYFvwg6yjy1NG3xteoqz644VCo/RPgnr1/GGt+ic3iJTzQ8Eu3TdM14SawnVUmGE6A==" }, "node_modules/emojis-list": { "version": "3.0.0", @@ -6922,6 +6966,12 @@ "node": ">= 4" } }, + "node_modules/encode-utf8": { + "version": "1.0.3", + "resolved": "https://registry.npmjs.org/encode-utf8/-/encode-utf8-1.0.3.tgz", + "integrity": "sha512-ucAnuBEhUK4boH2HjVYG5Q2mQyPorvv0u/ocS+zhdw0S8AlHYY+GOFhP1Gio5z4icpP2ivFSvhtFjQi8+T9ppw==", + "peer": true + }, "node_modules/encodeurl": { "version": "1.0.2", "resolved": "https://registry.npmjs.org/encodeurl/-/encodeurl-1.0.2.tgz", @@ -6945,7 +6995,6 @@ "resolved": "https://registry.npmjs.org/enhanced-resolve/-/enhanced-resolve-0.9.1.tgz", "integrity": "sha512-kxpoMgrdtkXZ5h0SeraBS1iRntpTpQ3R8ussdb38+UAFnMGX5DDyJXePm+OCHOcoXvHDw7mc2erbJBpDnl7TPw==", "dev": true, - "dependencies": { "graceful-fs": "^4.1.2", "memory-fs": "^0.2.0", @@ -8305,7 +8354,6 @@ "version": "4.1.0", "resolved": "https://registry.npmjs.org/find-up/-/find-up-4.1.0.tgz", "integrity": "sha512-PpOwAdQ/YlXQ2vj8a3h8IipDuYRi3wceVQQGYWxNINccq40Anw7BlsEXCMbt1Zt+OLA6Fq9suIpIWD0OsnISlw==", - "devOptional": true, "dependencies": { "locate-path": "^5.0.0", "path-exists": "^4.0.0" @@ -8669,7 +8717,6 @@ "version": "2.0.5", "resolved": "https://registry.npmjs.org/get-caller-file/-/get-caller-file-2.0.5.tgz", "integrity": "sha512-DyFP3BM/3YHTQOCUL/w0OZHR0lpKeGrxotcHWcqNEdnltqFwXVfhEBQ94eIo34AfQpo0rGki4cyIiftY06h2Fg==", - "dev": true, "engines": { "node": "6.* || 8.* || >= 10.*" } @@ -8838,7 +8885,6 @@ "version": "4.2.11", "resolved": "https://registry.npmjs.org/graceful-fs/-/graceful-fs-4.2.11.tgz", "integrity": "sha512-RbJ5/jmFcNNCcDV5o9eTnBLJ/HszWV0P73bc+Ff4nS/rJj+YaS6IGyiOL0VoBYX+l1Wrl3k63h/KrH+nhJ0XvQ==", - "devOptional": true }, "node_modules/gzip-size": { @@ -9541,7 +9587,6 @@ "version": "3.0.0", "resolved": "https://registry.npmjs.org/is-fullwidth-code-point/-/is-fullwidth-code-point-3.0.0.tgz", "integrity": "sha512-zymm5+u+sCsSWyD9qNaejV3DFvhCKclKdizYaJUuHA83RLjb7nSuGnddCHGv0hk+KY7BMAlsWeK4Ueg6EV6XQg==", - "dev": true, "engines": { "node": ">=8" } @@ -12421,6 +12466,12 @@ "integrity": "sha512-Bdboy+l7tA3OGW6FjyFHWkP5LuByj1Tk33Ljyq0axyzdk9//JSi2u3fP1QSmd1KNwq6VOKYGlAu87CisVir6Pw==", "dev": true }, + "node_modules/json-stringify-safe": { + "version": "5.0.1", + "resolved": "https://registry.npmjs.org/json-stringify-safe/-/json-stringify-safe-5.0.1.tgz", + "integrity": "sha512-ZClg6AaYvamvYEE82d3Iyd3vSSIjQ+odgjaTzRuO3s7toCdFKczob2i0zCh7JE8kWn17yvAWhUVxvqGwUalsRA==", + "dev": true + }, "node_modules/json5": { "version": "2.2.3", "resolved": "https://registry.npmjs.org/json5/-/json5-2.2.3.tgz", @@ -12454,6 +12505,11 @@ "node": ">=0.10.0" } }, + "node_modules/just-extend": { + "version": "4.2.1", + "resolved": "https://registry.npmjs.org/just-extend/-/just-extend-4.2.1.tgz", + "integrity": "sha512-g3UB796vUFIY90VIv/WX3L2c8CS2MdWUww3CNrYmqza1Fg0DURc2K/O4YrnklBdQarSJ/y8JnJYDGc+1iumQjg==" + }, "node_modules/kind-of": { "version": "3.2.2", "resolved": "https://registry.npmjs.org/kind-of/-/kind-of-3.2.2.tgz", @@ -12579,7 +12635,6 @@ "version": "5.0.0", "resolved": "https://registry.npmjs.org/locate-path/-/locate-path-5.0.0.tgz", "integrity": "sha512-t7hw9pI+WvuwNJXwk5zVHpyhIqzg2qTlklJOf0mVxGSbe3Fp2VieZcduNYjaLDoy6p9uGpQEGWG87WpMKlNq8g==", - "devOptional": true, "dependencies": { "p-locate": "^4.1.0" }, @@ -12604,6 +12659,11 @@ "integrity": "sha512-3j8wdDzYuWO3lM3Reg03MuQR957t287Rpcxp1njpEa8oDrikb+FwGdW3n+FELh/A6qib6yPit0j/pv9G/yeAqA==", "dev": true }, + "node_modules/lodash.get": { + "version": "4.4.2", + "resolved": "https://registry.npmjs.org/lodash.get/-/lodash.get-4.4.2.tgz", + "integrity": "sha512-z+Uw/vLuy6gQe8cfaFWD7p0wVv8fJl3mbzXh33RS+0oW2wvUqiRXiQ69gLWSLpgB5/6sU+r6BlQR0MBILadqTQ==" + }, "node_modules/lodash.kebabcase": { "version": "4.1.1", "resolved": "https://registry.npmjs.org/lodash.kebabcase/-/lodash.kebabcase-4.1.1.tgz", @@ -13258,6 +13318,47 @@ "integrity": "sha512-1nh45deeb5olNY7eX82BkPO7SSxR5SSYJiPTrTdFUVYwAl8CKMA5N9PjTYkHiRjisVcxcQ1HXdLhx2qxxJzLNQ==", "dev": true }, + "node_modules/nise": { + "version": "5.1.4", + "resolved": "https://registry.npmjs.org/nise/-/nise-5.1.4.tgz", + "integrity": "sha512-8+Ib8rRJ4L0o3kfmyVCL7gzrohyDe0cMFTBa2d364yIrEGMEoetznKJx899YxjybU6bL9SQkYPSBBs1gyYs8Xg==", + "dependencies": { + "@sinonjs/commons": "^2.0.0", + "@sinonjs/fake-timers": "^10.0.2", + "@sinonjs/text-encoding": "^0.7.1", + "just-extend": "^4.0.2", + "path-to-regexp": "^1.7.0" + } + }, + "node_modules/nise/node_modules/@sinonjs/commons": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/@sinonjs/commons/-/commons-2.0.0.tgz", + "integrity": "sha512-uLa0j859mMrg2slwQYdO/AkrOfmH+X6LTVmNTS9CqexuE2IvVORIkSpJLqePAbEnKJ77aMmCwr1NUZ57120Xcg==", + "dependencies": { + "type-detect": "4.0.8" + } + }, + "node_modules/nise/node_modules/@sinonjs/fake-timers": { + "version": "10.0.2", + "resolved": "https://registry.npmjs.org/@sinonjs/fake-timers/-/fake-timers-10.0.2.tgz", + "integrity": "sha512-SwUDyjWnah1AaNl7kxsa7cfLhlTYoiyhDAIgyh+El30YvXs/o7OLXpYH88Zdhyx9JExKrmHDJ+10bwIcY80Jmw==", + "dependencies": { + "@sinonjs/commons": "^2.0.0" + } + }, + "node_modules/nise/node_modules/isarray": { + "version": "0.0.1", + "resolved": "https://registry.npmjs.org/isarray/-/isarray-0.0.1.tgz", + "integrity": "sha512-D2S+3GLxWH+uhrNEcoh/fnmYeP8E8/zHl644d/jdA0g2uyXvy3sb0qxotE+ne0LtccHknQzWwZEzhak7oJ0COQ==" + }, + "node_modules/nise/node_modules/path-to-regexp": { + "version": "1.8.0", + "resolved": "https://registry.npmjs.org/path-to-regexp/-/path-to-regexp-1.8.0.tgz", + "integrity": "sha512-n43JRhlUKUAlibEJhPeir1ncUID16QnEjNpwzNdO3Lm4ywrBpBZ5oLD0I6br9evr1Y9JTqwRtAh7JLoOzAQdVA==", + "dependencies": { + "isarray": "0.0.1" + } + }, "node_modules/no-case": { "version": "3.0.4", "resolved": "https://registry.npmjs.org/no-case/-/no-case-3.0.4.tgz", @@ -13268,6 +13369,21 @@ "tslib": "^2.0.3" } }, + "node_modules/nock": { + "version": "13.3.1", + "resolved": "https://registry.npmjs.org/nock/-/nock-13.3.1.tgz", + "integrity": "sha512-vHnopocZuI93p2ccivFyGuUfzjq2fxNyNurp7816mlT5V5HF4SzXu8lvLrVzBbNqzs+ODooZ6OksuSUNM7Njkw==", + "dev": true, + "dependencies": { + "debug": "^4.1.0", + "json-stringify-safe": "^5.0.1", + "lodash": "^4.17.21", + "propagate": "^2.0.0" + }, + "engines": { + "node": ">= 10.13" + } + }, "node_modules/node-fetch": { "version": "2.6.9", "resolved": "https://registry.npmjs.org/node-fetch/-/node-fetch-2.6.9.tgz", @@ -13750,7 +13866,6 @@ "version": "2.3.0", "resolved": "https://registry.npmjs.org/p-limit/-/p-limit-2.3.0.tgz", "integrity": "sha512-//88mFWSJx8lxCzwdAABTJL2MyWB12+eIY7MDL2SqLmAkeKU9qxRvWuSyTjm3FUmpBEMuFfckAIqEaVGUDxb6w==", - "devOptional": true, "dependencies": { "p-try": "^2.0.0" }, @@ -13765,7 +13880,6 @@ "version": "4.1.0", "resolved": "https://registry.npmjs.org/p-locate/-/p-locate-4.1.0.tgz", "integrity": "sha512-R79ZZ/0wAxKGu3oYMlz8jy/kbhsNrS7SKZ7PxEHBgJ5+F2mtFW2fK2cOtBh1cHYkQsbzFV7I+EoRKe6Yt0oK7A==", - "devOptional": true, "dependencies": { "p-limit": "^2.2.0" }, @@ -13790,7 +13904,6 @@ "version": "2.2.0", "resolved": "https://registry.npmjs.org/p-try/-/p-try-2.2.0.tgz", "integrity": "sha512-R4nPAVTAU0B9D35/Gk3uJf/7XYbQcyohSKdvAxIRSNghFl4e71hVoGnBNQz9cWaXxO2I10KTC+3jMdvvoKw6dQ==", - "devOptional": true, "engines": { "node": ">=6" } @@ -13879,7 +13992,6 @@ "version": "4.0.0", "resolved": "https://registry.npmjs.org/path-exists/-/path-exists-4.0.0.tgz", "integrity": "sha512-ak9Qy5Q7jYb2Wwcey5Fpvg2KoAc/ZIhLSLOSBmRmygPsGwkVVt0fZa0qrtMz+m6tJTAHfZQ8FnmB4MG4LWy7/w==", - "devOptional": true, "engines": { "node": ">=8" } @@ -13961,6 +14073,15 @@ "node": ">=8" } }, + "node_modules/pngjs": { + "version": "5.0.0", + "resolved": "https://registry.npmjs.org/pngjs/-/pngjs-5.0.0.tgz", + "integrity": "sha512-40QW5YalBNfQo5yRYmiw7Yz6TKKVr3h6970B2YE+3fQpsWcrbj1PzJgxeJ19DRQjhMbKPIuMY8rFaXc8moolVw==", + "peer": true, + "engines": { + "node": ">=10.13.0" + } + }, "node_modules/portfinder": { "version": "1.0.32", "resolved": "https://registry.npmjs.org/portfinder/-/portfinder-1.0.32.tgz", @@ -14693,6 +14814,15 @@ "node": ">= 6" } }, + "node_modules/propagate": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/propagate/-/propagate-2.0.1.tgz", + "integrity": "sha512-vGrhOavPSTz4QVNuBNdcNXePNdNMaO1xj9yBeH1ScQPjk/rhg9sSlCXPhMkFuaNNW/syTvYqsnbIJxMBfRbbag==", + "dev": true, + "engines": { + "node": ">= 8" + } + }, "node_modules/proto-list": { "version": "1.2.4", "resolved": "https://registry.npmjs.org/proto-list/-/proto-list-1.2.4.tgz", @@ -14752,6 +14882,132 @@ "node": ">=6" } }, + "node_modules/qrcode": { + "version": "1.5.3", + "resolved": "https://registry.npmjs.org/qrcode/-/qrcode-1.5.3.tgz", + "integrity": "sha512-puyri6ApkEHYiVl4CFzo1tDkAZ+ATcnbJrJ6RiBM1Fhctdn/ix9MTE3hRph33omisEbC/2fcfemsseiKgBPKZg==", + "peer": true, + "dependencies": { + "dijkstrajs": "^1.0.1", + "encode-utf8": "^1.0.3", + "pngjs": "^5.0.0", + "yargs": "^15.3.1" + }, + "bin": { + "qrcode": "bin/qrcode" + }, + "engines": { + "node": ">=10.13.0" + } + }, + "node_modules/qrcode/node_modules/ansi-styles": { + "version": "4.3.0", + "resolved": "https://registry.npmjs.org/ansi-styles/-/ansi-styles-4.3.0.tgz", + "integrity": "sha512-zbB9rCJAT1rbjiVDb2hqKFHNYLxgtk8NURxZ3IZwD3F6NtxbXZQCnnSi1Lkx+IDohdPlFp222wVALIheZJQSEg==", + "peer": true, + "dependencies": { + "color-convert": "^2.0.1" + }, + "engines": { + "node": ">=8" + }, + "funding": { + "url": "https://github.com/chalk/ansi-styles?sponsor=1" + } + }, + "node_modules/qrcode/node_modules/camelcase": { + "version": "5.3.1", + "resolved": "https://registry.npmjs.org/camelcase/-/camelcase-5.3.1.tgz", + "integrity": "sha512-L28STB170nwWS63UjtlEOE3dldQApaJXZkOI1uMFfzf3rRuPegHaHesyee+YxQ+W6SvRDQV6UrdOdRiR153wJg==", + "peer": true, + "engines": { + "node": ">=6" + } + }, + "node_modules/qrcode/node_modules/cliui": { + "version": "6.0.0", + "resolved": "https://registry.npmjs.org/cliui/-/cliui-6.0.0.tgz", + "integrity": "sha512-t6wbgtoCXvAzst7QgXxJYqPt0usEfbgQdftEPbLL/cvv6HPE5VgvqCuAIDR0NgU52ds6rFwqrgakNLrHEjCbrQ==", + "peer": true, + "dependencies": { + "string-width": "^4.2.0", + "strip-ansi": "^6.0.0", + "wrap-ansi": "^6.2.0" + } + }, + "node_modules/qrcode/node_modules/color-convert": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/color-convert/-/color-convert-2.0.1.tgz", + "integrity": "sha512-RRECPsj7iu/xb5oKYcsFHSppFNnsj/52OVTRKb4zP5onXwVF3zVmmToNcOfGC+CRDpfK/U584fMg38ZHCaElKQ==", + "peer": true, + "dependencies": { + "color-name": "~1.1.4" + }, + "engines": { + "node": ">=7.0.0" + } + }, + "node_modules/qrcode/node_modules/color-name": { + "version": "1.1.4", + "resolved": "https://registry.npmjs.org/color-name/-/color-name-1.1.4.tgz", + "integrity": "sha512-dOy+3AuW3a2wNbZHIuMZpTcgjGuLU/uBL/ubcZF9OXbDo8ff4O8yVp5Bf0efS8uEoYo5q4Fx7dY9OgQGXgAsQA==", + "peer": true + }, + "node_modules/qrcode/node_modules/wrap-ansi": { + "version": "6.2.0", + "resolved": "https://registry.npmjs.org/wrap-ansi/-/wrap-ansi-6.2.0.tgz", + "integrity": "sha512-r6lPcBGxZXlIcymEu7InxDMhdW0KDxpLgoFLcguasxCaJ/SOIZwINatK9KY/tf+ZrlywOKU0UDj3ATXUBfxJXA==", + "peer": true, + "dependencies": { + "ansi-styles": "^4.0.0", + "string-width": "^4.1.0", + "strip-ansi": "^6.0.0" + }, + "engines": { + "node": ">=8" + } + }, + "node_modules/qrcode/node_modules/y18n": { + "version": "4.0.3", + "resolved": "https://registry.npmjs.org/y18n/-/y18n-4.0.3.tgz", + "integrity": "sha512-JKhqTOwSrqNA1NY5lSztJ1GrBiUodLMmIZuLiDaMRJ+itFd+ABVE8XBjOvIWL+rSqNDC74LCSFmlb/U4UZ4hJQ==", + "peer": true + }, + "node_modules/qrcode/node_modules/yargs": { + "version": "15.4.1", + "resolved": "https://registry.npmjs.org/yargs/-/yargs-15.4.1.tgz", + "integrity": "sha512-aePbxDmcYW++PaqBsJ+HYUFwCdv4LVvdnhBy78E57PIor8/OVvhMrADFFEDh8DHDFRv/O9i3lPhsENjO7QX0+A==", + "peer": true, + "dependencies": { + "cliui": "^6.0.0", + "decamelize": "^1.2.0", + "find-up": "^4.1.0", + "get-caller-file": "^2.0.1", + "require-directory": "^2.1.1", + "require-main-filename": "^2.0.0", + "set-blocking": "^2.0.0", + "string-width": "^4.2.0", + "which-module": "^2.0.0", + "y18n": "^4.0.0", + "yargs-parser": "^18.1.2" + }, + "engines": { + "node": ">=8" + } + }, + "node_modules/qrcode/node_modules/yargs-parser": { + "version": "18.1.3", + "resolved": "https://registry.npmjs.org/yargs-parser/-/yargs-parser-18.1.3.tgz", + "integrity": "sha512-o50j0JeToy/4K6OZcaQmW6lyXXKhq7csREXcDwk2omFPJEwUNOVtJKvmDr9EI1fAJZUyZcRF7kxGBWmRXudrCQ==", + "peer": true, + "dependencies": { + "camelcase": "^5.0.0", + "decamelize": "^1.2.0" + }, + "engines": { + "node": ">=6" + } + }, "node_modules/qs": { "version": "6.11.0", "resolved": "https://registry.npmjs.org/qs/-/qs-6.11.0.tgz", @@ -15060,7 +15316,6 @@ "version": "2.1.1", "resolved": "https://registry.npmjs.org/require-directory/-/require-directory-2.1.1.tgz", "integrity": "sha512-fGxEI7+wsG9xrvdjsrlmL22OMTTiHRwAMroiEeMgq8gzoLC/PQr7RsRDSTLUg/bZAZtF+TVIkHc6/4RIKrui+Q==", - "dev": true, "engines": { "node": ">=0.10.0" } @@ -15074,6 +15329,12 @@ "node": ">=0.10.0" } }, + "node_modules/require-main-filename": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/require-main-filename/-/require-main-filename-2.0.0.tgz", + "integrity": "sha512-NKN5kMDylKuldxYLSUfrbo5Tuzh4hd+2E8NPPX02mZtn1VuREQToYe/ZdlJy+J3uCpfaiGF05e7B8W0iXbQHmg==", + "peer": true + }, "node_modules/requires-port": { "version": "1.0.0", "resolved": "https://registry.npmjs.org/requires-port/-/requires-port-1.0.0.tgz", @@ -15658,6 +15919,12 @@ "node": ">= 0.8.0" } }, + "node_modules/set-blocking": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/set-blocking/-/set-blocking-2.0.0.tgz", + "integrity": "sha512-KiKBS8AnWGEyLzofFfmvKwpdPzqiy16LvQfK3yv/fVH7Bj13/wl3JSR1J+rfgRE9q7xUJK4qvgS8raSOeLUehw==", + "peer": true + }, "node_modules/setprototypeof": { "version": "1.2.0", "resolved": "https://registry.npmjs.org/setprototypeof/-/setprototypeof-1.2.0.tgz", @@ -15758,6 +16025,74 @@ "integrity": "sha512-wnD2ZE+l+SPC/uoS0vXeE9L1+0wuaMqKlfz9AMUo38JsyLSBWSFcHR1Rri62LZc12vLr1gb3jl7iwQhgwpAbGQ==", "dev": true }, + "node_modules/sinon": { + "version": "15.0.4", + "resolved": "https://registry.npmjs.org/sinon/-/sinon-15.0.4.tgz", + "integrity": "sha512-uzmfN6zx3GQaria1kwgWGeKiXSSbShBbue6Dcj0SI8fiCNFbiUDqKl57WFlY5lyhxZVUKmXvzgG2pilRQCBwWg==", + "dependencies": { + "@sinonjs/commons": "^3.0.0", + "@sinonjs/fake-timers": "^10.0.2", + "@sinonjs/samsam": "^8.0.0", + "diff": "^5.1.0", + "nise": "^5.1.4", + "supports-color": "^7.2.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/sinon" + } + }, + "node_modules/sinon/node_modules/@sinonjs/commons": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/@sinonjs/commons/-/commons-3.0.0.tgz", + "integrity": "sha512-jXBtWAF4vmdNmZgD5FoKsVLv3rPgDnLgPbU84LIJ3otV44vJlDRokVng5v8NFJdCf/da9legHcKaRuZs4L7faA==", + "dependencies": { + "type-detect": "4.0.8" + } + }, + "node_modules/sinon/node_modules/@sinonjs/fake-timers": { + "version": "10.0.2", + "resolved": "https://registry.npmjs.org/@sinonjs/fake-timers/-/fake-timers-10.0.2.tgz", + "integrity": "sha512-SwUDyjWnah1AaNl7kxsa7cfLhlTYoiyhDAIgyh+El30YvXs/o7OLXpYH88Zdhyx9JExKrmHDJ+10bwIcY80Jmw==", + "dependencies": { + "@sinonjs/commons": "^2.0.0" + } + }, + "node_modules/sinon/node_modules/@sinonjs/fake-timers/node_modules/@sinonjs/commons": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/@sinonjs/commons/-/commons-2.0.0.tgz", + "integrity": "sha512-uLa0j859mMrg2slwQYdO/AkrOfmH+X6LTVmNTS9CqexuE2IvVORIkSpJLqePAbEnKJ77aMmCwr1NUZ57120Xcg==", + "dependencies": { + "type-detect": "4.0.8" + } + }, + "node_modules/sinon/node_modules/diff": { + "version": "5.1.0", + "resolved": "https://registry.npmjs.org/diff/-/diff-5.1.0.tgz", + "integrity": "sha512-D+mk+qE8VC/PAUrlAU34N+VfXev0ghe5ywmpqrawphmVZc1bEfn56uo9qpyGp1p4xpzOHkSW4ztBd6L7Xx4ACw==", + "engines": { + "node": ">=0.3.1" + } + }, + "node_modules/sinon/node_modules/has-flag": { + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/has-flag/-/has-flag-4.0.0.tgz", + "integrity": "sha512-EykJT/Q1KjTWctppgIAgfSO0tKVuZUjhgMr17kqTumMl6Afv3EISleU7qZUzoXDFTAHTDC4NOoG/ZxU3EvlMPQ==", + "engines": { + "node": ">=8" + } + }, + "node_modules/sinon/node_modules/supports-color": { + "version": "7.2.0", + "resolved": "https://registry.npmjs.org/supports-color/-/supports-color-7.2.0.tgz", + "integrity": "sha512-qpCAvRl9stuOHveKsn7HncJRvv501qIacKzQlO/+Lwxc9+0q2wLyv4Dfvt80/DPn2pqOBsJdDiogXGR9+OvwRw==", + "dependencies": { + "has-flag": "^4.0.0" + }, + "engines": { + "node": ">=8" + } + }, "node_modules/sirv": { "version": "1.0.19", "resolved": "https://registry.npmjs.org/sirv/-/sirv-1.0.19.tgz", @@ -16046,7 +16381,6 @@ "version": "4.2.3", "resolved": "https://registry.npmjs.org/string-width/-/string-width-4.2.3.tgz", "integrity": "sha512-wKyQRQpjJ0sIp62ErSZdGsjMJWsap5oRNihHhu6G7JVO/9jIB6UyevL+tXuOqrng8j/cxKTWyWUwvSTriiZz/g==", - "dev": true, "dependencies": { "emoji-regex": "^8.0.0", "is-fullwidth-code-point": "^3.0.0", @@ -16138,7 +16472,6 @@ "version": "6.0.1", "resolved": "https://registry.npmjs.org/strip-ansi/-/strip-ansi-6.0.1.tgz", "integrity": "sha512-Y38VPSHcqkFrCpFnQ9vuSXmquuv5oXOKpGeT6aGrr3o3Gc9AlVa6JBfUSOCnbxGGZF+/0ooI7KrPuUSztUdU5A==", - "dev": true, "dependencies": { "ansi-regex": "^5.0.1" }, @@ -16378,7 +16711,6 @@ "resolved": "https://registry.npmjs.org/tapable/-/tapable-0.1.10.tgz", "integrity": "sha512-jX8Et4hHg57mug1/079yitEKWGB3LCwoxByLsNim89LABq8NqgiX+6iYVOsq0vX8uJHkU+DZ5fnq95f800bEsQ==", "dev": true, - "engines": { "node": ">=0.6" } @@ -16454,7 +16786,6 @@ "version": "5.16.8", "resolved": "https://registry.npmjs.org/terser/-/terser-5.16.8.tgz", "integrity": "sha512-QI5g1E/ef7d+PsDifb+a6nnVgC4F22Bg6T0xrBrz6iloVB4PUkkunp6V8nzoOOZJIzjWVdAGqCdlKlhLq/TbIA==", - "devOptional": true, "dependencies": { "@jridgewell/source-map": "^0.3.2", @@ -16473,7 +16804,6 @@ "version": "5.3.7", "resolved": "https://registry.npmjs.org/terser-webpack-plugin/-/terser-webpack-plugin-5.3.7.tgz", "integrity": "sha512-AfKwIktyP7Cu50xNjXF/6Qb5lBNzYaWpU6YfoX3uZicTx0zTy0stDDCsvjDapKsSDvOeWo5MEq4TmdBy2cNoHw==", - "devOptional": true, "dependencies": { "@jridgewell/trace-mapping": "^0.3.17", @@ -17053,7 +17383,6 @@ "version": "4.0.8", "resolved": "https://registry.npmjs.org/type-detect/-/type-detect-4.0.8.tgz", "integrity": "sha512-0fr/mIH1dlO+x7TlcMy+bIDqKPsw/70tVyeHW787goQjhmqaZe10uwLujubK9q9Lg6Fiho1KUKDYz0Z7k7g5/g==", - "dev": true, "engines": { "node": ">=4" } @@ -17765,7 +18094,6 @@ "version": "5.77.0", "resolved": "https://registry.npmjs.org/webpack/-/webpack-5.77.0.tgz", "integrity": "sha512-sbGNjBr5Ya5ss91yzjeJTLKyfiwo5C628AFjEa6WSXcZa4E+F57om3Cc8xLb1Jh0b243AWuSYRf3dn7HVeFQ9Q==", - "devOptional": true, "dependencies": { "@types/eslint-scope": "^3.7.3", @@ -18356,6 +18684,12 @@ "url": "https://github.com/sponsors/ljharb" } }, + "node_modules/which-module": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/which-module/-/which-module-2.0.1.tgz", + "integrity": "sha512-iBdZ57RDvnOR9AGBhML2vFZf7h8vmBjhoaZqODJBFWHVtKkDmKuHai3cx5PgVMrX5YDNp27AofYbAwctSS+vhQ==", + "peer": true + }, "node_modules/which-typed-array": { "version": "1.1.9", "resolved": "https://registry.npmjs.org/which-typed-array/-/which-typed-array-1.1.9.tgz", diff --git a/frontend/package.json b/frontend/package.json index c939b05b..90faee85 100644 --- a/frontend/package.json +++ b/frontend/package.json @@ -5,16 +5,18 @@ "scripts": { "serve": "vue-cli-service serve", "build": "vue-cli-service build", - "test:unit": "vue-cli-service test:unit", + "test:unit": "vue-cli-service test:unit --silent", "lint": "vue-cli-service lint", "force": "npm install --force" }, "dependencies": { + "@chenfengyuan/vue-qrcode": "^2.0.0", "@mdi/font": "5.9.55", "core-js": "^3.8.3", "echofetch": "file:src/api/EchoFetch", "register-service-worker": "^1.7.2", "roboto-fontface": "*", + "sinon": "^15.0.4", "tiny-emitter": "^2.1.0", "v-calendar": "^3.0.3", "vue": "^3.2.13", @@ -41,12 +43,13 @@ "@vue/eslint-config-standard": "^6.1.0", "@vue/test-utils": "^2.0.0-0", "@vue/vue3-jest": "^27.0.0-alpha.1", - "babel-jest": "^27.0.6", + "babel-jest": "^27.5.1", "eslint": "^7.32.0", "eslint-plugin-import": "^2.25.3", "eslint-plugin-node": "^11.1.0", "eslint-plugin-promise": "^5.1.0", "jest": "^27.0.5", + "nock": "^13.3.1", "ts-jest": "^27.0.4", "typescript": "~4.5.5", "vue-cli-plugin-vuetify": "~2.5.8", diff --git a/frontend/src/App.vue b/frontend/src/App.vue index 2e0ca2e0..862a57a0 100644 --- a/frontend/src/App.vue +++ b/frontend/src/App.vue @@ -22,7 +22,7 @@ const emitter = require('tiny-emitter/instance') export default defineComponent({ name: 'App', async beforeCreate() { - const noLogin = ['login', 'register', 'forgot']; // Pages that can be accessed without logging in + const noLogin = ['login', 'register', 'forgot', 'building_page']; // Pages that can be accessed without logging in const router = useRouter(); router.beforeEach( async (to, from, next) => { @@ -39,11 +39,12 @@ export default defineComponent({ this.navbar = true; if (!(config.AUTHORIZED[user.role].includes(to.name.toString()))) { - return next({path: '/unauthorized'}); + return next({name: 'unauthorized'}); } } else { if (user !== null) { this.navbar = true; + if (to.name.toString() === 'building_page') return next(); return next({path: '/'}); } this.navbar = false; diff --git a/frontend/src/api/DateUtil.js b/frontend/src/api/DateUtil.js new file mode 100644 index 00000000..ffe1a320 --- /dev/null +++ b/frontend/src/api/DateUtil.js @@ -0,0 +1,6 @@ +export function getWeek(date) { + var d = new Date(Date.UTC(date.getFullYear(), date.getMonth(), date.getDate())); + d.setUTCDate(d.getUTCDate() - d.getUTCDay()); + var yearStart = new Date(Date.UTC(d.getUTCFullYear(), 0, 1)); + return Math.ceil((((d - yearStart) / 86400000) + 1) / 7); +} diff --git a/frontend/src/api/error/ErrorHandler.ts b/frontend/src/api/error/ErrorHandler.ts index 56193b65..657d20c6 100644 --- a/frontend/src/api/error/ErrorHandler.ts +++ b/frontend/src/api/error/ErrorHandler.ts @@ -105,10 +105,7 @@ export class ErrorHandler { // Set the new error message, when available. if (fieldNewError) { fieldValue.error = fieldNewError.message; - } - - // Otherwise set an empty error. (necessary for reset of previous error) - else { + } else { // Otherwise set an empty error. (necessary for reset of previous error) fieldValue.error = ""; } } diff --git a/frontend/src/api/models/Building.ts b/frontend/src/api/models/Building.ts index 09a43169..772d74a8 100644 --- a/frontend/src/api/models/Building.ts +++ b/frontend/src/api/models/Building.ts @@ -1,5 +1,6 @@ import BuildingManual from "@/api/models/BuildingManual"; +// TODO fix, object not correct with what backend sends export default class Building { name: string; id: number; diff --git a/frontend/src/api/models/BuildingContainer.ts b/frontend/src/api/models/BuildingContainer.ts new file mode 100644 index 00000000..e42b9161 --- /dev/null +++ b/frontend/src/api/models/BuildingContainer.ts @@ -0,0 +1,6 @@ +import Building from "@/api/models/Building"; + +export default class BuildingContainer { + building: Building; + trash_ids: number[] +} diff --git a/frontend/src/api/models/BuildingInfo.ts b/frontend/src/api/models/BuildingInfo.ts index 9fd64950..ce115caa 100644 --- a/frontend/src/api/models/BuildingInfo.ts +++ b/frontend/src/api/models/BuildingInfo.ts @@ -2,4 +2,5 @@ export default class BuildingInfo { id: number; remark: string; dagPlanning: number; + building: number; } diff --git a/frontend/src/api/models/ContainerCollectionDay.ts b/frontend/src/api/models/ContainerCollectionDay.ts index 591de679..26a3f999 100644 --- a/frontend/src/api/models/ContainerCollectionDay.ts +++ b/frontend/src/api/models/ContainerCollectionDay.ts @@ -1,7 +1,7 @@ import {Weekday} from "@/api/models/Weekday"; export default class ContainerCollectionDay { - day: Weekday + day: String start_hour: string end_hour: string } diff --git a/frontend/src/api/models/ContainerType.ts b/frontend/src/api/models/ContainerType.ts index 2bd9e83f..18f4e26c 100644 --- a/frontend/src/api/models/ContainerType.ts +++ b/frontend/src/api/models/ContainerType.ts @@ -1,7 +1,25 @@ export enum ContainerType { - PMD = "PM", - REST = "RE", - PK = "PK", - GLAS = "GL", - GFT = "GF" + PMD , + REST , + PK, + GLAS, + GFT +} + +export function container_to_api(type: ContainerType): String { + return type.toString().substring(0,2) +} + +export function container_from_api(type: String): ContainerType { + if (type === "PM"){ + return ContainerType.PMD + } else if (type === "RE") { + return ContainerType.REST + } else if (type === "PK") { + return ContainerType.PK + } else if (type === "GL") { + return ContainerType.GLAS + } else { + return ContainerType.GFT + } } diff --git a/frontend/src/api/models/TrashContainer.ts b/frontend/src/api/models/TrashContainer.ts index 1eb46c65..e6d97509 100644 --- a/frontend/src/api/models/TrashContainer.ts +++ b/frontend/src/api/models/TrashContainer.ts @@ -3,5 +3,5 @@ import {ContainerType} from "@/api/models/ContainerType"; export default class TrashContainer { collection_day: ContainerCollectionDay; - type: ContainerType; + type: string; } diff --git a/frontend/src/api/models/TrashTemplate.ts b/frontend/src/api/models/TrashTemplate.ts index 5903c216..63dc319e 100644 --- a/frontend/src/api/models/TrashTemplate.ts +++ b/frontend/src/api/models/TrashTemplate.ts @@ -1,5 +1,6 @@ import {TrashTemplateStatus} from "@/api/models/TrashTemplateStatus"; import Building from "@/api/models/Building"; +import BuildingContainer from "@/api/models/BuildingContainer"; export default class TrashTemplate { id: number = 1; @@ -9,5 +10,5 @@ export default class TrashTemplate { year: number = 2000; week: number = 1; location: number = 0; - buildings: Building[] + buildings: BuildingContainer[] } diff --git a/frontend/src/api/models/TrashTemplateStatus.ts b/frontend/src/api/models/TrashTemplateStatus.ts index f6a45979..fa5e24c6 100644 --- a/frontend/src/api/models/TrashTemplateStatus.ts +++ b/frontend/src/api/models/TrashTemplateStatus.ts @@ -1,6 +1,6 @@ export enum TrashTemplateStatus { - eenmalig = "Eenmalig", - vervangen = "Vervangen", - actief = "Actief", - inactief = "Inactief" + eenmalig = "E", + vervangen = "V", + actief = "A", + inactief = "I" } diff --git a/frontend/src/api/models/Weekday.ts b/frontend/src/api/models/Weekday.ts index 27d056fc..6dbe746e 100644 --- a/frontend/src/api/models/Weekday.ts +++ b/frontend/src/api/models/Weekday.ts @@ -1,9 +1,46 @@ export enum Weekday { - MONDAY = "MO", - TUESDAY = "TU", - WEDNESDAY = "WE", - THURSDAY = "TH", - FRIDAY = "FR", - SATURDAY = "SA", - SUNDAY = "SU" + Maandag, + Dinsdag, + Woensdag, + Donderdag, + Vrijdag, + Zaterdag, + Zondag } + +export function weekday_to_api(type: string): String { + if (type === 'Maandag' ){ + return "MO" + } else if (type === 'Dinsdag') { + return "TU" + } else if (type === 'Woensdag') { + return "WE" + } else if (type === 'Donderdag') { + return "TH" + } else if (type === 'Vrijdag') { + return "FR" + } else if (type === 'Zaterdag') { + return "SA" + } else if (type === 'Zondag') { + return "SU" + } +} + +export function weekday_from_api(type: String): Weekday { + if (type === "MO"){ + return Weekday.Maandag + } else if (type === "TU") { + return Weekday.Dinsdag + } else if (type === "WE") { + return Weekday.Woensdag + } else if (type === "TH") { + return Weekday.Donderdag + } else if (type === "FR") { + return Weekday.Vrijdag + } else if (type === "SA") { + return Weekday.Zaterdag + } else if (type === "SU") { + return Weekday.Zondag + } +} + diff --git a/frontend/src/api/services/AuthService.ts b/frontend/src/api/services/AuthService.ts index 9a043ff7..be9ce928 100644 --- a/frontend/src/api/services/AuthService.ts +++ b/frontend/src/api/services/AuthService.ts @@ -3,6 +3,7 @@ import { EchoPromise, EchoService, EchoServiceBuilder, + PATCH, POST, } from 'echofetch'; import { ErrorHandler } from "@/api/error/ErrorHandler"; @@ -81,7 +82,7 @@ class AuthService extends EchoService { store.dispatch("session/clear"); await store.dispatch("session/fetch"); - await router.push(goHome ? "/" : "/login"); + await router.push(goHome ? {name: 'home'} : {name: 'login'}); }) .catch((error) => { ErrorHandler.handle(error, { @@ -95,7 +96,7 @@ class AuthService extends EchoService { /** * Change the role of a user */ - @POST('/role/') + @PATCH('/role/') updateRoleOfUser(@Body() body : {}) : EchoPromise { return {} as EchoPromise } diff --git a/frontend/src/api/services/LocationService.ts b/frontend/src/api/services/LocationService.ts index 9bbb9a0f..c81aae89 100644 --- a/frontend/src/api/services/LocationService.ts +++ b/frontend/src/api/services/LocationService.ts @@ -1,4 +1,4 @@ -import {Body, EchoPromise, EchoService, EchoServiceBuilder, GET, POST} from "@/api/EchoFetch"; +import {Body, DELETE, EchoPromise, EchoService, EchoServiceBuilder, GET, Path, POST} from "@/api/EchoFetch"; import config from "@/config"; import Location from "@/api/models/Location"; import {InputFields} from "@/types/fields/InputFields"; @@ -20,6 +20,13 @@ class LocationService extends EchoService { return {} as EchoPromise } + /** + * Remove location + */ + @DELETE('/ronde/locatie/{id}') + deleteLocationById(@Path('id') id : number) : EchoPromise { + return {} as EchoPromise + } } export default new EchoServiceBuilder() diff --git a/frontend/src/api/services/MailTemplateService.ts b/frontend/src/api/services/MailTemplateService.ts index fc727885..8c2b4cfb 100644 --- a/frontend/src/api/services/MailTemplateService.ts +++ b/frontend/src/api/services/MailTemplateService.ts @@ -29,6 +29,15 @@ class MailTemplateService extends EchoService { return {} as EchoPromise } + /** + * Get all mail templates + */ + + @GET('/mailtemplates/') + getMailTemplates(): EchoPromise { + return {} as EchoPromise + } + /** * Update mail template */ diff --git a/frontend/src/api/services/PlanningService.ts b/frontend/src/api/services/PlanningService.ts index a67dee62..e1618e1e 100644 --- a/frontend/src/api/services/PlanningService.ts +++ b/frontend/src/api/services/PlanningService.ts @@ -18,11 +18,39 @@ class PlanningService extends EchoService { /** * Get a day planning */ - @GET("/dagplanning/{year}/{week}/{day}") + @GET("/dagplanning/{year}/{week}/{day}/") get(@Path('year') year: number, @Path('week') week: number, - @Path('day') day: number): EchoPromise { - return {} as EchoPromise; + @Path('day') day: number): EchoPromise<[DayPlanning]> { + return {} as EchoPromise<[DayPlanning]>; + } + + /** + * Get the statuses for a planning + */ + @GET("/dagplanning/{year}/{week}/{id}/status/") + getStatus(@Path('year') year: number, + @Path('week') week: number, + @Path('id') id: number): EchoPromise { + return {} as EchoPromise; + } + + /** + * Get the pictures for a planning + */ + @GET("/dagplanning/{year}/{week}/{id}/pictures/") + getStatusPictures(@Path('year') year: number, + @Path('week') week: number, + @Path('id') id: number): EchoPromise { + return {} as EchoPromise; + } + + /** + * Find the student template for a planning + */ + @GET("/studenttemplates/find/planning/{id}/") + findTemplate(@Path('id') id: number): EchoPromise { + return {} as EchoPromise; } /** @@ -41,6 +69,17 @@ class PlanningService extends EchoService { return {} as EchoPromise<[Round]>; } + @GET("/studenttemplates/rondes/{year}/{week}/{day}/{location}/") + getRoundFromBuilding( + @Path('year') year: number, + @Path('week') week: number, + @Path('day') day: number, + @Path('location') location: number, + building: number): EchoPromise { + return {} as EchoPromise; + } + + /** * Get building info for a day planning */ @@ -49,11 +88,39 @@ class PlanningService extends EchoService { return {} as EchoPromise; } + /** + * Get building info by id + */ + @GET("/infoperbuilding/{id}/") + getInfoById(@Path('id') id: number): EchoPromise { + return {} as EchoPromise + } + + /** + *Get building info for a day planning and building + */ + @GET("/infoperbuilding/") + getInfoOfBuilding(@Query('dagPlanning') dagPlanning: number, + @Query('building') building: number): EchoPromise { + return {} as EchoPromise + } + + + /** + * Get building info for a building and date + */ + @GET("/infoperbuilding/") + getInfoFromBuilding(@Query('building') building: number, @Query('date') date: string): EchoPromise { + return {} as EchoPromise; + } + /** * Get student images for building info */ @GET("/buildingpicture/") - getPictures(@Query('infoPerBuilding') infoPerBuilding: number): EchoPromise { + getPictures(@Query('infoPerBuilding') infoPerBuilding: number, + @Query('year') year: number, + @Query('week') week: number): EchoPromise { return {} as EchoPromise; } @@ -119,14 +186,6 @@ class PlanningService extends EchoService { return {} as EchoPromise; } - /** - * Update planning status - */ - @PATCH("/dagplanning/{id}/") - updatePlanningStatus(@Path('id') id: number, @Body() body: PlanningStatusWrapper) { - return {} as EchoPromise; - } - } export default new EchoServiceBuilder() diff --git a/frontend/src/api/services/RoundService.ts b/frontend/src/api/services/RoundService.ts index 4a55fc6d..82b24bba 100644 --- a/frontend/src/api/services/RoundService.ts +++ b/frontend/src/api/services/RoundService.ts @@ -4,7 +4,7 @@ import { EchoService, EchoServiceBuilder, GET, - Path, POST + Path, POST, PATCH } from "@/api/EchoFetch"; import config from "@/config"; import {AuthInterceptor} from "@/api/interceptors/AuthInterceptor"; @@ -37,6 +37,14 @@ class RoundService extends EchoService { return {} as EchoPromise; } + /** + * Get all buildings for a syndicus + */ + @GET("/ronde/building/syndicus") + getBuildingsForSyndicus(): EchoPromise<[Building]> { + return {} as EchoPromise<[Building]>; + } + /** * Create new round */ @@ -61,6 +69,38 @@ class RoundService extends EchoService { return {} as EchoPromise } + /** + * Get round by ID + */ + @GET('/ronde/{id}/') + getRoundById(@Path('id') id: number): EchoPromise { + return {} as EchoPromise + } + + /** + * Update round by ID + */ + @PATCH('/ronde/{id}/') + updateRoundById(@Path('id') id: number, @Body() body: RoundWrapper): EchoPromise { + return {} as EchoPromise + } + + /** + * Reset the UUID for a building + */ + @GET('/ronde/building/uuid/{id}/reset/') + resetBuilding(@Path('id') id: string): EchoPromise { + return {} as EchoPromise + } + + /** + * Get a building by its UUID + */ + @GET('/ronde/building/uuid/{id}/') + getBuildingByUUID(@Path('id') id: string): EchoPromise { + return {} as EchoPromise + } + } export default new EchoServiceBuilder() diff --git a/frontend/src/api/services/TrashTemplateService.ts b/frontend/src/api/services/TrashTemplateService.ts index de6912a3..d73ccc3d 100644 --- a/frontend/src/api/services/TrashTemplateService.ts +++ b/frontend/src/api/services/TrashTemplateService.ts @@ -1,7 +1,8 @@ -import {Body, EchoPromise, EchoService, EchoServiceBuilder, GET, PATCH, Path, POST} from "@/api/EchoFetch"; +import {Body, DELETE, EchoPromise, EchoService, EchoServiceBuilder, GET, PATCH, Path, POST} from "@/api/EchoFetch"; import config from "@/config"; import {AuthInterceptor} from "@/api/interceptors/AuthInterceptor"; import TrashTemplate from "@/api/models/TrashTemplate"; +import BuildingContainer from "@/api/models/BuildingContainer"; import Container from "@/api/models/Container"; import Building from "@/api/models/Building"; import {TrashTemplateWrapper} from "@/api/wrappers/TrashTemplateWrapper"; @@ -20,6 +21,11 @@ class TrashTemplateService extends EchoService { return {} as EchoPromise; } + @GET("/trashtemplates/{year}/{week}/") + getContainers(@Path('year') year: number, @Path('week') week: number): EchoPromise { + return {} as EchoPromise; + } + @GET("/trashtemplates/{id}/trashcontainers/") getTrashContainersOfTemplate(@Path('id') id: number): EchoPromise { return {} as EchoPromise; @@ -41,8 +47,8 @@ class TrashTemplateService extends EchoService { } @GET("/trashtemplates/{id}/buildings/") - getBuildingsOfTemplate(@Path('id') id: number): EchoPromise { - return {} as EchoPromise; + getBuildingsOfTemplate(@Path('id') id: number): EchoPromise { + return {} as EchoPromise; } @GET("/trashtemplates/{id}/buildings/eenmalig/") @@ -51,8 +57,8 @@ class TrashTemplateService extends EchoService { } @GET("/trashtemplates/{id}/buildings/{buildingId}/") - getBuildingOfTemplate(@Path('id') id: number, @Path('buildingId') buildingId: number): EchoPromise { - return {} as EchoPromise; + getBuildingOfTemplate(@Path('id') id: number, @Path('buildingId') buildingId: number): EchoPromise { + return {} as EchoPromise; } @GET("/trashtemplates/{id}/buildings/{buildingId}/eenmalig/") @@ -73,7 +79,7 @@ class TrashTemplateService extends EchoService { } @POST("/trashtemplates/{id}/trashcontainers/eenmalig/") - newContainerToTemplateEenmalig(@Path('id') id: number, @Body() body: Container): EchoPromise { + newContainerToTemplateEenmalig(@Path('id') id: number, @Body() body: ContainerWrapper): EchoPromise { return {} as EchoPromise; } @@ -83,7 +89,7 @@ class TrashTemplateService extends EchoService { } @POST("/trashtemplates/{id}/buildings/eenmalig/") - newBuildingToTemplateEenmalig(@Path('id') id: number, @Body() body: Building): EchoPromise { + newBuildingToTemplateEenmalig(@Path('id') id: number, @Body() body: Object): EchoPromise { return {} as EchoPromise; } @@ -100,17 +106,34 @@ class TrashTemplateService extends EchoService { } @PATCH("/trashtemplates/{id}/trashcontainers/{containerId}/eenmalig/") - updateContainerTemplateEenmalig(@Path('id') id: number, @Path('containerId') containerId: number, @Body() body: Container): EchoPromise { + updateContainerTemplateEenmalig(@Path('id') id: number, @Path('containerId') containerId: number, @Body() body: ContainerWrapper): EchoPromise { + return {} as EchoPromise; + } + + @PATCH("/trashtemplates/{id}/buildings/{buildingId}/") + updateBuildingTemplate(@Path('id') id: number, @Path('buildingId') buildingId: number, @Body() body: Object): EchoPromise { + return {} as EchoPromise; + } + + @PATCH("/trashtemplates/{id}/buildings/{buildingId}/eenmalig/") + updateBuildingTemplateEenmalig(@Path('id') id: number, @Path('buildingId') buildingId: number, @Body() body: Object): EchoPromise { + return {} as EchoPromise; + } + + /* All the needed DELETE requests */ + + @DELETE("/trashtemplates/{id}/buildings/{buildingId}/") + deleteBuildingTemplate(@Path('id') id: number, @Path('buildingId') buildingId: number): EchoPromise { return {} as EchoPromise; } - @PATCH("/trashtemplates/{id}/buildings/") - updateBuildingTemplate(@Path('id') id: number, @Body() body: Building): EchoPromise { + @DELETE("/trashtemplates/{id}/trashcontainers/{extraId}/") + deleteContainerFromTemplate(@Path('id') id: number, @Path('extraId') extraId: number): EchoPromise { return {} as EchoPromise; } - @PATCH("/trashtemplates/{id}/buildings/eenmalig/") - updateBuildingTemplateEenmalig(@Path('id') id: number, @Body() body: Building): EchoPromise { + @DELETE("/trashtemplates/{id}/") + deleteTrashTemplate(@Path('id') id: number): EchoPromise { return {} as EchoPromise; } diff --git a/frontend/src/api/services/UserService.ts b/frontend/src/api/services/UserService.ts index 3a211885..80d7ac66 100644 --- a/frontend/src/api/services/UserService.ts +++ b/frontend/src/api/services/UserService.ts @@ -24,11 +24,11 @@ class UserService extends EchoService { } /** - * Patch user by id + * Patch data of logged-in user */ - @PATCH("/user/{id}/") - updateUserById(@Path('id') id: number, @Body() body: {}): EchoPromise { - return {} as EchoPromise + @PATCH("/user/") + patchLoggedInUser(@Body() body : {}) : EchoPromise { + return {} as EchoPromise } /** @@ -41,7 +41,6 @@ class UserService extends EchoService { /** - * Get the logged in user. * Get all users. */ @GET("/users/") @@ -57,15 +56,6 @@ class UserService extends EchoService { return {} as EchoPromise; } - /** - * Update the current user - * TODO Backend support - */ - @PATCH("/user/") - update(@Body() body: InputFields): EchoPromise { - return {} as EchoPromise; - } - /** * Get the role of the logged in user. */ diff --git a/frontend/src/api/wrappers/AuthWrappers.ts b/frontend/src/api/wrappers/AuthWrappers.ts index 774a1870..3b3b15f7 100644 --- a/frontend/src/api/wrappers/AuthWrappers.ts +++ b/frontend/src/api/wrappers/AuthWrappers.ts @@ -1,16 +1,21 @@ export class AuthRegisterWrapper { public email: string; public password: string; + public password2: string; public first_name: string; public last_name: string; public phone_nr: string; - constructor(email: string, password: string, first_name: string, last_name: string, phone_nr: string) { + public locations: []; + + constructor(email: string, password: string, password2: string, first_name: string, last_name: string, phone_nr: string, locations: []) { this.email = email; this.password = password; + this.password2 = password2; this.first_name = first_name; this.last_name = last_name; this.phone_nr = phone_nr; + this.locations = locations; } } @@ -28,12 +33,14 @@ export class AuthForgotWrapper { export class AuthResetWrapper { public email: string; - public new_password: string; + public password: string; + public password2: string; public otp: string; - constructor(email: string, new_password: string, otp: string) { + constructor(email: string, password: string, password2: string, otp: string) { this.email = email - this.new_password = new_password + this.password = password + this.password2 = password2 this.otp = otp } } diff --git a/frontend/src/api/wrappers/ContainerWrapper.ts b/frontend/src/api/wrappers/ContainerWrapper.ts index 351d4dea..6107908a 100644 --- a/frontend/src/api/wrappers/ContainerWrapper.ts +++ b/frontend/src/api/wrappers/ContainerWrapper.ts @@ -3,5 +3,5 @@ import {ContainerType} from "@/api/models/ContainerType"; export default class ContainerWrapper { collection_day: ContainerCollectionDay; - type: ContainerType; + type: String; } diff --git a/frontend/src/components/AccountInformation.vue b/frontend/src/components/AccountInformation.vue index 92d9966a..1ed430b3 100644 --- a/frontend/src/components/AccountInformation.vue +++ b/frontend/src/components/AccountInformation.vue @@ -10,7 +10,9 @@ - - - @@ -38,7 +44,9 @@ - @@ -46,33 +54,24 @@

Rol

- - -
+
- + - - + +
- - + import NormalButton from '@/components/NormalButton' import ConfirmDialog from '@/components/util/ConfirmDialog' +import UserService from "@/api/services/UserService"; +import {check_errors, get_errors} from "@/error_handling"; +import AuthService from "@/api/services/AuthService"; +import {RequestHandler} from "@/api/RequestHandler"; export default { name: 'AccountInformation', components: {ConfirmDialog, NormalButton}, props: { - get_data: { - type: Function, default: () => { - } - }, - save_data: { - type: Function, default: () => { - } - }, - delete_current: { - type: Function, default: () => { - - } - }, not_admin: {type: Boolean, default: true}, can_edit_permission: {type: Boolean, default: true} }, - data: () => { - return { - data: { - type: Object, - default: () => ({ - first_name: '', - last_name: '', - email: '', - phone_nr: '', - role: '', - rondes: [] - }) - }, - roles: [ - {name: 'Aanvrager', value: 'AA'}, {name: 'Student', value: 'ST'}, - {name: 'Superstudent', value: 'SU'}, {name: 'Admin', value: 'AD'}], - edit: false, - smallScreen: false, - can_edit_permission: true - } - }, + data: () => ({ + user: null, + first_name: '', + last_name: '', + email: '', + phone_nr: '', + role: '', + rondes: [], + roles: [ + {name: 'Aanvrager', value: 'AA'}, {name: 'Student', value: 'ST'}, + {name: 'Superstudent', value: 'SU'}, {name: 'Admin', value: 'AD'}, + {name: 'Syndicus', value: 'SY'} + ], + edit: false, + smallScreen: false, + can_edit: true, + errors: null + }), async beforeMount() { - this.data = await this.get_data() + let id = this.$route.params.id const currentUser = await this.$store.getters['session/currentUser'] + this.user = currentUser + + if (id !== undefined) { + await UserService.getUserById(id) + .then(async data => { + this.user = data + }) + .catch(async (error) => { + this.errors = await get_errors(error) + }); + } + + this.first_name = this.user.first_name + this.last_name = this.user.last_name + this.email = this.user.email + this.phone_nr = this.user.phone_nr + this.role = this.user.role + + this.can_edit = this.can_edit_permission if (!this.not_admin) { const currentUserRole = currentUser.role if (currentUserRole === 'SU') { - if (this.data.role === 'AD') { - this.can_edit_permission = false + if (this.role === 'AD') { + this.can_edit = false } else { this.roles = [{name: 'Aanvrager', value: 'AA'}, {name: 'Student', value: 'ST'}, {name: 'Superstudent', value: 'SU'}] @@ -151,16 +157,52 @@ export default { } , methods: { + check_errors, async cancel_save() { this.edit = !this.edit - this.data = await this.get_data() - } - , + + this.first_name = this.user.first_name + this.last_name = this.user.last_name + this.email = this.user.email + this.phone_nr = this.user.phone_nr + this.role = this.user.role + }, async save() { - this.edit = !this.edit - await this.save_data(this.data) - } - , + let id = this.$route.params.id + + let handle + if (id !== undefined) { + handle = RequestHandler.handle(AuthService.updateRoleOfUser({ + role: this.role, + email: this.email + }), { + id: 'updateRoleOfUserAccountInformation', + style: 'SNACKBAR', + }).then() + } else { + handle = RequestHandler.handle(UserService.patchLoggedInUser({ + first_name: this.first_name, + last_name: this.last_name, + email: this.email, + role: this.role, + phone_nr: this.phone_nr + }), { + id: 'patchLoggedInUserAccountInformation', + style: 'SNACKBAR' + }) + } + handle.then(() => { + this.edit = !this.edit + this.email = this.email.toLowerCase() + this.errors = null + }).catch(async (error) => { + this.errors = await get_errors(error) + }) + }, + delete_current() { + UserService.deleteUserById(this.user.id) + this.$router.push({name: 'admin_user_register'}) + }, onResize() { this.smallScreen = window.innerWidth < 500 } diff --git a/frontend/src/components/LoginTopBar.vue b/frontend/src/components/LoginTopBar.vue index d9404027..bc74754d 100644 --- a/frontend/src/components/LoginTopBar.vue +++ b/frontend/src/components/LoginTopBar.vue @@ -6,7 +6,7 @@ diff --git a/frontend/src/components/NavigationBar.vue b/frontend/src/components/NavigationBar.vue index 466edfea..1490f90f 100644 --- a/frontend/src/components/NavigationBar.vue +++ b/frontend/src/components/NavigationBar.vue @@ -1,35 +1,41 @@ diff --git a/frontend/src/components/admin/RoundListPage.vue b/frontend/src/components/admin/RoundListPage.vue index bf965699..e0295a71 100644 --- a/frontend/src/components/admin/RoundListPage.vue +++ b/frontend/src/components/admin/RoundListPage.vue @@ -20,7 +20,7 @@ Heeft als nodige argumenten nodig: - + @@ -55,7 +55,7 @@ export default { }, addFunction: { type: Function, - default: null, + default: () => null, required: true }, headComponent: { @@ -77,6 +77,11 @@ export default { type: Array, default: () => ['default'], required: true + }, + mapKeys: { + type: Map, + default: {}, + required: true } }, methods: { diff --git a/frontend/src/components/admin/RoundViewCard.vue b/frontend/src/components/admin/RoundViewCard.vue index 386f9bb5..4f82efc8 100644 --- a/frontend/src/components/admin/RoundViewCard.vue +++ b/frontend/src/components/admin/RoundViewCard.vue @@ -13,27 +13,26 @@ it displays the information of one round so that a list of round views can easil

{{data.date}}

- - - - - - -

{{student.first_name}} {{student.last_name}}

-
-
+ + + + +

+ + {{ s.first_name }} {{ s.last_name }}

+
+
+
@@ -44,27 +43,60 @@ it displays the information of one round so that a list of round views can easil + + diff --git a/frontend/src/components/admin/student_template/DagPlanningCard.vue b/frontend/src/components/admin/student_template/DagPlanningCard.vue index fb1ed71c..066cacb4 100644 --- a/frontend/src/components/admin/student_template/DagPlanningCard.vue +++ b/frontend/src/components/admin/student_template/DagPlanningCard.vue @@ -1,39 +1,44 @@ @@ -63,21 +68,21 @@ export default { methods: { format_day(day) { const day_mapping = { - "MO": "Maandag", - "TU": "Dinsdag", - "WE": "Woensdag", - "TH": "Donderdag", - "FR": "Vrijdag", - "SA": "Zaterdag", - "SU": "Zondag", - } - return day_mapping[day] + "MO": "Maandag", + "TU": "Dinsdag", + "WE": "Woensdag", + "TH": "Donderdag", + "FR": "Vrijdag", + "SA": "Zaterdag", + "SU": "Zondag", + } + return day_mapping[day] }, async remove_dagplanning() { const response = await RequestHandler.handle(StudentTemplateService.deleteDagPlanning(this.data.template_id, this.data.dag_id), { id: "deleteDagplanningError", style: "SNACKBAR" - }).then(res => res).catch(() => {}) + }).then(res => res).catch(() => null) this.$emit('remove', response["new_id"]) } } diff --git a/frontend/src/components/admin/student_template/StudentTemplateCard.vue b/frontend/src/components/admin/student_template/StudentTemplateCard.vue index f67e4d06..6305586c 100644 --- a/frontend/src/components/admin/student_template/StudentTemplateCard.vue +++ b/frontend/src/components/admin/student_template/StudentTemplateCard.vue @@ -2,36 +2,36 @@
-
+
{{ data.location }}
-
+
{{ data.even ? "even" : "oneven" }}
-
+
{{ data.name }}
-
{{ data.status }}
+
{{ data.status }}
- + Aanpassen - + Verwijderen -
Om deze template aan te passen moeten eerst de eenmalige aanpassingen ongedaan worden.
+
Om deze template aan te passen moeten eerst de eenmalige aanpassingen ongedaan worden.
diff --git a/frontend/src/components/admin/student_template/TemplateRondeCard.vue b/frontend/src/components/admin/student_template/TemplateRondeCard.vue index 16658529..a5de9c90 100644 --- a/frontend/src/components/admin/student_template/TemplateRondeCard.vue +++ b/frontend/src/components/admin/student_template/TemplateRondeCard.vue @@ -2,17 +2,17 @@
-
+
{{ data.location }}
-
+
{{ data.name }}
@@ -20,10 +20,10 @@ - + Dagplanningen - + Verwijderen diff --git a/frontend/src/components/containerTemplates/TrashContainerTemplateCard.vue b/frontend/src/components/containerTemplates/TrashContainerTemplateCard.vue index 110d101f..2a13209b 100644 --- a/frontend/src/components/containerTemplates/TrashContainerTemplateCard.vue +++ b/frontend/src/components/containerTemplates/TrashContainerTemplateCard.vue @@ -5,30 +5,22 @@

{{ this.data.name }}

-

Zie Vuilnisbakken

+

Zie Vuilnisbakken

-

Zie Gebouwen

+

Zie Gebouwen

- {{ this.data.year }} + {{ status_mapping[this.data.status] }} - - {{ this.data.week }} - - + {{ this.locatie }} - - {{ this.data.even }} - - - - - + + {{ this.data.even ? 'Even' : 'Oneven' }} - - + + @@ -43,10 +35,11 @@ import EditIcon from '@/components/icons/EditIcon.vue' import router from '@/router' import {RequestHandler} from "@/api/RequestHandler"; import LocationService from "@/api/services/LocationService"; +import TrashTemplateService from "@/api/services/TrashTemplateService"; export default { name: 'TrashContainerTemplateCard', - components: {EditIcon, DeleteIcon}, + components: {DeleteIcon}, props: { data: { type: TrashTemplate, @@ -54,25 +47,38 @@ export default { }, data: () => ({ locations: [], - locatie: "" + locatie: "", + status_mapping: { + "A": "Actief", + "E": "Eenmalig", + "V": "Vervangen" + } }), methods: { editTemplate: function () { router.push({ - path: '/trashtemplates/'+ this.data.id +'/edit' + name: 'editTrashtemplates', + params: {id: this.data.id} }); }, deleteTemplate: function () { - //todo + RequestHandler.handle(TrashTemplateService.deleteTrashTemplate(this.data.id), { + id: 'deleteTrashTemplateError', + style: 'SNACKBAR' + }).then(() => { + router.go(0) // refresh the page + }) }, goToTrashTemplateBuildingsPage: function () { router.push({ - path: '/trashtemplates/' + this.data.id + '/buildings' + name: 'trashtemplateBuildings', + params: {id: this.data.id} }); }, goToTrashTemplateContainersPage: function () { router.push({ - path: '/trashtemplates/' + this.data.id + '/containers' + name: 'trashtemplateContainers', + params: {id: this.data.id} }) } }, diff --git a/frontend/src/components/containerTemplates/TrashContainerTemplateCreate.vue b/frontend/src/components/containerTemplates/TrashContainerTemplateCreate.vue index 745c36a9..fde7e54a 100644 --- a/frontend/src/components/containerTemplates/TrashContainerTemplateCreate.vue +++ b/frontend/src/components/containerTemplates/TrashContainerTemplateCreate.vue @@ -6,38 +6,27 @@ - + - + - - - - - Aanmaken + Aanmaken @@ -45,66 +34,40 @@ - - diff --git a/frontend/src/components/containerTemplates/buildings/TrashTemplateBuildingAdd.vue b/frontend/src/components/containerTemplates/buildings/TrashTemplateBuildingAdd.vue new file mode 100644 index 00000000..2de401ae --- /dev/null +++ b/frontend/src/components/containerTemplates/buildings/TrashTemplateBuildingAdd.vue @@ -0,0 +1,110 @@ + + + diff --git a/frontend/src/components/containerTemplates/buildings/TrashTemplateBuildingCard.vue b/frontend/src/components/containerTemplates/buildings/TrashTemplateBuildingCard.vue index 00f74d46..19c9b364 100644 --- a/frontend/src/components/containerTemplates/buildings/TrashTemplateBuildingCard.vue +++ b/frontend/src/components/containerTemplates/buildings/TrashTemplateBuildingCard.vue @@ -36,7 +36,7 @@ - - diff --git a/frontend/src/components/containerTemplates/containers/TrashContainerEdit.vue b/frontend/src/components/containerTemplates/containers/TrashContainerEdit.vue index 3d31becb..963e117b 100644 --- a/frontend/src/components/containerTemplates/containers/TrashContainerEdit.vue +++ b/frontend/src/components/containerTemplates/containers/TrashContainerEdit.vue @@ -7,56 +7,66 @@ - + - + - - - Aanpassen - - - + + - diff --git a/frontend/src/components/containerTemplates/containers/TrashContainerHeader.vue b/frontend/src/components/containerTemplates/containers/TrashContainerHeader.vue index 68e74a1d..361fd6c0 100644 --- a/frontend/src/components/containerTemplates/containers/TrashContainerHeader.vue +++ b/frontend/src/components/containerTemplates/containers/TrashContainerHeader.vue @@ -11,7 +11,7 @@

Type

- + Acties diff --git a/frontend/src/components/containerTemplates/containers/TrashTemplateContainersList.vue b/frontend/src/components/containerTemplates/containers/TrashTemplateContainersList.vue index 7d935022..5b19b216 100644 --- a/frontend/src/components/containerTemplates/containers/TrashTemplateContainersList.vue +++ b/frontend/src/components/containerTemplates/containers/TrashTemplateContainersList.vue @@ -1,5 +1,5 @@ diff --git a/frontend/src/views/BuildingPageStudent.vue b/frontend/src/views/BuildingPageStudent.vue index d4f0ef56..ebe985e0 100644 --- a/frontend/src/views/BuildingPageStudent.vue +++ b/frontend/src/views/BuildingPageStudent.vue @@ -27,7 +27,7 @@ :title="trashMap[item.type]" :subtitle="item.special_actions" align="left" - active-color="primary" + color="primary" > diff --git a/frontend/tests/unit/Components/AccountInformation.spec.ts b/frontend/tests/unit/Components/AccountInformation.spec.ts new file mode 100644 index 00000000..b9000b67 --- /dev/null +++ b/frontend/tests/unit/Components/AccountInformation.spec.ts @@ -0,0 +1,71 @@ +import {mount} from '@vue/test-utils'; +import AccountInformation from "@/components/AccountInformation.vue" +import NormalButton from "@/components/NormalButton.vue"; +import ConfirmDialog from "@/components/util/ConfirmDialog.vue" + +describe('AccountInformation.vue', () => { + + const dataOfComponent = { + first_name: 'Test', + last_name: 'Tester', + email: 'test@test.com', + phone_nr: '0474441313', + role: 'ST', + } + + let wrapper; + + beforeEach(() => { + AccountInformation.beforeMount = () => Promise.resolve(); + wrapper = mount(AccountInformation) + }); + + + it('render the component', () => { + try { + expect(wrapper.exists()).toBeTruthy(); + } catch (err) { + + } + }) + + it('user data is shown without edit', async () => { + await wrapper.setData(dataOfComponent); + await wrapper.vm.$nextTick(); + expect(wrapper.vm.$data.first_name === dataOfComponent.first_name).toBeTruthy(); + expect(wrapper.vm.$data.last_name === dataOfComponent.last_name).toBeTruthy(); + expect(wrapper.vm.$data.email === dataOfComponent.email).toBeTruthy(); + expect(wrapper.vm.$data.phone_nr === dataOfComponent.phone_nr).toBeTruthy(); + expect(wrapper.vm.$data.role === dataOfComponent.role).toBeTruthy(); + expect(wrapper.find('v-btn').attributes('icon')).toBeFalsy(); + }); + + it('user can be deleted by admin', async () => { + await wrapper.setProps({not_admin: false, can_edit_permission: true}); + expect(wrapper.findComponent(NormalButton).text()).toBe('Pas aan'); + expect(wrapper.findComponent(ConfirmDialog).exists()).toBeTruthy(); + expect(wrapper.find('[data-test="delete-button"]').exists()).toBeTruthy(); + }); + + it('user can not be deleted by admin', async () => { + await wrapper.setProps({not_admin: true, can_edit_permission: true}); + expect(wrapper.findComponent(NormalButton).text()).toBe('Pas aan'); + expect(wrapper.find('[data-test="delete-button"]').exists()).toBeFalsy(); + }); + + it('user can not edit data', async () => { + await wrapper.setData({can_edit: false}) + expect(wrapper.find('[data-test="edit-button"]').exists()).toBeFalsy() + expect(wrapper.find('[data-test="save-button"]').exists()).toBeFalsy(); + expect(wrapper.find('[data-test="cancel-button"]').exists()).toBeFalsy(); + }) + + it('user can edit data', async () => { + await wrapper.setProps({not_admin: true, can_edit_permission: true}) + const editButton = wrapper.find('[data-test="edit-button"]') + expect(editButton.exists()).toBeTruthy() + await editButton.trigger('click'); + expect(wrapper.find('[data-test="save-button"]').exists()).toBeTruthy(); + expect(wrapper.find('[data-test="cancel-button"]').exists()).toBeTruthy(); + }) +}) diff --git a/frontend/tests/unit/Components/LoginTopBar.spec.ts b/frontend/tests/unit/Components/LoginTopBar.spec.ts new file mode 100644 index 00000000..9a659d6d --- /dev/null +++ b/frontend/tests/unit/Components/LoginTopBar.spec.ts @@ -0,0 +1,20 @@ +import {mount} from "@vue/test-utils" +import LoginTopBar from "@/components/LoginTopBar.vue" + +describe('LoginTopBar.vue', () => { + + it('render of component', () => { + const wrapper = mount(LoginTopBar); + expect(wrapper.exists()).toBeTruthy(); + }); + + it('rendering of v-app-bar', async () => { + // Mount doesn't render templates + const wrapper = mount(LoginTopBar); + await wrapper.vm.$nextTick(); + const appBar = wrapper.find('v-app-bar'); + expect(appBar.exists()).toBeTruthy(); + }); + + +}) diff --git a/frontend/tests/unit/Components/NavigationBar.spec.ts b/frontend/tests/unit/Components/NavigationBar.spec.ts new file mode 100644 index 00000000..95413534 --- /dev/null +++ b/frontend/tests/unit/Components/NavigationBar.spec.ts @@ -0,0 +1,69 @@ +import {mount} from "@vue/test-utils"; +import NavigationBar from "@/components/NavigationBar.vue"; + +describe('NavigationBar.vue', () => { + + let wrapper; + let logout; + + beforeEach(() => { + NavigationBar.beforeCreate = () => Promise.resolve(); + logout = jest.fn(); + wrapper = mount(NavigationBar, { + setup() { + return { + logout + } + } + }); + }); + + it('render of component', () => { + expect(wrapper.exists()).toBeTruthy(); + }) + + it('test for different routes no admin', () => { + expect(wrapper.find('[data-test="list"]').exists()).toBeTruthy(); + expect(wrapper.vm.$data.isAdminOrSu).toBeFalsy(); + + expect(wrapper.find('[data-test="home"]').exists()).toBeTruthy(); + expect(wrapper.find('[data-test="account"]').exists()).toBeTruthy(); + expect(wrapper.find('[data-test="logout"]').exists()).toBeTruthy(); + }) + + it('test for routes admin', async () => { + await wrapper.setData({isAdminOrSu: true}); + await wrapper.vm.$nextTick(); + expect(wrapper.vm.$data.isAdminOrSu).toBeTruthy(); + + expect(wrapper.find('[data-test="home"]').exists()).toBeFalsy(); + + expect(wrapper.find('[data-test="dashboard"]').exists()).toBeTruthy(); + expect(wrapper.find('[data-test="templates"]').exists()).toBeTruthy(); + expect(wrapper.find('[data-test="locations"]').exists()).toBeTruthy(); + expect(wrapper.find('[data-test="afval-templates"]').exists()).toBeTruthy(); + expect(wrapper.find('[data-test="rounds"]').exists()).toBeTruthy(); + expect(wrapper.find('[data-test="buildings"]').exists()).toBeTruthy(); + expect(wrapper.find('[data-test="student"]').exists()).toBeTruthy(); + expect(wrapper.find('[data-test="syndicus"]').exists()).toBeTruthy(); + expect(wrapper.find('[data-test="emails"]').exists()).toBeTruthy(); + expect(wrapper.find('[data-test="account"]').exists()).toBeTruthy(); + expect(wrapper.find('[data-test="logout"]').exists()).toBeTruthy(); + }); + + it('log-out user', async () => { + const logoutListItem = wrapper.find('[data-test="logout"'); + await logoutListItem.trigger('click'); + expect(logout).toHaveBeenCalled(); + }); + + it('rendering of bar', () => { + expect(wrapper.find('[data-test="bar"]').exists()).toBeTruthy() + }); + + it('check for mount of drawer', () => { + expect(wrapper.find('[data-test="drawer"]').exists()).toBeTruthy(); + }); + + +}); diff --git a/frontend/tests/unit/Components/NormalButton.spec.ts b/frontend/tests/unit/Components/NormalButton.spec.ts new file mode 100644 index 00000000..40726571 --- /dev/null +++ b/frontend/tests/unit/Components/NormalButton.spec.ts @@ -0,0 +1,35 @@ +import {mount} from "@vue/test-utils" +import NormalButton from "@/components/NormalButton.vue" + +describe('NormalButton.vue', () => { + + let wrapper; + + const data = { + text: 'TestButton', + dropDown: false, + parentFunction: jest.fn() + } + + beforeEach(() => { + wrapper = mount(NormalButton, { + propsData: data + }) + }) + + it('render of component', () => { + expect(wrapper.exists).toBeTruthy(); + }) + + it('text is present', () => { + const button = wrapper.find('v-btn'); + expect(button.text()).toBe('TestButton'); + }) + + it('click button', async () => { + const button = wrapper.find('v-btn'); + await button.trigger('click'); + expect(wrapper.vm.$props.parentFunction).toHaveBeenCalled(); + }) + +}) diff --git a/frontend/tests/unit/Components/RegisterDone.spec.ts b/frontend/tests/unit/Components/RegisterDone.spec.ts new file mode 100644 index 00000000..d2c28988 --- /dev/null +++ b/frontend/tests/unit/Components/RegisterDone.spec.ts @@ -0,0 +1,31 @@ +import {mount} from "@vue/test-utils" +import RegisterDone from "@/components/RegisterDone.vue" + +describe('RegisterDone.vue', () => { + + let wrapper; + + beforeEach(() => { + wrapper = mount(RegisterDone); + }) + + it('render of component', () => { + expect(wrapper.exists()).toBeTruthy() + }) + + it('check for succes text', () => { + const div = wrapper.find('[data-test="succes"]'); + expect(div.text()).toBe('Registratie is gelukt!') + }) + + it('check for thank you text', () => { + const div = wrapper.find('[data-test="thank-you"]'); + expect(div.text()).toBe('Bedankt voor het registreren bij Dr. Trottoir.'); + }) + + it('check for waiting text', () => { + const div = wrapper.find('[data-test="waiting"]'); + expect(div.text()).toBe('Als uw aanmelding is verwerkt, zal de applicatie hier beschikbaar zijn.'); + }) + +}) diff --git a/frontend/tests/unit/Components/StateButtonsTest.spec.js b/frontend/tests/unit/Components/StateButtonsTest.spec.js new file mode 100644 index 00000000..16e408ce --- /dev/null +++ b/frontend/tests/unit/Components/StateButtonsTest.spec.js @@ -0,0 +1,84 @@ +import {mount} from '@vue/test-utils'; +import StateButtons from "@/components/StateButtons.vue"; + +describe('StateButtons.vue', function () { + it('renders the correct buttons when status is "A"', () => { + const wrapper = mount(StateButtons, { + propsData: { + eenmalig: jest.fn(), + permanent: jest.fn(), + status: 'A', + }, + }); + + expect(wrapper.find('.bg-secondary').exists()).toBe(true); + expect(wrapper.find('.bg-primary').exists()).toBe(true); + expect(wrapper.find('.text-primary').exists()).toBe(true); + expect(wrapper.find('.text-secondary').exists()).toBe(true); + }); + + it('renders the correct buttons when status is "E"', () => { + const wrapper = mount(StateButtons, { + propsData: { + eenmalig: jest.fn(), + permanent: jest.fn(), + status: 'E', + }, + }); + + expect(wrapper.find('.bg-primary').exists()).toBe(true); + expect(wrapper.find('.text-secondary').exists()).toBe(true); + }); + + it('renders the correct message when status is "V"', () => { + const wrapper = mount(StateButtons, { + propsData: { + eenmalig: jest.fn(), + permanent: jest.fn(), + status: 'V', + }, + }); + + expect(wrapper.find('.text-caption').exists()).toBe(true); + }); + + it('emits the correct events when buttons are clicked status A', () => { + const eenmaligMock = jest.fn(); + const permanentMock = jest.fn(); + const wrapper = mount(StateButtons, { + propsData: { + eenmalig: eenmaligMock, + permanent: permanentMock, + status: 'A', + }, + }); + + const eenmalig = wrapper.find('.bg-secondary') + eenmalig.trigger('click'); + expect(eenmaligMock).toHaveBeenCalledTimes(1); + expect(eenmalig.text()).toBe('Eenmalig aanpassen') + + const permanent = wrapper.find('.bg-primary') + permanent.trigger('click'); + expect(permanentMock).toHaveBeenCalledTimes(1); + expect(permanent.text()).toBe('Permanent aanpassen') + }); + + it('emits the correct events when buttons are clicked status E', () => { + const eenmaligMock = jest.fn(); + const permanentMock = jest.fn(); + const wrapper = mount(StateButtons, { + propsData: { + eenmalig: eenmaligMock, + permanent: permanentMock, + status: 'E', + }, + }); + + const eenmalig = wrapper.find('.bg-primary') + eenmalig.trigger('click'); + expect(eenmaligMock).toHaveBeenCalledTimes(1); + expect(eenmalig.text()).toBe('Aanpassing opslaan') + + }); +}); diff --git a/frontend/tests/unit/Components/admin/AdminBuildingInfoEdit.spec.ts b/frontend/tests/unit/Components/admin/AdminBuildingInfoEdit.spec.ts new file mode 100644 index 00000000..582eb2fe --- /dev/null +++ b/frontend/tests/unit/Components/admin/AdminBuildingInfoEdit.spec.ts @@ -0,0 +1,63 @@ +import {mount} from "@vue/test-utils" +import AdminBuildingInfoEdit from "@/components/admin/AdminBuildingInfoEdit.vue" + +describe('AdminBuildingInfoEdit.vue', () => { + + let wrapper; + + const data = { + name: 'Test gebouw', + adres: 'Teststraat 1', + manual: {file: 'test.pdf', fileType: 'pdf', manualStatus: 'Klaar'}, + ivago_klantnr: 34, + planningen: [], + new_manual: null, + errors: null + } + + beforeEach(() => { + AdminBuildingInfoEdit.beforeMount = () => Promise.resolve(); + AdminBuildingInfoEdit.methods.save = jest.fn(); + AdminBuildingInfoEdit.methods.cancel_save = jest.fn(); + wrapper = mount(AdminBuildingInfoEdit); + }) + + it('render of component', () => { + expect(wrapper.exists()).toBeTruthy(); + }); + + it('check if data is present', async () => { + await wrapper.setData(data); + await wrapper.vm.$nextTick(); + expect(wrapper.find('[data-test="name"]').attributes('model-value')).toBe('Test gebouw'); + expect(wrapper.find('[data-test="adres"]').attributes('model-value')).toBe('Teststraat 1') + expect(wrapper.find('[data-test="client-nr"]').attributes('model-value')).toBe("34"); + expect(wrapper.find('[data-test="manual"]').exists()).toBeTruthy(); + }); + + it('should have save and cancel button', () => { + expect(wrapper.find('[data-test="save-button"]').exists()).toBeTruthy(); + expect(wrapper.find('[data-test="cancel-button"]').exists()).toBeTruthy(); + }) + + it('check for confirmdialog', () => { + expect(wrapper.find('[data-test="dialog"]').exists()).toBeTruthy() + }) + + it('test save', async () => { + const saveButton = wrapper.find('[data-test="save-button"]'); + expect(saveButton.exists()).toBeTruthy(); + await saveButton.trigger('click'); + expect(AdminBuildingInfoEdit.methods.save).toHaveBeenCalled(); + }) + + it('cancel edit', async () => { + await wrapper.setProps({edit: true}); + await wrapper.vm.$nextTick(); + const editButton = wrapper.find('[data-test="cancel-button"]'); + expect(editButton.exists()).toBeTruthy(); + await editButton.trigger('click'); + expect(AdminBuildingInfoEdit.methods.cancel_save).toHaveBeenCalled(); + }) + +}); diff --git a/frontend/tests/unit/Components/admin/BuildingCard.spec.ts b/frontend/tests/unit/Components/admin/BuildingCard.spec.ts new file mode 100644 index 00000000..1179fa6d --- /dev/null +++ b/frontend/tests/unit/Components/admin/BuildingCard.spec.ts @@ -0,0 +1,66 @@ +import {mount} from "@vue/test-utils"; +import BuildingCard from "@/components/admin/BuildingCard.vue"; + +describe('BuildingCard.vue', () => { + + let wrapper + + beforeEach(async () => { + BuildingCard.beforeMount = () => Promise.resolve(); + BuildingCard.methods.downloadDocument = jest.fn() + BuildingCard.methods.goToEditBuilding = jest.fn() + BuildingCard.methods.deletePost = jest.fn(); + wrapper = mount(BuildingCard, { + props: { + data: { + name: 'Test Gebouw', + adres: 'TestStraat 1', + id: 0, + ivago_klantnr: 0, + buildingID: '', + manual: null, + containers: null, + location: null + } + } + }); + }); + + it('render of component', async () => { + expect(wrapper.exists()).toBeTruthy(); + }) + + it('check for name', () => { + expect(wrapper.find('[data-test="name"]').text()).toBe('Test Gebouw'); + }); + + it('check for adress', () => { + expect(wrapper.find('[data-test="adres"]').text()).toBe('TestStraat 1'); + }); + + it('check for manual', () => { + expect(wrapper.find('[data-test="manual"]').text()).toBe('Handleiding'); + }); + + it('download manual', async () => { + const manualMenu = wrapper.find('[data-test="manual"]'); + await manualMenu.trigger('click'); + await wrapper.vm.$nextTick(); + expect(BuildingCard.methods.downloadDocument).toHaveBeenCalled(); + }); + + it('go to edit page', async () => { + const editButton = wrapper.find('[data-test="edit"]'); + await editButton.trigger('click'); + await wrapper.vm.$nextTick(); + expect(BuildingCard.methods.goToEditBuilding).toHaveBeenCalled(); + }); + + it('delete building', async () => { + const editButton = wrapper.find('[data-test="delete"]'); + await editButton.trigger('click'); + await wrapper.vm.$nextTick(); + expect(BuildingCard.methods.deletePost).toHaveBeenCalled(); + }); + +}) diff --git a/frontend/tests/unit/Components/admin/BuildingHeader.spec.ts b/frontend/tests/unit/Components/admin/BuildingHeader.spec.ts new file mode 100644 index 00000000..2ef5fd57 --- /dev/null +++ b/frontend/tests/unit/Components/admin/BuildingHeader.spec.ts @@ -0,0 +1,31 @@ +import {mount} from "@vue/test-utils" +import BuildingHeader from "@/components/admin/BuildingHeader.vue" + +describe('BuildingHeader.vue', () => { + + let wrapper; + + beforeEach(() => { + wrapper = mount(BuildingHeader); + }) + + it('render of comonent', () => { + expect(wrapper.exists()).toBeTruthy(); + }) + + it('check field are present', async () => { + await wrapper.setProps({round: true}); + await wrapper.vm.$nextTick(); + expect(wrapper.find('[data-test="title"]').text()).toBe('Gebouw') + expect(wrapper.find('[data-test="adres"]').text()).toBe('Adres') + expect(wrapper.find('[data-test="round"]').exists()).toBeFalsy(); + expect(wrapper.find('[data-test="manual"]').text()).toBe('Handleiding'); + expect(wrapper.find('[data-test="status"]').text()).toBe('Document status') + }); + + it('test for round', () => { + expect(wrapper.find('[data-test="round"]').exists()).toBeTruthy(); + expect(wrapper.find('[data-test="round"]').text()).toBe('Efficiëntie') + }) + +}) diff --git a/frontend/tests/unit/Components/admin/createEditSyndicusTest.spec.js b/frontend/tests/unit/Components/admin/createEditSyndicusTest.spec.js new file mode 100644 index 00000000..4ba37371 --- /dev/null +++ b/frontend/tests/unit/Components/admin/createEditSyndicusTest.spec.js @@ -0,0 +1,54 @@ +import {mount} from '@vue/test-utils' +import CreateEditSyndicus from "@/components/admin/CreateEditSyndicus.vue"; + +describe('CreateEditSyndicus.vue', () => { + let wrapper; + + beforeEach(() => { + CreateEditSyndicus.mounted = jest.fn() + wrapper = mount(CreateEditSyndicus) + }) + + + it('renders props.msg when passed', () => { + expect(wrapper.exists()).toBe(true); + expect(CreateEditSyndicus.mounted).toHaveBeenCalled(); + }) + + it('show right title', async () => { + expect(wrapper.find('h1').text()).toBe('Maak nieuwe Syndicus aan'); + await wrapper.setProps({edit: true}); + await wrapper.vm.$forceUpdate(); + expect(wrapper.find('h1').text()).toBe('Syndicus aanpassen'); + }) + + it('call addSyndicus when submit', async () => { + CreateEditSyndicus.methods.addSyndicus = jest.fn(); + wrapper = mount(CreateEditSyndicus); + const button = await wrapper.find('[data-test="add"]') + await button.trigger('click'); + expect(CreateEditSyndicus.methods.addSyndicus).toHaveBeenCalled(); + }) + + it('call editSyndicus when submit', async () => { + CreateEditSyndicus.methods.editSyndicus = jest.fn(); + wrapper = mount(CreateEditSyndicus, { + propsData: { + edit: true + } + }); + const button = await wrapper.find('[data-test="edit"]') + await button.trigger('click'); + expect(CreateEditSyndicus.methods.editSyndicus).toHaveBeenCalled(); + }) + + it('render component correctly', () => { + const labels = wrapper.findAll('label'); + expect(labels.length).toBe(3); + expect(labels.at(0).text()).toBe('Syndicus'); + expect(labels.at(1).text()).toBe('Locatie'); + expect(labels.at(2).text()).toBe('Gebouwen'); + + expect(wrapper.findAll('v-autocomplete').length).toBe(3); + }) +}) diff --git a/frontend/tests/unit/Components/admin/mail/mailTests.spec.ts b/frontend/tests/unit/Components/admin/mail/mailTests.spec.ts new file mode 100644 index 00000000..3e5fb44f --- /dev/null +++ b/frontend/tests/unit/Components/admin/mail/mailTests.spec.ts @@ -0,0 +1,205 @@ +import {mount} from '@vue/test-utils' +import CreateEditMailTemplate from '@/components/admin/mail/CreateEditMailTemplate.vue' +import SendMail from '@/components/admin/mail/SendMail.vue' +import {triggerInput} from "../../../../utils/testHelper"; + +describe('CreateEditMailTemplate.vue', () => { + + let wrapper; + + beforeEach(() => { + wrapper = mount(CreateEditMailTemplate) + }) + + it('renders the component', () => { + expect(wrapper.exists()).toBe(true); + }); + + it('Render title create', () => { + expect(wrapper.find('h1').text()).toMatch('Mail template aanmaken') + }) + + it('Render title edit', async () => { + await wrapper.setProps({edit: true}) + await wrapper.vm.$nextTick() + expect(wrapper.find('h1').text()).toMatch('Mail template aanpassen') + }) + + it('sets the v-model for the textfield of the name of the template', async () => { + + const textField = wrapper.find('v-text-field') + textField.element.value = 'test'; + const activator = (x) => { + return {template: {name: x}} + } + triggerInput(textField, wrapper, activator) + expect(wrapper.vm.$data.template.name).toBe('test'); + }) + + it('sets the v-model for the textArea of the text of the template', async () => { + const textArea = wrapper.find('v-textarea') + textArea.element.value = 'Dit is een test mail template #test#'; + const activator = (x) => { + return {template: {text: x}} + } + triggerInput(textArea, wrapper, activator) + expect(wrapper.vm.$data.template.text).toBe('Dit is een test mail template #test#'); + }) + + it('opens the dialog when the information icon is clicked', async () => { + const infoIcon = wrapper.find('v-icon'); + infoIcon.trigger('click'); + await wrapper.vm.$nextTick(); + expect(wrapper.vm.showDialog).toBe(true); + }); + + it('test if createTemplate is called when the create button is clicked', async () => { + CreateEditMailTemplate.methods.createTemplate = jest.fn() + const wrapper = mount(CreateEditMailTemplate) + await wrapper.setData({template: {name: 'test', text: 'Dit is een test mail template #test#'}}) + await wrapper.vm.$nextTick(); + + const saveButton = wrapper.find('[data-test="create-button"]'); + await saveButton.trigger('click'); + await wrapper.vm.$nextTick(); + + + expect(CreateEditMailTemplate.methods.createTemplate).toHaveBeenCalled(); + }); + + it('test if editTemplate is called when the edit button is clicked', async () => { + CreateEditMailTemplate.methods.editTemplate = jest.fn() + const wrapper = mount(CreateEditMailTemplate) + await wrapper.setProps({edit: true}) + await wrapper.setData({template: {name: 'test', text: 'Dit is een test mail template #test#'}}) + await wrapper.vm.$nextTick(); + + const saveButton = wrapper.find('[data-test="edit-button"]'); + await saveButton.trigger('click'); + await wrapper.vm.$nextTick(); + + + expect(CreateEditMailTemplate.methods.editTemplate).toHaveBeenCalled(); + }); + + it('formats text correctly', () => { + wrapper.setData({ + template: { + name: 'Test Template', + text: 'Hello #name#,
Test test
Best regards,
#syndicus#', + } + }) + + expect(wrapper.vm.formattedText).toBe('Hello name,
Test test
Best regards,
syndicus'); + }); + + it('formats text correctly when there are no variables', () => { + expect(wrapper.vm.formattedText).toBe(''); + }) + + +}) + +describe('Sendmail.vue', () => { + let wrapper; + + function initializeV_Autocomplete(wrapper) { + wrapper.setData({ + templates: [ + { + id: 1, + name: 'Test template', + text: 'Hello #name# #surname#' + }, + { + id: 2, + name: 'Test template 2', + text: 'Hello #name# #surname#' + }] + }) + const template = wrapper.vm.templates[0] + const autocomplete = wrapper.find('v-autocomplete') + wrapper.vm.$data.description = template.text + autocomplete.wrapperElement._vei.onSlotchange.value(); // de onChange function als er een andere template wordt geselecteerd + + } + + beforeEach(() => { + SendMail.beforeMount = jest.fn() + SendMail.mounted = jest.fn() + wrapper = mount(SendMail) + }) + + it('renders the component', () => { + expect(wrapper.exists()).toBe(true); + }); + + it('Render title create', () => { + expect(wrapper.find('h1').text()).toMatch('Mail versturen') + }) + + it('Render sender label', () => { + expect(wrapper.find('[data-test="aan"]').text()).toMatch('Aan') + }) + + it('Render name sender', async () => { + const testEmail = ['test@test.be'] + wrapper.setData({emails: testEmail}) + await wrapper.vm.$nextTick() + const emailField = wrapper.find('[data-test="email"]') + expect(wrapper.find('[data-test="email"]').text()).toMatch(testEmail.toString()) + }) + + it('Render template label', () => { + expect(wrapper.find('[data-test="template"]').text()).toMatch('Template') + }) + + it('initialize argument fields when template is selected', async () => { + initializeV_Autocomplete(wrapper) + + expect(wrapper.vm.$data.nameOfArguments).toStrictEqual(["#name#", "#surname#"]) + expect(wrapper.vm.$data.inputArguments).toStrictEqual(['', '']) + + + }) + + it('test when inputting data in the arguments', async () => { + initializeV_Autocomplete(wrapper) + + let description = wrapper.vm.getDescriptionWithArguments() + + expect(description).toBe("Hello #name# #surname#") + + wrapper.vm.$data.inputArguments = ['test', ''] + description = wrapper.vm.getDescriptionWithArguments() + + expect(description).toBe("Hello test #surname#") + }) + + it('test formattedText function', () => { + initializeV_Autocomplete(wrapper) + wrapper.vm.$data.inputArguments = ['test', ''] + + expect(wrapper.vm.formattedText).toBe("Hello test surname") + }) + + it('test if sendMail is called when the send button is clicked', async () => { + SendMail.methods.sendMail = jest.fn() + const wrapper = mount(SendMail) + const button = wrapper.find('[data-test="send-button"]'); + button.trigger('click'); + await wrapper.vm.$nextTick(); + expect(SendMail.methods.sendMail).toHaveBeenCalled(); + }) + + it('getMailbody', () => { + initializeV_Autocomplete(wrapper) + wrapper.vm.$data.inputArguments = ['test', 'test2'] + const date = new Date() + wrapper.setProps({data: {syndicusEmail: 'test@test.be', post: {timeStamp: date, imageURL: '', description: 'test2'}}}) + expect(wrapper.vm.getMailBody()).toBeTruthy() + }) + + + +}) diff --git a/frontend/tests/unit/Components/admin/student_template/DagPlanningCard.spec.ts b/frontend/tests/unit/Components/admin/student_template/DagPlanningCard.spec.ts new file mode 100644 index 00000000..244788e4 --- /dev/null +++ b/frontend/tests/unit/Components/admin/student_template/DagPlanningCard.spec.ts @@ -0,0 +1,68 @@ +import {mount} from "@vue/test-utils"; +import DagPlanningCard from "@/components/admin/student_template/DagPlanningCard.vue" + +describe('DagPlanningCard.vue', () => { + + let wrapper; + + beforeEach(() => { + DagPlanningCard.methods.remove_dagplanning = jest.fn() + wrapper = mount(DagPlanningCard); + }) + + it('render of component', () => { + expect(wrapper.exists()).toBeTruthy(); + }) + + it('check if the data gets rendered without "Vervaning"', async () => { + await wrapper.setProps({ + data: { + template_id: 1, + ronde_id: 1, + status: "Actief", + dag_id: 0, + students: [{first_name: 'Test'}], + day: 'MO', + start_hour: '17:00', + end_hour: '20:00' + } + }); + await wrapper.vm.$nextTick(); + expect(wrapper.find('[data-test="students"]').text()).toBe('Test'); + expect(wrapper.find('[data-test="format"]').text()).toBe('Maandag'); + expect(wrapper.find('[data-test="start-hour"]').text()).toBe('17:00 -'); + expect(wrapper.find('[data-test="end-hour"]').text()).toBe('20:00'); + expect(wrapper.find('[data-test="edit"]').exists()).toBeTruthy(); + expect(wrapper.find('[data-test="remove-button"]').exists()).toBeTruthy(); + + expect(wrapper.find('[data-test="message"]').exists()).toBeFalsy(); + }); + + it('test delete button', async () => { + const deleteButton = wrapper.find('[data-test="remove-button"]'); + expect(deleteButton.exists()).toBeTruthy(); + await deleteButton.trigger('click'); + expect(DagPlanningCard.methods.remove_dagplanning).toHaveBeenCalled(); + }) + + it('vervanging render', async () => { + await wrapper.setProps({ + data: { + template_id: 1, + ronde_id: 1, + status: "Vervangen", + dag_id: 0, + students: [{first_name: 'Test'}], + day: 'MO', + start_hour: '17:00', + end_hour: '20:00' + } + }); + await wrapper.vm.$nextTick(); + expect(wrapper.find('[data-test="edit"]').exists()).toBeFalsy(); + expect(wrapper.find('[data-test="remove-button"]').exists()).toBeFalsy(); + + expect(wrapper.find('[data-test="message"]').exists()).toBeTruthy(); + }) + +}); diff --git a/frontend/tests/unit/Components/admin/student_template/StudentTemplateCard.spec.ts b/frontend/tests/unit/Components/admin/student_template/StudentTemplateCard.spec.ts new file mode 100644 index 00000000..a07e7d4a --- /dev/null +++ b/frontend/tests/unit/Components/admin/student_template/StudentTemplateCard.spec.ts @@ -0,0 +1,63 @@ +import {mount} from "@vue/test-utils"; +import StudentTemplateCard from "@/components/admin/student_template/StudentTemplateCard.vue"; + +describe('StudentTemplateCard.vue', () => { + + let wrapper; + + beforeEach(() => { + StudentTemplateCard.methods.delete_template = jest.fn(); + wrapper = mount(StudentTemplateCard); + }); + + it('render of component', () => { + expect(wrapper.exists()).toBeTruthy(); + }); + + it('render of data', async () => { + await wrapper.setProps({ + data: { + template_id: 0, + name: 'Test template', + location: 'Gent', + even: true, + status: 'Actief' + } + }); + await wrapper.vm.$nextTick(); + expect(wrapper.find('[data-test="location"]').text()).toBe('Gent'); + expect(wrapper.find('[data-test="even"]').text()).toBe('even'); + expect(wrapper.find('[data-test="name"]').text()).toBe('Test template'); + expect(wrapper.find('[data-test="status"]').text()).toBe('Actief'); + + expect(wrapper.find('[data-test="edit"]').exists()).toBeTruthy() + expect(wrapper.find('[data-test="delete-button"]').exists()).toBeTruthy() + + expect(wrapper.find('[data-test="message"]').exists()).toBeFalsy() + }); + + it('delete template', async () => { + const deleteButton = wrapper.find('[data-test="delete-button"]'); + await deleteButton.trigger('click'); + expect(StudentTemplateCard.methods.delete_template).toHaveBeenCalled(); + }); + + it('vervangen render', async () => { + await wrapper.setProps({ + data: { + template_id: 0, + name: 'Test template', + location: 'Gent', + even: true, + status: 'Vervangen' + } + }); + await wrapper.vm.$nextTick(); + + expect(wrapper.find('[data-test="edit"]').exists()).toBeFalsy() + expect(wrapper.find('[data-test="delete-button"]').exists()).toBeFalsy() + + expect(wrapper.find('[data-test="message"]').exists()).toBeTruthy() + }) + +}); diff --git a/frontend/tests/unit/Components/admin/student_template/TemplateRondeCard.spec.ts b/frontend/tests/unit/Components/admin/student_template/TemplateRondeCard.spec.ts new file mode 100644 index 00000000..d0e0e640 --- /dev/null +++ b/frontend/tests/unit/Components/admin/student_template/TemplateRondeCard.spec.ts @@ -0,0 +1,55 @@ +import {mount} from "@vue/test-utils"; +import TemplateRondeCard from "@/components/admin/student_template/TemplateRondeCard.vue"; + +describe('TemplateRondeCard.vue', () => { + + let wrapper; + + beforeEach(() => { + TemplateRondeCard.methods.on_delete = jest.fn(); + wrapper = mount(TemplateRondeCard); + }); + + it('render of component', () => { + expect(wrapper.exists()).toBeTruthy(); + }) + + it('render of data', async () => { + await wrapper.setProps({ + data: { + template_id: 0, + ronde_id: 0, + status: "Actief", + name: 'Test Template Ronde', + location: 'Gent' + } + }); + await wrapper.vm.$nextTick(); + expect(wrapper.find('[data-test="location"]').text()).toBe('Gent'); + expect(wrapper.find('[data-test="name"]').text()).toBe('Test Template Ronde'); + + expect(wrapper.find('[data-test="dagplanning-button"]').exists()).toBeTruthy(); + expect(wrapper.find('[data-test="delete-button"]').exists()).toBeTruthy(); + }); + + it('delete template', async () => { + const deleteButton = wrapper.find('[data-test="delete-button"]'); + await deleteButton.trigger('click'); + expect(TemplateRondeCard.methods.on_delete).toHaveBeenCalled(); + }); + + it('template can not delete', async () => { + await wrapper.setProps({ + data: { + template_id: 0, + ronde_id: 0, + status: "Vervangen", + name: 'Test Template Ronde', + location: 'Gent' + } + }); + await wrapper.vm.$nextTick(); + expect(wrapper.find('[data-test="delete-button"]').exists()).toBeFalsy(); + }) + +}); diff --git a/frontend/tests/unit/Components/containerTemplates/trashContainerTemplateTests.spec.js b/frontend/tests/unit/Components/containerTemplates/trashContainerTemplateTests.spec.js new file mode 100644 index 00000000..1b299960 --- /dev/null +++ b/frontend/tests/unit/Components/containerTemplates/trashContainerTemplateTests.spec.js @@ -0,0 +1,228 @@ +import {mount} from '@vue/test-utils' +import TrashContainerTemplateCard from "@/components/containerTemplates/TrashContainerTemplateCard.vue"; +import TrashContainerTemplateList from "@/components/containerTemplates/TrashContainerTemplateList.vue"; +import TrashContainerTemplateCreate from "@/components/containerTemplates/TrashContainerTemplateCreate.vue"; +import TrashContainerTemplateEdit from "@/components/containerTemplates/TrashContainerTemplateEdit.vue"; +import {triggerInput} from "../../../utils/testHelper"; +import TrashContainerTemplateHeader from "@/components/containerTemplates/TrashContainerTemplateHeader.vue"; + +describe('trashContainerTemplateCard.vue', () => { + + let wrapper; + + beforeEach(() => { + TrashContainerTemplateCard.beforeMount = jest.fn(); + wrapper = mount(TrashContainerTemplateCard, { + propsData: { + data: { + id: 1, + name: 'Template 1', + location: 1, + even: true, + } + } + }); + }) + + it('render', () => { + expect(wrapper.exists()).toBe(true); + expect(TrashContainerTemplateCard.beforeMount).toBeCalled(); + }) + + it('displays the props', () => { + expect(wrapper.find('p').text()).toBe('Template 1'); + expect(wrapper.find('[data-test="even"]').text()).toBe("Even"); + }); + + it('displays the data', async () => { + await wrapper.setData({locatie: "1"}) + await wrapper.vm.$nextTick(); + expect(wrapper.find('[data-test="location"]').text()).toBe("1"); + }) + + it('check methods are called', async () => { + TrashContainerTemplateCard.methods.goToTrashTemplateContainersPage = jest.fn(); + TrashContainerTemplateCard.methods.goToTrashTemplateBuildingsPage = jest.fn(); + TrashContainerTemplateCard.methods.editTemplate = jest.fn(); + TrashContainerTemplateCard.methods.deleteTemplate = jest.fn(); + + wrapper = mount(TrashContainerTemplateCard, { + propsData: { + data: { + id: 1, + name: 'Template 1', + year: 2023, + week: 20, + location: 1, + even: true, + } + } + }); + + const goToTrashTemplateContainersPageButton = wrapper.find('[data-test="goToTrashTemplateContainersPage"]'); + const goToTrashTemplateBuildingsPageButton = wrapper.find('[data-test="goToTrashTemplateBuildingsPage"]'); + const editTemplateButton = wrapper.find('[data-test="editTemplate"]'); + const deleteTemplateButton = wrapper.find('[data-test="deleteTemplate"]'); + + + await goToTrashTemplateContainersPageButton.trigger('click'); + expect(TrashContainerTemplateCard.methods.goToTrashTemplateContainersPage).toBeCalled(); + await goToTrashTemplateBuildingsPageButton.trigger('click'); + expect(TrashContainerTemplateCard.methods.goToTrashTemplateBuildingsPage).toBeCalled(); + await deleteTemplateButton.trigger('click'); + expect(TrashContainerTemplateCard.methods.deleteTemplate).toBeCalled(); + }) + +}) + +describe('TrashContainerTemplateList.vue', () => { + + let wrapper; + + beforeEach(() => { + TrashContainerTemplateList.beforeMount = jest.fn(); + wrapper = mount(TrashContainerTemplateList); + }) + + + it('renders the component correctly', () => { + expect(wrapper.exists()).toBe(true); + expect(TrashContainerTemplateList.beforeMount).toBeCalled(); + }); + + it('renders the ListPage component with correct props', () => { + const listPage = wrapper.find('[data-test="listPage"]'); + expect(listPage.exists()).toBe(true); + }); + + // component is empty in test environment waardoor er niet verder getest kan worden + +}); + + +describe('trashContainerTemplateCreate.vue', () => { + + let wrapper; + + beforeEach(() => { + TrashContainerTemplateCreate.beforeMount = jest.fn(); + wrapper = mount(TrashContainerTemplateCreate); + }) + + it('renders the component correctly', () => { + expect(wrapper.exists()).toBe(true); + expect(TrashContainerTemplateCreate.beforeMount).toBeCalled(); + }) + + it('sets textfield correctly', async () => { + const textField = wrapper.find('v-text-field'); + textField.element.value = 'test'; + const activator = (x) => { + return {name: x} + } + await triggerInput(textField, wrapper, activator); + expect(wrapper.vm.name).toBe('test'); + expect(textField.attributes('label')).toBe('Naam'); + }) + + it('sets checkbox correctly', async () => { + const checkbox = wrapper.find('v-checkbox'); + const activator = (x) => { + return {even: x} + } + expect(wrapper.vm.even).toBe(true); + checkbox.element.value = false; + triggerInput(checkbox, wrapper, activator); + expect(wrapper.vm.even).toBe(false); + expect(checkbox.attributes('label')).toBe('Even'); + }) + + it('create button is called', async () => { + TrashContainerTemplateCreate.methods.create = jest.fn(); + wrapper = mount(TrashContainerTemplateCreate); + const createButton = wrapper.find('[data-test="create"]'); + await createButton.trigger('click'); + expect(TrashContainerTemplateCreate.methods.create).toBeCalled(); + }) + + it('v-select exists', () => { + const vSelect = wrapper.find('v-select'); + expect(vSelect.exists()).toBeTruthy() + + expect(vSelect.attributes('label')).toBe('Locatie'); + }) +}) + +describe('trashContainerTemplateEdit.vue', () => { + let wrapper; + + beforeEach(() => { + TrashContainerTemplateEdit.beforeMount = jest.fn(); + wrapper = mount(TrashContainerTemplateEdit) + }) + + it('renders the component correctly', () => { + expect(wrapper.exists()).toBe(true); + expect(TrashContainerTemplateEdit.beforeMount).toBeCalled(); + }) + + it('sets textfield correctly', async () => { + const textField = wrapper.find('v-text-field'); + textField.element.value = 'test'; + const activator = (x) => { + return {name: x} + } + await triggerInput(textField, wrapper, activator); + expect(wrapper.vm.name).toBe('test'); + expect(textField.attributes('label')).toBe('Naam'); + }) + + it('sets checkbox correctly', async () => { + const checkbox = wrapper.find('v-checkbox'); + + const activator = (x) => { + return {even: x} + } + expect(wrapper.vm.even).toBe(true); + checkbox.element.value = false; + triggerInput(checkbox, wrapper, activator); + expect(wrapper.vm.even).toBe(false); + expect(checkbox.attributes('label')).toBe('Even'); + + }) + + it('v-select exists', () => { + const vSelect = wrapper.find('v-select'); + expect(vSelect.attributes('label')).toBe('Locatie'); + }) +}) + +describe('TrashContainerTemplateHeader.vue', () => { + + let wrapper; + + beforeEach(() => { + wrapper = mount(TrashContainerTemplateHeader); + }) + + it('renders the component correctly with the right text', () => { + expect(wrapper.exists()).toBe(true); + + expect(wrapper.find('v-container.border').exists()).toBe(true); + expect(wrapper.find('v-row[align="center"][justify="center"]').exists()).toBe(true); + expect(wrapper.findAll('v-col.col').length).toBe(6); + + expect(wrapper.find('v-col.col:nth-child(1) p[title="Naam"]').text()).toBe('Naam'); + expect(wrapper.find('v-col.col:nth-child(2) p[title="Vuilnisbakken"]').text()).toBe('Vuilnisbakken'); + expect(wrapper.find('v-col.col:nth-child(3) p[title="Gebouwen"]').text()).toBe('Gebouwen'); + expect(wrapper.find('v-col.col:nth-child(4) p[title="Jaar"]').text()).toBe('Status'); + expect(wrapper.find('v-col.col:nth-child(5) p[title="Locatie"]').text()).toBe('Locatie'); + expect(wrapper.find('v-col.col:nth-child(6) p[title="Even"]').text()).toBe('Even'); + expect(wrapper.find('v-col.text-right').text()).toBe('Acties'); + + expect(wrapper.props().round).toBe(false); + + expect(wrapper.vm.$options.name).toBe('TrashContainerTemplateHeader'); + }); + +}) diff --git a/frontend/tests/unit/Components/containerTemplates/trashContainerTests.spec.js b/frontend/tests/unit/Components/containerTemplates/trashContainerTests.spec.js new file mode 100644 index 00000000..84450236 --- /dev/null +++ b/frontend/tests/unit/Components/containerTemplates/trashContainerTests.spec.js @@ -0,0 +1,242 @@ +import {mount} from '@vue/test-utils'; +import TrashContainerCard from "@/components/containerTemplates/containers/TrashContainerCard.vue"; +import TrashContainerCreate from "@/components/containerTemplates/containers/TrashContainerCreate.vue"; +import TrashContainerEdit from "@/components/containerTemplates/containers/TrashContainerEdit.vue"; +import TrashContainerHeader from "@/components/containerTemplates/containers/TrashContainerHeader.vue"; +import {triggerInput} from "../../../utils/testHelper"; +import TrashTemplateContainersList from "@/components/containerTemplates/containers/TrashTemplateContainersList.vue"; + + +describe('trashContainerCard.vue', () => { + + let wrapper; + + beforeEach(() => { + TrashContainerCard.beforeMount = jest.fn(); + TrashContainerCard.methods.editContainer = jest.fn(); + TrashContainerCard.methods.deleteContainer = jest.fn(); + wrapper = mount(TrashContainerCard, { + propsData: { + data: { + trash_container: { + collection_day: { + day: 'MO', + start_hour: '10 AM', + end_hour: '12 PM', + }, + type: 'PM', + }, + extra_id: 1, + }, + }, + }); + }) + + it('renders the component correctly', () => { + expect(wrapper.exists()).toBe(true); + expect(TrashContainerCard.beforeMount).toBeCalled(); + + expect(wrapper.find('.container-border').exists()).toBe(true); + expect(wrapper.find('v-row[align="center"][justify="center"]').exists()).toBe(true); + expect(wrapper.findAll('v-col').length).toBe(6); + + expect(wrapper.find('v-col:nth-child(1) p').text()).toBe('Maandag'); + expect(wrapper.find('v-col:nth-child(2) p').text()).toBe('10 AM - 12 PM'); + expect(wrapper.find('v-col:nth-child(3) p').text()).toBe('PMD'); + + expect(wrapper.find('v-col:nth-child(5) .button-style').exists()).toBe(true); + expect(wrapper.find('v-col:nth-child(6) .button-style').exists()).toBe(true); + }); + + it('edit button is called', async () => { + const editButton = wrapper.find('[data-test="edit"]'); + await editButton.trigger('click'); + expect(TrashContainerCard.methods.editContainer).toBeCalled(); + }) + + it('delete button is called', async () => { + const deleteButton = wrapper.find('[data-test="delete"]'); + await deleteButton.trigger('click'); + expect(TrashContainerCard.methods.deleteContainer).toBeCalled(); + }) + +}) + +describe('trashContainerCreate.vue', () => { + + let wrapper; + + beforeEach(() => { + TrashContainerCreate.beforeMount = jest.fn(); + wrapper = mount(TrashContainerCreate); + }) + + it('renders the component correctly', () => { + expect(wrapper.exists()).toBe(true); + expect(TrashContainerCreate.beforeMount).toBeCalled(); + + expect(wrapper.find('.justify-center.my-10').exists()).toBe(true); + expect(wrapper.find('.text-h2').text()).toBe('Maak een nieuwe container aan'); + + expect(wrapper.find('.my-10.py-5.mx-auto.w-75').exists()).toBe(true); + expect(wrapper.find('v-form[fast-fail]').exists()).toBe(true); + expect(wrapper.findAll('.justify-space-between.mx-auto v-col').length).toBe(4); + + expect(wrapper.find('v-col:nth-child(1) v-select[label="type container"]').exists()).toBe(true); + expect(wrapper.find('v-col:nth-child(2) v-select[label="Dag van de week"]').exists()).toBe(true); + expect(wrapper.find('v-col:nth-child(3) v-text-field[label="Beginuur"]').exists()).toBe(true); + expect(wrapper.find('v-col:nth-child(4) v-text-field[label="Einduur"]').exists()).toBe(true); + + expect(wrapper.find('.overflow-hidden').text()).toBe('Aanmaken'); + }); + + it('createContainer is called', async () => { + TrashContainerCreate.methods.createContainer = jest.fn(); + wrapper = mount(TrashContainerCreate); + const createButton = wrapper.find('.overflow-hidden'); + await createButton.trigger('click'); + expect(TrashContainerCreate.methods.createContainer).toBeCalled(); + }) + + it('sets textfield values correctly', async () => { + const beginUur = wrapper.find('v-col:nth-child(3) v-text-field[label="Beginuur"]'); + const eindUur = wrapper.find('v-col:nth-child(4) v-text-field[label="Einduur"]'); + + const activator1 = (x) => { + return {start_hour: x} + } + + const activator2 = (x) => { + return {end_hour: x} + } + beginUur.element.value = '10 AM'; + eindUur.element.value = '12 PM'; + + await triggerInput(beginUur, wrapper, activator1); + await triggerInput(eindUur, wrapper, activator2); + + expect(wrapper.vm.start_hour).toBe('10 AM'); + expect(wrapper.vm.end_hour).toBe('12 PM'); + }) + + it('check v-select is rendered correctly', () => { + const vSelects = wrapper.findAll('v-select'); + + expect(vSelects.length).toBe(2); + }) + +}) + +describe('trashContainerEdit.vue', () => { + let wrapper; + + beforeEach(() => { + TrashContainerEdit.beforeMount = jest.fn(); + wrapper = mount(TrashContainerEdit); + }) + + it('renders the component correctly', () => { + expect(wrapper.exists()).toBe(true); + + expect(wrapper.find('.justify-center.my-10').exists()).toBe(true); + expect(wrapper.find('.text-h2').text()).toBe('Pas de container aan'); + + expect(wrapper.find('v-form[fast-fail]').exists()).toBe(true); + expect(wrapper.findAll('.justify-space-between.mx-auto v-col').length).toBe(4); + + expect(wrapper.find('v-col:nth-child(1) v-select[label="type container"]').exists()).toBe(true); + expect(wrapper.find('v-col:nth-child(2) v-select[label="dag van de week"]').exists()).toBe(true); + expect(wrapper.find('v-col:nth-child(3) v-text-field[label="Beginuur"]').exists()).toBe(true); + expect(wrapper.find('v-col:nth-child(4) v-text-field[label="Einduur"]').exists()).toBe(true); + + }); + + it('sets textfield values correctly', async () => { + const beginUur = wrapper.find('v-col:nth-child(3) v-text-field[label="Beginuur"]'); + const eindUur = wrapper.find('v-col:nth-child(4) v-text-field[label="Einduur"]'); + + const activator1 = (x) => { + return {start_hour: x} + } + + const activator2 = (x) => { + return {end_hour: x} + } + beginUur.element.value = '10 AM'; + eindUur.element.value = '12 PM'; + + await triggerInput(beginUur, wrapper, activator1); + await triggerInput(eindUur, wrapper, activator2); + + expect(wrapper.vm.start_hour).toBe('10 AM'); + expect(wrapper.vm.end_hour).toBe('12 PM'); + }) + + it('check v-select is rendered correctly', () => { + const vSelects = wrapper.findAll('v-select'); + + expect(vSelects.length).toBe(2); + }) +}) + +describe('TrashContainerHeader', () => { + let wrapper; + + beforeEach(() => { + wrapper = mount(TrashContainerHeader, { + propsData: { + round: true, + }, + }); + }); + + + it('renders the component correctly', () => { + expect(wrapper.exists()).toBe(true); + + expect(wrapper.find('.border').exists()).toBe(true); + expect(wrapper.find('.col').exists()).toBe(true); + expect(wrapper.findAll('.col').length).toBe(4); + + expect(wrapper.find('.col:nth-child(1) p[title="Dag"]').exists()).toBe(true); + expect(wrapper.find('.col:nth-child(2) p[title="Uren"]').exists()).toBe(true); + expect(wrapper.find('.col:nth-child(3) p[title="Type"]').exists()).toBe(true); + + expect(wrapper.find('[data-test="acties"]').text()).toBe('Acties'); + }); + + it('sets the "round" prop correctly', async () => { + expect(wrapper.props('round')).toBe(true); + + await wrapper.setProps({ + round: false, + }); + + expect(wrapper.props('round')).toBe(false); + }); +}); + + +describe('TrashContainerTemplateList.vue', () => { + + let wrapper; + + beforeEach(() => { + TrashTemplateContainersList.beforeMount = jest.fn(); + wrapper = mount(TrashTemplateContainersList); + }) + + + it('renders the component correctly', () => { + expect(wrapper.exists()).toBe(true); + expect(TrashTemplateContainersList.beforeMount).toBeCalled(); + }); + + it('renders the ListPage component with correct props', () => { + const listPage = wrapper.find('[data-test="listPage"]'); + expect(listPage.exists()).toBe(true); + }); + + // component is empty in test environment waardoor er niet verder getest kan worden + +}); diff --git a/frontend/tests/unit/Components/containerTemplates/trashTemplateBuildingAddTests.spec.js b/frontend/tests/unit/Components/containerTemplates/trashTemplateBuildingAddTests.spec.js new file mode 100644 index 00000000..bf2470cf --- /dev/null +++ b/frontend/tests/unit/Components/containerTemplates/trashTemplateBuildingAddTests.spec.js @@ -0,0 +1,179 @@ +import {mount} from '@vue/test-utils'; +import TrashTemplateBuildingAdd from "@/components/containerTemplates/buildings/TrashTemplateBuildingAdd.vue"; +import TrashTemplateBuildingCard from "@/components/containerTemplates/buildings/TrashTemplateBuildingCard.vue"; +import TrashTemplateBuildingEdit from "@/components/containerTemplates/buildings/TrashTemplateBuildingEdit.vue"; +import TrashTemplateBuildingHeader from "@/components/containerTemplates/buildings/TrashTemplateBuildingHeader.vue"; +import TrashTemplateBuildingsList from "@/components/containerTemplates/buildings/TrashTemplateBuildingsList.vue"; + +describe('TrashTemplateBuildingAdd.vue', () => { + + let wrapper; + + beforeEach(() => { + TrashTemplateBuildingAdd.beforeMount = jest.fn(); + wrapper = mount(TrashTemplateBuildingAdd) + }); + + it('render', () => { + expect(wrapper.exists()).toBe(true); + expect(TrashTemplateBuildingAdd.beforeMount).toBeCalled(); + }) + + it('initializes data correctly', () => { + expect(wrapper.vm.building_options).toEqual([]); + expect(wrapper.vm.building_chosen).toEqual([]); + expect(wrapper.vm.building_originals).toEqual([]); + expect(wrapper.vm.status).toBe('I'); + }); + + it('redenders the component correctly', () => { + expect(wrapper.find('div[class="text-h2"]').text()).toBe("Kies gebouwen voor deze template"); + expect(wrapper.find('v-select[label="Gekozen gebouwen"]').exists()).toBe(true); + }) + +}) + +describe('TrashTemplateBuildingCard.vue', () => { + let wrapper; + + beforeEach(() => { + TrashTemplateBuildingCard.mounted = jest.fn(); + TrashTemplateBuildingCard.beforeMount = jest.fn(); + TrashTemplateBuildingCard.methods.downloadDocument = jest.fn(); + TrashTemplateBuildingCard.methods.goToBuildingPage = jest.fn(); + + wrapper = mount(TrashTemplateBuildingCard, { + data: () => ({ + building: { + id: 1, + name: 'Building Name', + adres: 'Building Address', + efficiency: 80, + manual: { + id: 1, + manualStatus: 'Klaar', + file: 'manual.pdf', + }, + }, + status: 'I', + }) + }); + + + }); + + + it('renders the component correctly', () => { + expect(wrapper.exists()).toBe(true); + expect(TrashTemplateBuildingCard.beforeMount).toBeCalled(); + expect(wrapper.find('span').text()).toBe('I'); + }) + + it('display the correct building data', () => { + const building = wrapper.findAll('p'); + expect(building.at(0).text()).toBe('Building Name'); + expect(building.at(1).text()).toBe('Building Address'); + expect(building.at(2).text()).toBe('80%'); + }) + + it('downloadDocument method is called', async () => { + const downloadButton = wrapper.find('[value="download"]'); + await downloadButton.trigger('click'); + expect(TrashTemplateBuildingCard.methods.downloadDocument).toBeCalled(); + }); + + it('goToBuildingPage method is called', async () => { + const goToBuildingPageButton = wrapper.find('p[class="text-style-building"]'); + await goToBuildingPageButton.trigger('click'); + expect(TrashTemplateBuildingCard.methods.goToBuildingPage).toBeCalled(); + }); + +}) + + +describe('TrashTemplateBuildingEdit.vue', () => { + + let wrapper; + + beforeEach(() => { + const building = { + name: 'Building Name', + adres: 'Building Address', + } + + TrashTemplateBuildingEdit.beforeMount = jest.fn(); + + wrapper = mount(TrashTemplateBuildingEdit, { + data: () => ({ + building: {building: building}, + }) + }); + }) + + it('renders the component correctly', () => { + expect(wrapper.exists()).toBe(true); + expect(TrashTemplateBuildingEdit.beforeMount).toBeCalled(); + expect(wrapper.find('div[class="text-h2"]').text()).toBe('Pas de containers van dit gebouw aan.'); + expect(wrapper.find('v-select[label="Kies containers voor dit gebouw"]').exists()).toBe(true); + }) + + it('renders the correct building data', () => { + const building = wrapper.findAll('p'); + expect(building.at(0).text()).toBe('Building Name'); + expect(building.at(1).text()).toBe('Building Address'); + }) +}) + +describe('TrashTemplateBuildingHeader.vue', () => { + + let wrapper; + + beforeEach(() => { + wrapper = mount(TrashTemplateBuildingHeader) + }) + + it('renders the component', () => { + expect(wrapper.exists()).toBe(true); + }); + + it('renders the correct elements', () => { + expect(wrapper.find('v-container').exists()).toBe(true); + expect(wrapper.find('v-row').exists()).toBe(true); + expect(wrapper.findAll('v-col').length).toBe(6); + expect(wrapper.find('p[title="Gebouw"]').exists()).toBe(true); + expect(wrapper.find('p[title="Adres"]').exists()).toBe(true); + expect(wrapper.find('p[title="Efficiëntie"]').exists()).toBe(true); + expect(wrapper.find('p[title="Handleiding"]').exists()).toBe(true); + }); + + it('renders the "Efficiëntie" element based on the "round" prop', async () => { + const wrapperWithRoundProp = mount(TrashTemplateBuildingHeader, { + propsData: {round: true}, + }); + const wrapperWithoutRoundProp = mount(TrashTemplateBuildingHeader, { + propsData: {round: false}, + }); + + expect(wrapperWithRoundProp.find('p[title="Efficiëntie"]').exists()).toBe(false); + expect(wrapperWithoutRoundProp.find('p[title="Efficiëntie"]').exists()).toBe(true); + }); +}) + +describe('TrashTemplateBuildingsList.vue', () => { + + let wrapper; + beforeEach(() => { + TrashTemplateBuildingsList.beforeMount = jest.fn(); + wrapper = mount(TrashTemplateBuildingsList) + }) + + it('renders the component', () => { + expect(wrapper.exists()).toBe(true); + expect(TrashTemplateBuildingsList.beforeMount).toBeCalled(); + }) + + it('renders the ListPage component with correct props', () => { + const listPage = wrapper.find('[data-test="listPage"]'); + expect(listPage.exists()).toBe(true); + }); +}) diff --git a/frontend/tests/unit/Components/icons/iconTest.spec.ts b/frontend/tests/unit/Components/icons/iconTest.spec.ts new file mode 100644 index 00000000..05748fc3 --- /dev/null +++ b/frontend/tests/unit/Components/icons/iconTest.spec.ts @@ -0,0 +1,38 @@ +import { mount } from '@vue/test-utils' +import DeleteIcon from '@/components/icons/DeleteIcon.vue' +import EditIcon from '@/components/icons/EditIcon.vue' +import InfoIcon from '@/components/icons/InfoIcon.vue' + + +describe('Test Icon components', () => { + it('renders a delete icon with the correct color', () => { + const wrapper = mount(DeleteIcon) + const avatar = wrapper.find('.button-style') + const icon = avatar.find('v-icon') + + expect(avatar.attributes('color')).toBe('#F0533D') + expect(icon.text()).toBe('mdi-delete') + expect(icon.attributes('color')).toBe('white') + }) + + it('renders a Edit icon with the correct color', () => { + const wrapper = mount(EditIcon) + const avatar = wrapper.find('.button-style') + const icon = avatar.find('v-icon') + + expect(avatar.attributes('color')).toBe('#FFE600') + expect(icon.text()).toBe('mdi-pencil') + expect(icon.attributes('color')).toBe('white') + }) + + it('renders a Edit icon with the correct color', () => { + const wrapper = mount(InfoIcon) + const avatar = wrapper.find('.button-style') + const icon = avatar.find('v-icon') + + expect(avatar.attributes('color')).toBe('#9DCF62') + expect(icon.text()).toBe('mdi-information-variant') + expect(icon.attributes('color')).toBe('white') + }) +}) + diff --git a/frontend/tests/unit/Components/student/InfoScreenBuildingTest.spec.ts b/frontend/tests/unit/Components/student/InfoScreenBuildingTest.spec.ts new file mode 100644 index 00000000..68e4d12d --- /dev/null +++ b/frontend/tests/unit/Components/student/InfoScreenBuildingTest.spec.ts @@ -0,0 +1,55 @@ +import { mount } from "@vue/test-utils"; +import InfoScreenBuilding from "@/components/student/InfoScreenBuilding.vue"; + +describe("InfoScreenBuilding.vue", () => { + + let wrapper; + + beforeEach(() => { + InfoScreenBuilding.created = jest.fn() + wrapper = mount(InfoScreenBuilding); + }) + + it("renders the component", () => { + expect(wrapper.exists()).toBe(true); + }) + + it("call downloadFile methode when clicked on the download button", async () => { + InfoScreenBuilding.methods.downloadFile = jest.fn() + const wrapper = mount(InfoScreenBuilding) + const downloadButton = wrapper.find('[data-test="download-button"]'); + await downloadButton.trigger('click') + await wrapper.vm.$nextTick(); + expect(InfoScreenBuilding.methods.downloadFile).toHaveBeenCalled() + }) + + it("render name building", async () => { + await wrapper.setData({building: {name: 'test'}}) + const headerName = wrapper.find('[data-test="buildingName"]') + expect(headerName.text()).toBe('test') + }) + + it("render text", async () => { + const p = wrapper.find('p') + const opmerking = wrapper.find('h2') + expect(p.text()).toBe('Handleiding') + expect(opmerking.text()).toBe('Opmerkingen:') + }) + + + it('renders remarks correct', async () => { + const remarks = ['Remark 1', 'Remark 2', 'Remark 3'] + + await wrapper.setData({building: {remarks: remarks}}) + wrapper.vm.$nextTick(); + wrapper.vm.$forceUpdate() + + const remarkItems = wrapper.findAll('[data-test="remarks"]'); + expect(remarkItems).toHaveLength(remarks.length); + + remarkItems.forEach((item, index) => { + expect(item.text()).toBe(remarks[index]); + }); + }); + +}) diff --git a/frontend/tests/unit/Components/student/dayPlanBuildingTest.spec.ts b/frontend/tests/unit/Components/student/dayPlanBuildingTest.spec.ts new file mode 100644 index 00000000..185ff910 --- /dev/null +++ b/frontend/tests/unit/Components/student/dayPlanBuildingTest.spec.ts @@ -0,0 +1,80 @@ +import { mount } from "@vue/test-utils"; +import DayPlanBuilding from "@/components/student/DayPlanBuilding.vue"; + +describe("dayPlanBuilding.vue", () => { + + let wrapper; + + beforeEach(() => { + wrapper = mount(DayPlanBuilding); + }) + + it("renders", () => { + expect(wrapper.exists()).toBe(true); + }); + + it("buildingClicked is called when clicked on the building", async () => { + DayPlanBuilding.methods.buildingClicked = jest.fn() + wrapper = mount(DayPlanBuilding) + const building = wrapper.find('[data-test="building"]'); + await building.trigger('click') + await wrapper.vm.$nextTick(); + expect(DayPlanBuilding.methods.buildingClicked).toHaveBeenCalled() + }) + + it("render important components", () => { + expect(wrapper.find('v-card')).toBeTruthy() + expect(wrapper.find('v-card-title')).toBeTruthy() + expect(wrapper.find('[data-test="building"]')).toBeTruthy() + }) + + it("render status div", async () => { + expect(wrapper.find('[data-test="status"]').text()).toBe("Status") + }) + + it("test colour unknown", async () => { + const data = {building: {status: 'Onbekend'}} + await wrapper.setProps({data: data}) + await wrapper.vm.$nextTick(); + const building = wrapper.find('[data-test="building"]'); + expect(building.attributes().color).toBe('red-lighten-1') + }) + + it("test colour succeeded", async () => { + const data = {building: {status: 'Voltooid'}} + await wrapper.setProps({data: data}) + await wrapper.vm.$nextTick(); + const building = wrapper.find('[data-test="building"]'); + expect(building.attributes().color).toBe('green-lighten-1') + }) + + it("test colour still running", async () => { + const data = {building: {status: 'Bezig'}} + await wrapper.setProps({data: data}) + await wrapper.vm.$nextTick(); + const building = wrapper.find('[data-test="building"]'); + expect(building.attributes().color).toBe('yellow-lighten-1') + }) + + it("check building name", async () => { + const data = {building: {name: 'test'}} + await wrapper.setProps({data: data}) + const name = wrapper.find('[data-test="name-building"]'); + expect(name.text()).toBe('test') + }) + + it("check building adres", async () => { + const data = {building: {adres: 'test'}} + await wrapper.setProps({data: data}) + const name = wrapper.find('[data-test="adres-building"]'); + expect(name.text()).toBe('test') + }) + + it("check building status", async () => { + const data = {building: {status: 'test'}} + await wrapper.setProps({data: data}) + const name = wrapper.find('[data-test="status-building"]'); + expect(name.text()).toBe('test') + }) + +}); diff --git a/frontend/tests/unit/Components/student/fotoCardStudentTest.spec.ts b/frontend/tests/unit/Components/student/fotoCardStudentTest.spec.ts new file mode 100644 index 00000000..53a77a5f --- /dev/null +++ b/frontend/tests/unit/Components/student/fotoCardStudentTest.spec.ts @@ -0,0 +1,54 @@ +import { mount } from "@vue/test-utils"; +import FotoCardStudent from "@/components/student/FotoCardStudent.vue"; + +describe("InfoScreenBuilding.vue", () => { + + let wrapper; + + beforeEach(() => { + wrapper = mount(FotoCardStudent); + }) + + it("renders the component", () => { + expect(wrapper.exists()).toBe(true); + }) + + it("goToEditPage is called when clicked on the edit button", async () => { + FotoCardStudent.methods.goToEditPage = jest.fn() + wrapper = mount(FotoCardStudent) + const editButton = wrapper.find('[data-test="edit-button"]'); + await editButton.trigger('click') + await wrapper.vm.$nextTick(); + expect(FotoCardStudent.methods.goToEditPage).toHaveBeenCalled() + }) + + it("deletPost is called when clicked on the delete button", async () => { + FotoCardStudent.methods.deletePost = jest.fn() + wrapper = mount(FotoCardStudent) + const deleteButton = wrapper.find('[data-test="delete-button"]'); + await deleteButton.trigger('click') + await wrapper.vm.$nextTick(); + expect(FotoCardStudent.methods.deletePost).toHaveBeenCalled() + }) + + it("renders the important information", () => { + expect(wrapper.find('v-card')).toBeTruthy() + expect(wrapper.find('v-card-text')).toBeTruthy() + expect(wrapper.find('[data-test="edit-button"]')).toBeTruthy() + expect(wrapper.find('[data-test="delete-button"]')).toBeTruthy() + }) + + it("renders the props data", async () => { + const data = {time: new Date(), remark: "testRemark", image: "test",id: 1} + await wrapper.setProps({data: data}) + await wrapper.vm.$nextTick(); + const remark = wrapper.find('[data-test="remark"]'); + const image = wrapper.find('[data-test="image"]'); + const time = wrapper.find('[data-test="time"]'); + expect(remark.text()).toMatch(data.remark) + expect(image.attributes().src).toMatch(data.image) + expect(time.text()).toMatch(data.time.toLocaleString()) + }) + + +}) diff --git a/frontend/tests/unit/Components/student/studentPosts.spec.ts b/frontend/tests/unit/Components/student/studentPosts.spec.ts new file mode 100644 index 00000000..894c83b3 --- /dev/null +++ b/frontend/tests/unit/Components/student/studentPosts.spec.ts @@ -0,0 +1,133 @@ +import {mount} from '@vue/test-utils' +import CreateEditPostStudent from "@/components/student/CreateEditPostStudent.vue"; +import {triggerInput} from "../../../utils/testHelper"; +import OverviewScreenStudentPosts from "@/components/student/OverviewScreenStudentPosts.vue"; + +describe('CreateEditPostStudent.vue', () => { + + let wrapper; + + beforeEach(() => { + wrapper = mount(CreateEditPostStudent) + }) + + it('renders the component', () => { + expect(wrapper.exists()).toBe(true); + }); + + it('Render "Klik om een foto toe te voegen"', () => { + expect(wrapper.find('p').text()).toMatch('Klik om een foto toe te voegen') + }); + + it('remove image is called when the remove button is clicked and deleted the image', async () => { + await wrapper.setData({imageUrl: 'test'}) + await wrapper.vm.$nextTick(); + const removeButton = wrapper.find('[data-test="delete-button"]'); + await removeButton.trigger('click') + await wrapper.vm.$nextTick(); + expect(wrapper.vm.imageUrl).toBe("") + }) + + it('call selectImage when clicked in the square', async () => { + CreateEditPostStudent.methods.selectImage = jest.fn() + const wrapper = mount(CreateEditPostStudent) + const square = wrapper.find('[data-test="square-button"]'); + await square.trigger('click') + await wrapper.vm.$nextTick(); + expect(CreateEditPostStudent.methods.selectImage).toHaveBeenCalled() + }) + + it('call uploadData when clicked on the upload button', async () => { + CreateEditPostStudent.methods.uploadData = jest.fn() + const wrapper = mount(CreateEditPostStudent) + const uploadButton = wrapper.find('[data-test="upload-button"]'); + await uploadButton.trigger('click') + await wrapper.vm.$nextTick(); + expect(CreateEditPostStudent.methods.uploadData).toHaveBeenCalled() + }) + + it('call editData when clicked on the edit button', async () => { + CreateEditPostStudent.methods.editData = jest.fn() + const wrapper = mount(CreateEditPostStudent) + await wrapper.setProps({data: {edit: true}}) + await wrapper.vm.$nextTick(); + const editButton = wrapper.find('[data-test="edit-button"]'); + await editButton.trigger('click') + await wrapper.vm.$nextTick(); + expect(CreateEditPostStudent.methods.editData).toHaveBeenCalled() + }) + + it('image check', async () => { + await wrapper.setData({imageUrl: 'test'}) + await wrapper.vm.$nextTick(); + expect(wrapper.vm.imageCheck).toBeTruthy() + }) + + it('set description', async () => { + const description = wrapper.find('[data-test="description"]'); + description.element.value = 'test'; + const activator = (x) => { + return {description: x} + } + triggerInput(description, wrapper, activator) + expect(wrapper.vm.description).toBe('test') + }) + + /*it('show building name and type', async () => { + const data = {nameBuilding: 'test', type: 'test', info: '', edit: false, id: '', planning: '', building_id: ''} + await wrapper.setProps({data: {nameBuilding: 'test', type: 'test', info: '', edit: false, id: '', planning: '', building_id: ''}}) + wrapper.vm.$forceUpdate() + const buildingName = wrapper.findComponent('[data-test="buildingName"]') + const buildingType = wrapper.find('[data-test="buildingType"]') + + expect(buildingName.text()).toBe(data.nameBuilding) + expect(buildingType.text()).toBe(data.type) + })*/ +}) + +describe('OverviewScreenStudentPost.vue', () => { + let wrapper; + + beforeEach(() => { + wrapper = mount(OverviewScreenStudentPosts) + }) + + it('renders the component', () => { + expect(wrapper.exists()).toBe(true); + }) + + it('check building name', async () => { + await wrapper.setProps({ + data: {buildingName: 'test', type: 'test', images: [], info: '', planning: '', building_id: ''} + }) + + expect(wrapper.find('h1').text()).toBe('test') + }) + + it('check type', async () => { + await wrapper.setProps({ + data: {buildingName: 'test', type: 'test2', images: [], info: '', planning: '', building_id: ''} + }) + + expect(wrapper.find('h4').text()).toBe('test2') + }) + + it('goToPhotoPage is called when clicked on the icon', async () => { + OverviewScreenStudentPosts.methods.goToPhotoPage = jest.fn() + const wrapper = mount(OverviewScreenStudentPosts) + const icon = wrapper.find('[data-test="goToFotoPage-button"]'); + await icon.trigger('click') + await wrapper.vm.$nextTick(); + expect(OverviewScreenStudentPosts.methods.goToPhotoPage).toHaveBeenCalled() + }) + + it('completeTask is called when clicked on the icon', async () => { + OverviewScreenStudentPosts.methods.completeTask = jest.fn() + const wrapper = mount(OverviewScreenStudentPosts) + const icon = wrapper.find('[data-test="completeTask-button"]'); + await icon.trigger('click') + await wrapper.vm.$nextTick(); + expect(OverviewScreenStudentPosts.methods.completeTask).toHaveBeenCalled() + }) + +}) diff --git a/frontend/tests/unit/Components/syndicus/fotoCardSyndicusTest.spec.ts b/frontend/tests/unit/Components/syndicus/fotoCardSyndicusTest.spec.ts new file mode 100644 index 00000000..999d5d7a --- /dev/null +++ b/frontend/tests/unit/Components/syndicus/fotoCardSyndicusTest.spec.ts @@ -0,0 +1,33 @@ +import {mount} from "@vue/test-utils"; +import FotoCardSyndicus from "@/components/syndicus/FotoCardSyndicus.vue"; + + +describe("FotoCardSyndicus.vue", () => { + + let wrapper; + beforeEach(() => { + wrapper = mount(FotoCardSyndicus); + }) + + it("renders the component", () => { + expect(wrapper.exists()).toBe(true); + }) + + it("renders the important information", () => { + expect(wrapper.find('v-card')).toBeTruthy() + expect(wrapper.find('v-card-text')).toBeTruthy() + }) + + it("renders the props data", async () => { + const dataSyndicus = {time: new Date(), remark: "test Description", image: "test"} + await wrapper.setProps({data: dataSyndicus}) + await wrapper.vm.$nextTick(); + const description = wrapper.find('[data-test="description"]'); + const image = wrapper.find('[data-test="image"]'); + const time = wrapper.find('[data-test="time"]'); + expect(description.text()).toMatch(dataSyndicus.remark) + expect(image.attributes().src).toMatch(dataSyndicus.image) + expect(time.text()).toMatch(dataSyndicus.time.toString()) + }) + +}) diff --git a/frontend/tests/unit/Components/util/confirmDialogTest.spec.ts b/frontend/tests/unit/Components/util/confirmDialogTest.spec.ts new file mode 100644 index 00000000..752d7221 --- /dev/null +++ b/frontend/tests/unit/Components/util/confirmDialogTest.spec.ts @@ -0,0 +1,56 @@ +import {mount} from '@vue/test-utils' +import ConfirmDialog from "@/components/util/ConfirmDialog.vue"; + +describe('ConfirmDialog.vue', () => { + + let wrapper; + + beforeEach(() => { + wrapper = mount(ConfirmDialog); + }) + + it('render', () => { + expect(wrapper.exists()).toBe(true); + }) + + it('check that it renders the props', async () => { + const confirmFunctionMock = jest.fn(); + + await wrapper.setProps( + { + text: 'test', + confirm_function: confirmFunctionMock + } + ) + + const text = wrapper.find('p') + expect(text.text()).toBe('test') + const confirmButton = wrapper.find('[data-test="confirm_button"]') + expect(confirmButton.text()).toBe('Ja') + + wrapper.vm.open(); + + confirmButton.trigger('click') + + expect(confirmFunctionMock).toHaveBeenCalled(); + }) + + it('check that dialog is closed', async () => { + wrapper.setData({dialog: true}) + + const closeButton = wrapper.find('[data-test="close_button"]') + + closeButton.trigger('click') + + expect(closeButton.text()).toBe('Nee') + + expect(wrapper.vm.dialog).toBe(false) + }) + + it('check important components', () => { + expect(wrapper.find('v-dialog')).toBeTruthy() + expect(wrapper.find('v-card')).toBeTruthy() + }) + + +}) diff --git a/frontend/tests/unit/Views/Unauthorized.spec.ts b/frontend/tests/unit/Views/Unauthorized.spec.ts new file mode 100644 index 00000000..84f928ba --- /dev/null +++ b/frontend/tests/unit/Views/Unauthorized.spec.ts @@ -0,0 +1,36 @@ +import Unauthorized from '../../../src/views/Unauthorized.vue'; +import {mount} from "@vue/test-utils"; +import NormalButton from "@/components/NormalButton.vue"; + +describe("Unauthorized.vue", () => { + + let wrapper; + + beforeEach(() => { + wrapper = mount(Unauthorized); + }); + + it('render the component', () => { + expect(wrapper.exists()).toBeTruthy(); + }) + + it('icon of unauthorized present', () => { + expect(wrapper.find('v-icon').attributes('icon')).toBe("mdi-cancel"); + }); + + it('text geen toegang', () => { + expect(wrapper.findAll('div').filter(div => div.text() === 'Geen toegang!').length).toBe(1); + }); + + it('text permission', () => { + expect(wrapper.findAll('div') + .filter(div => div.text() === 'U heeft onvoldoende rechten om deze pagina te bezoeken.') + .length).toBe(1); + }) + + it('normal button with go back exist', () => { + expect(wrapper.findComponent(NormalButton).exists()).toBeTruthy(); + }) + + +}); diff --git a/frontend/tests/unit/views/admin/CreateEditRoundView.spec.js b/frontend/tests/unit/views/admin/CreateEditRoundView.spec.js new file mode 100644 index 00000000..717faadc --- /dev/null +++ b/frontend/tests/unit/views/admin/CreateEditRoundView.spec.js @@ -0,0 +1,57 @@ +import { mount } from '@vue/test-utils' +import CreateEditRoundView from '@/views/admin/CreateEditRoundView.vue' + +describe('CreateEditRoundView.vue', () => { + + let wrapper; + + beforeEach(() => { + CreateEditRoundView.beforeCreate = jest.fn(); + wrapper = mount(CreateEditRoundView); + }) + + it('renders the correct', () => { + expect(wrapper.exists()).toBe(true); + expect(CreateEditRoundView.beforeCreate).toHaveBeenCalled(); + }) + + it('renders the correct title when id is undefined', () => { + const title = wrapper.find('h4.text-h4'); + expect(title.text()).toBe('Nieuwe ronde aanmaken'); + }); + + it('renders the correct title when id is defined', () => { + const wrapper = mount(CreateEditRoundView, { + propsData: { + id: '123' + } + }); + const title = wrapper.find('h4.text-h4'); + expect(title.text()).toBe('Ronde bewerken'); + }); + + it('calls createRound method when "Aanmaken" button is clicked', async () => { + CreateEditRoundView.methods.createRound = jest.fn(); + wrapper = mount(CreateEditRoundView); + const createButton = wrapper.find('[data-test="create"]'); + await createButton.trigger('click'); + expect(CreateEditRoundView.methods.createRound).toHaveBeenCalled(); + }); + + it('calls createRound method when "Opslaan" button is clicked', async () => { + CreateEditRoundView.methods.createRound = jest.fn(); + const wrapper = mount(CreateEditRoundView, { + propsData: { + id: '123' + } + }); + const saveButton = wrapper.find('[data-test="save"]'); + await saveButton.trigger('click'); + expect(CreateEditRoundView.methods.createRound).toHaveBeenCalled(); + }); + + it('renders the component correctly', async() => { + expect(wrapper.find('v-autocomplete[label="Locatie"]').exists()).toBeTruthy(); + expect(wrapper.find('v-autocomplete[label="Gebouwen"]').exists()).toBeTruthy(); + }) +}) diff --git a/frontend/tests/unit/views/admin/adminRoundViewTest.spec.js b/frontend/tests/unit/views/admin/adminRoundViewTest.spec.js new file mode 100644 index 00000000..4e7e3694 --- /dev/null +++ b/frontend/tests/unit/views/admin/adminRoundViewTest.spec.js @@ -0,0 +1,59 @@ +import { mount } from '@vue/test-utils' +import AdminRoundView from '@/views/admin/AdminRoundView.vue' + +describe('AdminRoundView.vue', () => { + let wrapper; + beforeEach(() => { + AdminRoundView.created = jest.fn(); + wrapper = mount(AdminRoundView); + }) + + it('renders the correct', () => { + expect(wrapper.exists()).toBe(true); + expect(AdminRoundView.created).toHaveBeenCalled(); + }) + + it('sets initial data correctly', () => { + const initialData = wrapper.vm.$data; + expect(initialData.date).toBeNull(); + expect(initialData.dateString).toBe(''); + expect(initialData.planning).toBeNull(); + expect(initialData.pictures).toBeNull(); + expect(initialData.duration).toBeNull(); + expect(initialData.template).toBeNull(); + expect(initialData.status).toBe('Niet voltooid'); + }); + + it('renders the component correctly', async() => { + const wrapper = mount(AdminRoundView, { + data() { + return { + planning: { + students: [ + { id: 1, first_name: 'John', last_name: 'Doe' }, + { id: 2, first_name: 'Jane', last_name: 'Smith' }, + ], + ronde:{name: 'Ronde 1'} + }, + pictures: [], + }; + }, + }); + + await wrapper.vm.$forceUpdate(); + const studentNames = wrapper.findAll('h2'); + expect(studentNames.length).toBe(2); + expect(studentNames[0].text()).toBe('John Doe'); + expect(studentNames[1].text()).toBe('Jane Smith'); + + expect(wrapper.find('h1').text()).toBe('Ronde Ronde 1 op door'); + + const titles = wrapper.findAll('h5'); + expect(titles.at(0).text()).toBe('Gebouw'); + expect(titles.at(1).text()).toBe('Status'); + expect(titles.at(2).text()).toBe('Opmerkingen'); + expect(titles.at(3).text()).toBe('Tijd'); + expect(titles.at(4).text()).toBe('Locatie'); + }); + +}) diff --git a/frontend/tests/unit/views/admin/createBuildingViewTest.spec.js b/frontend/tests/unit/views/admin/createBuildingViewTest.spec.js new file mode 100644 index 00000000..a3bfb1f1 --- /dev/null +++ b/frontend/tests/unit/views/admin/createBuildingViewTest.spec.js @@ -0,0 +1,43 @@ +import { mount } from '@vue/test-utils' +import CreateBuildingView from '@/views/admin/CreateBuildingView.vue' + +describe('CreateBuildingView.vue', () => { + + let wrapper; + + beforeEach(() => { + CreateBuildingView.beforeMount = jest.fn(); + wrapper = mount(CreateBuildingView); + }) + + it('renders the correct', () => { + expect(wrapper.exists()).toBe(true); + expect(CreateBuildingView.beforeMount).toHaveBeenCalled(); + }) + + it('initializes the data properties correctly', () => { + expect(wrapper.vm.name).toBe(''); + expect(wrapper.vm.adres).toBe(''); + expect(wrapper.vm.klant_nr).toBe(null); + expect(wrapper.vm.file).toBe(null); + expect(wrapper.vm.smallScreen).toBe(false); + expect(wrapper.vm.locations).toEqual([]); + expect(wrapper.vm.selectedLocation).toBe(null); + expect(wrapper.vm.errors).toBe(null); + }); + + it('call createBuilding when form is submitted', async () => { + CreateBuildingView.methods.createBuilding = jest.fn(); + wrapper = mount(CreateBuildingView); + const button = wrapper.find('[date-test="create"]'); + await button.trigger('click'); + expect(CreateBuildingView.methods.createBuilding).toHaveBeenCalled(); + }) + + it('renders the component correctly', async() => { + expect(wrapper.find('h1').text()).toBe('Nieuw gebouw aanmaken'); + expect(wrapper.find('v-select[label="Locatie"]').exists()).toBeTruthy(); + expect(wrapper.find('v-text-field[label="Klanten nummer"]').exists()).toBeTruthy(); + expect(wrapper.find('v-file-input[label="Handleiding"]').exists()).toBeTruthy(); + }) +}) diff --git a/frontend/tests/unit/views/admin/createLocationViewTest.spec.js b/frontend/tests/unit/views/admin/createLocationViewTest.spec.js new file mode 100644 index 00000000..dd5110dc --- /dev/null +++ b/frontend/tests/unit/views/admin/createLocationViewTest.spec.js @@ -0,0 +1,45 @@ +import { mount } from '@vue/test-utils' +import CreateLocationView from '@/views/admin/CreateLocationView.vue' +import {triggerInput} from "../../../utils/testHelper"; + +describe('CreateLocationView.vue', () => { + let wrapper; + + beforeEach(() => { + wrapper = mount(CreateLocationView); + }) + + it('renders the correct', () => { + expect(wrapper.exists()).toBe(true); + }) + + it('initializes data correctly', () => { + expect(wrapper.vm.name).toBe(''); + expect(wrapper.vm.errors).toBe(null); + }); + + it('calls addLocation method on button click', async () => { + CreateLocationView.methods.addLocation = jest.fn(); + wrapper = mount(CreateLocationView); + await wrapper.find('[data-test="add"]').trigger('click'); + + expect(CreateLocationView.methods.addLocation).toHaveBeenCalled(); + }); + + it('renders the component correctly', async() => { + const h1s = wrapper.findAll('h1'); + expect(h1s.at(0).text()).toBe('Nieuwe locatie aanmaken'); + expect(h1s.at(1).text()).toBe('Naam'); + }) + + it('updates name when v-model is changed', async () => { + const input = wrapper.find('v-text-field'); + input.element.value = 'New Location'; + const activator = (x) => { + return {name: x} + } + triggerInput(input, wrapper, activator) + + expect(wrapper.vm.name).toBe('New Location'); + }); +}) diff --git a/frontend/tests/unit/views/otherTests.spec.js b/frontend/tests/unit/views/otherTests.spec.js new file mode 100644 index 00000000..c227e3a9 --- /dev/null +++ b/frontend/tests/unit/views/otherTests.spec.js @@ -0,0 +1,55 @@ +import { mount } from '@vue/test-utils'; +import HomeView from "@/views/HomeView.vue"; +import Unauthorized from "@/views/Unauthorized.vue"; + +describe('HomeView', () => { + + + it('create is called when component is mounted', () => { + HomeView.created = jest.fn(); + mount(HomeView); + expect(HomeView.created).toHaveBeenCalled(); + }) + + it('renders the correct', () => { + HomeView.created = jest.fn(); + const wrapper = mount(HomeView); + expect(wrapper.exists()).toBe(true); + }) + +}); + +describe('Unauthorized.vue', () => { + + let wrapper; + + beforeEach(() => { + wrapper = mount(Unauthorized); + }) + + it('renders the correct', () => { + expect(wrapper.exists()).toBe(true); + }) + + it('displays the "Unauthorized" message', () => { + const message = wrapper.find('.mx-1'); + expect(message.text()).toBe('Geen toegang!'); + }); + + it('goBack is called when button is clicked', async () => { + Unauthorized.methods.goBack = jest.fn(); + wrapper = mount(Unauthorized); + const button = await wrapper.find('[data-test="goBack"]'); + await button.trigger('click'); + expect(Unauthorized.methods.goBack).toHaveBeenCalled(); + }) + + it('renders the component correctly', async() => { + const divs = wrapper.findAll('div[class="mx-1"]'); + expect(divs.at(0).text()).toBe('Geen toegang!'); + expect(divs.at(1).text()).toBe('U heeft onvoldoende rechten om deze pagina te bezoeken.'); + }) + +}) + + diff --git a/frontend/tests/unit/views/registerViewTest.spec.js b/frontend/tests/unit/views/registerViewTest.spec.js new file mode 100644 index 00000000..55679a90 --- /dev/null +++ b/frontend/tests/unit/views/registerViewTest.spec.js @@ -0,0 +1,47 @@ +import { mount } from '@vue/test-utils' +import RegisterView from "@/views/RegisterView.vue"; + +describe('RegisterView.vue', () => { + + let wrapper; + + beforeEach(() => { + wrapper = mount(RegisterView); + }) + + it('renders the correct', () => { + expect(wrapper.exists()).toBe(true); + }) + + it('initializes data correctly', () => { + expect(wrapper.vm.valid).toBe(true); + expect(wrapper.vm.showPassword).toBe(false); + expect(wrapper.vm.firstname).toBe(''); + expect(wrapper.vm.lastname).toBe(''); + expect(wrapper.vm.email).toBe(''); + expect(wrapper.vm.password).toBe(''); + expect(wrapper.vm.password2).toBe(''); + expect(wrapper.vm.phone_nr).toBe(''); + expect(wrapper.vm.errors).toBeNull(); + }); + + it('calls apiRegister method on button click', async () => { + RegisterView.methods.apiRegister = jest.fn(); + wrapper = mount(RegisterView); + await wrapper.find('[data-test="register"]').trigger('click'); + + expect(RegisterView.methods.apiRegister).toHaveBeenCalled(); + }) + + it('render correctly', async () => { + expect(wrapper.find('v-text-field[label="Voornaam"]').exists()).toBe(true); + expect(wrapper.find('v-text-field[label="Achternaam"]').exists()).toBe(true); + expect(wrapper.find('v-text-field[label="E-mail"]').exists()).toBe(true); + expect(wrapper.find('v-text-field[label="Wachtwoord"]').exists()).toBe(true); + expect(wrapper.find('v-text-field[label="Bevestig wachtwoord"]').exists()).toBe(true); + expect(wrapper.find('v-autocomplete[label="Locaties"]').exists()).toBe(true); + expect(wrapper.find('v-text-field[label="GSM-nummer"]').exists()).toBe(true); + expect(wrapper.find('div[class="mx-1"]').exists()).toBe(true); + }) + +}) diff --git a/frontend/tests/utils/testHelper.ts b/frontend/tests/utils/testHelper.ts new file mode 100644 index 00000000..0a137616 --- /dev/null +++ b/frontend/tests/utils/testHelper.ts @@ -0,0 +1,9 @@ + +/** + * Gaat het v-model van een input veld triggeren (omdat de gewone manier niet werkt, hebben + * we deze functie gemaakt) + */ +export function triggerInput(input, model, activator) { + const data = activator(input.element.value) + model.setData(data) +} diff --git a/frontend/vue.config.js b/frontend/vue.config.js index f7ba07c2..366e3e47 100644 --- a/frontend/vue.config.js +++ b/frontend/vue.config.js @@ -1,4 +1,5 @@ const { defineConfig } = require('@vue/cli-service') + module.exports = defineConfig({ transpileDependencies: true, devServer: {