diff --git a/backendapi/converters.py b/backendapi/converters.py index 0876959..bf228a7 100644 --- a/backendapi/converters.py +++ b/backendapi/converters.py @@ -5,7 +5,7 @@ class BinaryHexConverter: regex = "(?:[0-9a-fA-F]{2})+" def to_python(self, value: str) -> bytes: - return b16decode(value, casefold=True) + return b16encode(b16decode(value, casefold=True)).decode def to_url(self, value: bytes) -> str: - return b16encode(value).decode() + return value diff --git a/backendapi/exceptions.py b/backendapi/exceptions.py new file mode 100644 index 0000000..bec4070 --- /dev/null +++ b/backendapi/exceptions.py @@ -0,0 +1,3 @@ +from rest_framework.exceptions import APIException + +# write your custm exceptions here if we need it later. diff --git a/backendapi/forms.py b/backendapi/forms.py new file mode 100644 index 0000000..a809537 --- /dev/null +++ b/backendapi/forms.py @@ -0,0 +1,60 @@ +from django import forms +from database.models import Attendant, Crewmember, Crews + + +class AttendantNFCForm(forms.ModelForm): + prefix = "attendant" + + class Meta: + model = Attendant + fields = [ + "nfc_id", + ] + widgets = { + "nfc_id": forms.TextInput( + attrs={ + "class": "form-input", + "placeholder": "Scan NFC tag...", + # "disabled": True, + } + ), + } + + +class CrewmemberForm(forms.ModelForm): + # Allow multiple (different) forms in single page + # Can also be specified as an argument to the class init + prefix = "crewmember" + + class Meta: + model = Crewmember # Link the form to the Attendant model + fields = [ + "first_name", + "last_name", + "email", + "discord", + "phone_number", + "crew", + "profile_image", + ] + widgets = { + "first_name": forms.TextInput( + attrs={"class": "form-input", "placeholder": "Ola"} + ), + "last_name": forms.TextInput( + attrs={"class": "form-input", "placeholder": "Nordmann"} + ), + "email": forms.EmailInput( + attrs={"class": "form-input", "placeholder": "email@example.com"} + ), + "discord": forms.TextInput( + attrs={"class": "form-input", "placeholder": "Discord#1234"} + ), + "phone_number": forms.TextInput( + attrs={"class": "form-input", "placeholder": "+47012345678"} + ), + "crew": forms.TextInput( + # Crews.objects.all(), + attrs={"class": "form-input", "placeholder": "Select crew..."}, + ), + } diff --git a/backendapi/migrations/0001_initial.py b/backendapi/migrations/0001_initial.py new file mode 100644 index 0000000..0df89ab --- /dev/null +++ b/backendapi/migrations/0001_initial.py @@ -0,0 +1,31 @@ +# Generated by Django 5.1.3 on 2024-11-25 08:50 + +from django.db import migrations, models + + +class Migration(migrations.Migration): + + initial = True + + dependencies = [] + + operations = [ + migrations.CreateModel( + name="Attendant", + fields=[ + ( + "id", + models.BigAutoField( + auto_created=True, + primary_key=True, + serialize=False, + verbose_name="ID", + ), + ), + ("first_name", models.CharField(max_length=30)), + ("last_name", models.CharField(max_length=30)), + ("email", models.EmailField(max_length=100)), + ("phone_number", models.CharField(max_length=12)), + ], + ), + ] diff --git a/backendapi/migrations/0002_delete_attendant.py b/backendapi/migrations/0002_delete_attendant.py new file mode 100644 index 0000000..5bcfc37 --- /dev/null +++ b/backendapi/migrations/0002_delete_attendant.py @@ -0,0 +1,16 @@ +# Generated by Django 5.1.3 on 2025-01-06 22:56 + +from django.db import migrations + + +class Migration(migrations.Migration): + + dependencies = [ + ("backendapi", "0001_initial"), + ] + + operations = [ + migrations.DeleteModel( + name="Attendant", + ), + ] diff --git a/backendapi/migrations/0003_initial.py b/backendapi/migrations/0003_initial.py new file mode 100644 index 0000000..641a31e --- /dev/null +++ b/backendapi/migrations/0003_initial.py @@ -0,0 +1,33 @@ +# Generated by Django 4.2.7 on 2025-02-14 20:48 + +from django.db import migrations, models + + +class Migration(migrations.Migration): + + initial = True + + dependencies = [ + ("backendapi", "0002_delete_attendant"), + ] + + operations = [ + migrations.CreateModel( + name="Attendant", + fields=[ + ( + "id", + models.BigAutoField( + auto_created=True, + primary_key=True, + serialize=False, + verbose_name="ID", + ), + ), + ("first_name", models.CharField(max_length=30)), + ("last_name", models.CharField(max_length=30)), + ("email", models.EmailField(max_length=100)), + ("phone_number", models.CharField(max_length=12)), + ], + ), + ] diff --git a/backendapi/migrations/0004_attendant_crew_attendant_discord_and_more.py b/backendapi/migrations/0004_attendant_crew_attendant_discord_and_more.py new file mode 100644 index 0000000..a1546d2 --- /dev/null +++ b/backendapi/migrations/0004_attendant_crew_attendant_discord_and_more.py @@ -0,0 +1,55 @@ +# Generated by Django 4.2.7 on 2025-02-19 18:05 + +from django.db import migrations, models + + +class Migration(migrations.Migration): + + dependencies = [ + ("backendapi", "0003_initial"), + ] + + operations = [ + migrations.AddField( + model_name="attendant", + name="crew", + field=models.CharField(default="Undefined crew", max_length=100), + ), + migrations.AddField( + model_name="attendant", + name="discord", + field=models.CharField(default=False, max_length=100), + preserve_default=False, + ), + migrations.AddField( + model_name="attendant", + name="profile_image", + field=models.ImageField(blank=True, null=True, upload_to="profile_images/"), + ), + migrations.AddField( + model_name="attendant", + name="ticketCrewID", + field=models.CharField(default=False, max_length=100), + preserve_default=False, + ), + migrations.AlterField( + model_name="attendant", + name="email", + field=models.EmailField(max_length=200), + ), + migrations.AlterField( + model_name="attendant", + name="first_name", + field=models.CharField(max_length=100), + ), + migrations.AlterField( + model_name="attendant", + name="last_name", + field=models.CharField(max_length=100), + ), + migrations.AlterField( + model_name="attendant", + name="phone_number", + field=models.CharField(max_length=15), + ), + ] diff --git a/backendapi/migrations/0005_alter_attendant_crew_alter_attendant_ticketcrewid.py b/backendapi/migrations/0005_alter_attendant_crew_alter_attendant_ticketcrewid.py new file mode 100644 index 0000000..86a83e8 --- /dev/null +++ b/backendapi/migrations/0005_alter_attendant_crew_alter_attendant_ticketcrewid.py @@ -0,0 +1,23 @@ +# Generated by Django 4.2.7 on 2025-02-19 18:10 + +from django.db import migrations, models + + +class Migration(migrations.Migration): + + dependencies = [ + ("backendapi", "0004_attendant_crew_attendant_discord_and_more"), + ] + + operations = [ + migrations.AlterField( + model_name="attendant", + name="crew", + field=models.CharField(max_length=100), + ), + migrations.AlterField( + model_name="attendant", + name="ticketCrewID", + field=models.CharField(max_length=100, null=True), + ), + ] diff --git a/backendapi/migrations/0006_delete_attendant.py b/backendapi/migrations/0006_delete_attendant.py new file mode 100644 index 0000000..2e0e2fc --- /dev/null +++ b/backendapi/migrations/0006_delete_attendant.py @@ -0,0 +1,16 @@ +# Generated by Django 5.1.5 on 2025-02-20 14:45 + +from django.db import migrations + + +class Migration(migrations.Migration): + + dependencies = [ + ("backendapi", "0005_alter_attendant_crew_alter_attendant_ticketcrewid"), + ] + + operations = [ + migrations.DeleteModel( + name="Attendant", + ), + ] diff --git a/backendapi/models.py b/backendapi/models.py index 71a8362..4548db2 100644 --- a/backendapi/models.py +++ b/backendapi/models.py @@ -1,3 +1,5 @@ from django.db import models +from rest_framework import serializers +from django.contrib.auth.models import User # Create your models here. diff --git a/backendapi/serializers.py b/backendapi/serializers.py new file mode 100644 index 0000000..149a578 --- /dev/null +++ b/backendapi/serializers.py @@ -0,0 +1,88 @@ +import re +from rest_framework import serializers +from django.conf import settings +from database.models import Crewmember + +# Serializer for the Attendant model: handles validation, serialization, and desersialization of data. +# Change the model later depending on requirements. Please tell me what, and il remake the model later to fit requirements. + + +class CrewmemberAdmin(serializers.ModelSerializer): # defined a serialiser class + first_name = serializers.CharField( + label=("First Name* "), # Labels for the field + required=True, # This makes the fields required. + max_length=100, + style={ + "input_type": "text", + "autofocus": False, + "autocomplete": "off", + "required": True, + }, + error_messages={ + "required": "This field is required.", + "blank": "First Name is required.", + }, + ) + + last_name = serializers.CharField( + label=("Last Name* "), # Label for the field + required=True, # This makes the fields required. + max_length=100, + style={ + "input_type": "text", + "autofocus": False, + "autocomplete": "off", + "required": True, + }, + error_messages={ + "required": "This field is required.", + "blank": "Last Name is required.", + "invalid": "Last Name can only contain characters.", + }, + ) + + email = serializers.EmailField( + label=("Email* "), # Label for the field + required=True, # Field is required + max_length=100, + style={ + "input_type": "email", + "autofocus": False, + "autocomplete": "off", + "required": True, + }, + error_messages={ + "required": "This field is required.", + "blank": "Email is required.", + }, + ) + phone_number = serializers.CharField( + label="Phone Number* ", # Label for the field + max_length=14, + min_length=10, + required=True, # Field is required + error_messages={ + "required": "This field is required .", + "blank": "Phone number is required.", + }, + ) + + class Meta: + model = Crewmember + fields = ["first_name", "last_name", "email", "phone_number"] + + +def validate_first_name(value): + # Check if the first name contains only characters or letters with spaces and letters from a-Z + if not re.match(r"^[a-zA-Zå-öÅ-Ö ]*$", value): + raise serializers.ValidationError( + "First Name can only contain letters and spaces." + ) + + +def validate_last_name(value): + # Check if the first name contains only characters + if not re.match(r"^[a-zA-Zå-öÅ-Ö ]*$", value): + raise serializers.ValidationError( + "Last Name can only contain letters and spaces." + ) diff --git a/backendapi/urls.py b/backendapi/urls.py index 10e9e26..433f8fb 100644 --- a/backendapi/urls.py +++ b/backendapi/urls.py @@ -1,5 +1,4 @@ from django.urls import path, register_converter - from . import views, converters register_converter(converters.BinaryHexConverter, "hex") diff --git a/backendapi/views.py b/backendapi/views.py index 4112523..899f966 100644 --- a/backendapi/views.py +++ b/backendapi/views.py @@ -1,3 +1,4 @@ +from django.shortcuts import render from django.views.decorators.csrf import csrf_exempt from django.views.decorators.http import require_POST from django.views.decorators.cache import never_cache @@ -7,13 +8,21 @@ from rest_framework import status from base64 import b16decode, b16encode -from database.models import Attendant +from rest_framework.decorators import api_view +from rest_framework.response import Response +from rest_framework import status + +from database.models import Attendant, Crewmember + +from . import exceptions +from .serializers import CrewmemberAdmin +from .forms import CrewmemberForm, AttendantNFCForm @csrf_exempt @require_POST @never_cache -def scanned(request, tag: bytes): +def scanned(request, tag: str): try: attendant = Attendant.objects.get(nfc_id=tag) # TODO: Queue event @@ -23,12 +32,102 @@ def scanned(request, tag: bytes): return JsonResponse({"error": "No such tag", "valid": False}, status=404) -@api_view(["GET", "POST"]) -def api_get(request): +@api_view(["GET", "POST", "PUT", "PATCH", "DELETE"]) +def api_get(request, pk=None): if request.method == "GET": - return Response({"message": "GET request received"}, status=200) + # If there's no 'pk' parameter, return all attendants + if pk is None: + attendants = Attendant.objects.all() + serializer = CrewmemberAdmin(attendants, many=True) + return Response(serializer.data, status=status.HTTP_200_OK) + # Otherwise, return a single attendant + try: + attendant = Attendant.objects.get(pk=pk) + serializer = CrewmemberAdmin(attendant) + return Response(serializer.data, status=status.HTTP_200_OK) + except Attendant.DoesNotExist: + return Response( + {"detail": "Attendant not found."}, status=status.HTTP_404_NOT_FOUND + ) + elif request.method == "POST": - return Response({"message": "POST request received"}, status=200) + # Create a new attendant + serializer = CrewmemberAdmin(data=request.data) + if serializer.is_valid(): + serializer.save() # Save the new attendant to the database + return Response(serializer.data, status=status.HTTP_201_CREATED) + return Response(serializer.errors, status=status.HTTP_400_BAD_REQUEST) + + elif request.method == "PUT": + # Full update of an existing attendant + try: + attendant = Attendant.objects.get(pk=pk) + except Attendant.DoesNotExist: + return Response( + {"detail": "Attendant not found."}, status=status.HTTP_404_NOT_FOUND + ) + + serializer = CrewmemberAdmin(attendant, data=request.data) + if serializer.is_valid(): + serializer.save() + return Response(serializer.data, status=status.HTTP_200_OK) + return Response(serializer.errors, status=status.HTTP_400_BAD_REQUEST) + + elif request.method == "PATCH": + # Partial update of an existing attendant + try: + attendant = Attendant.objects.get(pk=pk) + except Attendant.DoesNotExist: + return Response( + {"detail": "Attendant not found."}, status=status.HTTP_404_NOT_FOUND + ) + + serializer = CrewmemberAdmin(attendant, data=request.data, partial=True) + if serializer.is_valid(): + serializer.save() + return Response(serializer.data, status=status.HTTP_200_OK) + return Response(serializer.errors, status=status.HTTP_400_BAD_REQUEST) + + elif request.method == "DELETE": + # Delete an existing attendant + try: + attendant = Attendant.objects.get(pk=pk) + except Attendant.DoesNotExist: + return Response( + {"detail": "Attendant not found."}, status=status.HTTP_404_NOT_FOUND + ) + + attendant.delete() + return Response( + {"detail": "Attendant deleted successfully."}, + status=status.HTTP_204_NO_CONTENT, + ) + + +def base_view(request): # basic frontend registration view. + return render(request, "base.html") + + +def registration_view(request): + success = None + if request.method == "POST": + cmform = CrewmemberForm(request.POST, request.FILES) + nfcform = AttendantNFCForm(request.POST, request.FILES) + if cmform.is_valid(): + cmform.save() + success = True + else: + success = False + else: + cmform = CrewmemberForm() + nfcform = AttendantNFCForm() + + context = { + "cmform": cmform, + "nfcform": nfcform, + "success": success, + } + return render(request, "registration.html", context) @api_view(["GET"]) diff --git a/config/settings.py b/config/settings.py index 7321958..40b23b0 100644 --- a/config/settings.py +++ b/config/settings.py @@ -13,6 +13,7 @@ import os from pathlib import Path + # Build paths inside the project like this: BASE_DIR / 'subdir'. BASE_DIR = Path(__file__).resolve().parent.parent @@ -45,6 +46,7 @@ "database", "backendapi", "rest_framework", + "theme", ] MIDDLEWARE = [ @@ -128,8 +130,11 @@ # Static files (CSS, JavaScript, Images) # https://docs.djangoproject.com/en/5.0/howto/static-files/ -STATIC_URL = "static/" +STATIC_URL = "/static/" STATIC_ROOT = os.getenv("DJANGO_STATIC_ROOT") +STATICFILES_DIRS = [ + os.path.join(BASE_DIR, "theme"), +] # Default primary key field type # https://docs.djangoproject.com/en/5.0/ref/settings/#default-auto-field diff --git a/config/urls.py b/config/urls.py index 282345a..42b5903 100644 --- a/config/urls.py +++ b/config/urls.py @@ -17,10 +17,13 @@ from django.contrib import admin from django.urls import include, path - import backendapi.urls +from backendapi import views + urlpatterns = [ path("admin/", admin.site.urls), path("api/", include(backendapi.urls)), + path("", views.base_view, name="base"), + path("registration", views.registration_view, name="registration"), ] diff --git a/requirements.txt b/requirements.txt index 04ec462..86a0e70 100644 --- a/requirements.txt +++ b/requirements.txt @@ -3,4 +3,4 @@ django-urlconfchecks==0.11.0 # TODO: Add to CI daphne==4.1.2 psycopg2-binary==2.9.10 djangorestframework==3.15.2 - +pillow==11.1.0 diff --git a/theme/__init__.py b/theme/__init__.py new file mode 100644 index 0000000..e69de29 diff --git a/theme/apps.py b/theme/apps.py new file mode 100644 index 0000000..dd24136 --- /dev/null +++ b/theme/apps.py @@ -0,0 +1,5 @@ +from django.apps import AppConfig + + +class ThemeConfig(AppConfig): + name = "theme" diff --git a/theme/static/theme/styles.css b/theme/static/theme/styles.css new file mode 100644 index 0000000..195e76a --- /dev/null +++ b/theme/static/theme/styles.css @@ -0,0 +1,125 @@ +/* General body styles */ +body { + font-family: Arial, sans-serif; + background-color: #f0f0f5; + margin: 0; + padding: 20px; + display: flex; + justify-content: center; + align-items: center; + height: 100vh; +} + +.container { + max-width: 600px; + width: 100%; + padding: 20px; + box-sizing: border-box; +} + +.form-container { + background-color: #ffffff; + padding: 20px; + border-radius: 10px; + box-shadow: 0 4px 8px rgba(0, 0, 0, 0.1); + width: 100%; + max-width: 400px; + margin: 0 auto; + box-sizing: border-box; +} + +label { + font-weight: bold; + margin-bottom: 5px; + display: block; +} + +input[type="text"], +input[type="email"] { + width: 100%; + padding: 10px; + margin-bottom: 15px; + font-size: 16px; + border: 1px solid #ccc; + border-radius: 5px; + box-sizing: border-box; +} + +.submit-btn { + background-color: #007bff; + color: white; + padding: 12px; + border: none; + border-radius: 5px; + font-size: 16px; + cursor: pointer; + width: 100%; +} + +.submit-btn:hover { + background-color: #0056b3; +} + +/* Responsive Design */ +@media (max-width: 600px) { + .form-container { + padding: 15px; + } + + input[type="text"], + input[type="email"] { + font-size: 14px; + } + + .submit-btn { + font-size: 14px; + padding: 10px; + } +} + +/* Toast message styles */ +.toast { + visibility: hidden; + max-width: 50px; + height: 50px; + margin-left: -125px; + margin-bottom: 50px; + background-color: #4CAF50; + color: white; + text-align: center; + border-radius: 2px; + padding: 16px; + position: fixed; + z-index: 1; + left: 50%; + bottom: 30px; + font-size: 17px; + box-shadow: 0 2px 4px rgba(0, 0, 0, 0.2); +} + +.toast.show { + visibility: visible; + animation: fadein 0.5s, fadeout 1.5s 4s; +} + +@keyframes fadein { + from { + bottom: 0; + opacity: 0; + } + + to { + bottom: 30px; + opacity: 1; + } +} + +@keyframes fadeout { + from { + opacity: 1; + } + + to { + opacity: 0; + } +} \ No newline at end of file diff --git a/theme/templates/base.html b/theme/templates/base.html new file mode 100644 index 0000000..930abca --- /dev/null +++ b/theme/templates/base.html @@ -0,0 +1,57 @@ +{% load static %} + + + + Django Form + + + + + + + +
+ {% block toast %} + {% endblock %} +
+
+ {% block content %} + {% endblock %} +
+ + + diff --git a/theme/templates/registration.html b/theme/templates/registration.html new file mode 100644 index 0000000..f00c71b --- /dev/null +++ b/theme/templates/registration.html @@ -0,0 +1,21 @@ +{% extends "base.html" %} + +{% block title %}User Registration{% endblock %} + +{% block content %} +
+
+ {% csrf_token %} + {{ cmform.as_p }} + {{ nfcform.as_p }} + +
+
+
+{% endblock %} + +{% block toast %} +{% if success %} +Saved! +{% endif %} +{% endblock %}