From f21c8021df9f5cf37ec6a4d0602396853b391e1e Mon Sep 17 00:00:00 2001 From: iPegii <51372604+iPegii@users.noreply.github.com> Date: Mon, 2 Feb 2026 19:17:28 +0200 Subject: [PATCH 001/149] first commit to make project work with compose --- .env-example | 20 + .gitignore | 2 - backend/Dockerfile | 2 +- backend/backend/settings.py | 10 + backend/ilotalo/migrations/0001_initial.py | 103 +++ ..._end_alter_event_room_alter_event_start.py | 28 + backend/ilotalo/models.py | 8 +- backend/ilotalo/serializers.py | 26 +- backend/ilotalo/views.py | 43 +- backend/poetry.lock | 130 +++- backend/pyproject.toml | 1 + docker-compose-dev.yml | 47 ++ docker-compose-prod.yml | 29 + frontend/src/axios.js | 25 +- frontend/src/components/ReservationsView.jsx | 2 + frontend/src/components/YkvLogoutFunction.jsx | 3 +- frontend/src/pages/cleaningschedulepage.jsx | 12 +- frontend/src/pages/cleaningsuppliespage.jsx | 3 +- frontend/src/pages/defectfaultpage.jsx | 3 +- frontend/src/pages/frontpage.jsx | 15 +- frontend/src/pages/ownkeys.jsx | 13 +- frontend/src/pages/ownpage.jsx | 9 +- frontend/src/pages/reservations.jsx | 90 ++- frontend/src/pages/statistics.jsx | 611 +++++++----------- frontend/src/utils/keyuserhelpers.js | 4 +- 25 files changed, 794 insertions(+), 445 deletions(-) create mode 100644 .env-example create mode 100644 backend/ilotalo/migrations/0001_initial.py create mode 100644 backend/ilotalo/migrations/0002_alter_event_end_alter_event_room_alter_event_start.py create mode 100644 docker-compose-dev.yml create mode 100644 docker-compose-prod.yml diff --git a/.env-example b/.env-example new file mode 100644 index 00000000..ca0b6aef --- /dev/null +++ b/.env-example @@ -0,0 +1,20 @@ +# Django Settings +DJANGO_SECRET_KEY=django-insecure-placeholder-change-this-in-production +DEBUG=True + +# Database Settings (for Docker Compose and Django) +POSTGRES_DB=ilotalo +POSTGRES_USER=leppis +POSTGRES_PASSWORD=leppis_password +POSTGRES_HOST=db +POSTGRES_PORT=5432 + +# Mapping for existing settings.py logic +TEST_DB_NAME=ilotalo +TEST_DB_USER=leppis +TEST_DB_PASSWORD=leppis_password +TEST_DB_HOST=db +TEST_DB_PORT=5432 + +# Frontend Settings +SITE_KEY=your_site_key_placeholder diff --git a/.gitignore b/.gitignore index 61da96f9..3bb10030 100644 --- a/.gitignore +++ b/.gitignore @@ -2,6 +2,4 @@ __pycache__ .coverage htmlcov .env -backend/ilotalo/migrations/* -!backend/ilotalo/migrations/__init__ siivousvuorot.json \ No newline at end of file diff --git a/backend/Dockerfile b/backend/Dockerfile index f228b903..2cefb3ef 100644 --- a/backend/Dockerfile +++ b/backend/Dockerfile @@ -12,7 +12,7 @@ RUN pip install poetry RUN poetry config virtualenvs.create false -RUN poetry install --no-dev +RUN poetry install --without dev --no-root EXPOSE 8000 diff --git a/backend/backend/settings.py b/backend/backend/settings.py index ca8353fc..91ac9b7a 100644 --- a/backend/backend/settings.py +++ b/backend/backend/settings.py @@ -128,6 +128,8 @@ "DEFAULT_AUTHENTICATION_CLASSES": ( "rest_framework_simplejwt.authentication.JWTAuthentication", ), + "DEFAULT_PAGINATION_CLASS": "rest_framework.pagination.PageNumberPagination", + "PAGE_SIZE": 100, } SIMPLE_JWT = { @@ -175,6 +177,14 @@ AUTH_USER_MODEL = "ilotalo.User" +PASSWORD_HASHERS = [ + "django.contrib.auth.hashers.BCryptSHA256PasswordHasher", + "django.contrib.auth.hashers.BCryptPasswordHasher", + "django.contrib.auth.hashers.PBKDF2PasswordHasher", + "django.contrib.auth.hashers.PBKDF2SHA1PasswordHasher", + "django.contrib.auth.hashers.MD5PasswordHasher", +] + # Default primary key field type # https://docs.djangoproject.com/en/5.0/ref/settings/#default-auto-field diff --git a/backend/ilotalo/migrations/0001_initial.py b/backend/ilotalo/migrations/0001_initial.py new file mode 100644 index 00000000..e88cb6fb --- /dev/null +++ b/backend/ilotalo/migrations/0001_initial.py @@ -0,0 +1,103 @@ +# Generated by Django 5.0.1 on 2026-02-02 13:17 + +import django.db.models.deletion +from django.conf import settings +from django.db import migrations, models + + +class Migration(migrations.Migration): + + initial = True + + dependencies = [ + ] + + operations = [ + migrations.CreateModel( + name='User', + fields=[ + ('last_login', models.DateTimeField(blank=True, null=True, verbose_name='last login')), + ('id', models.AutoField(primary_key=True, serialize=False)), + ('username', models.CharField(max_length=20, unique=True)), + ('password', models.CharField(default='', max_length=255)), + ('email', models.EmailField(default='', max_length=100, unique=True)), + ('telegram', models.CharField(blank=True, default='', max_length=100)), + ('role', models.IntegerField(default=5)), + ('rights_for_reservation', models.BooleanField(default=False)), + ('first_login', models.BooleanField(default=False)), + ], + options={ + 'abstract': False, + }, + ), + migrations.CreateModel( + name='CleaningSupplies', + fields=[ + ('id', models.AutoField(primary_key=True, serialize=False)), + ('tool', models.CharField(default='', max_length=100, unique=True)), + ], + ), + migrations.CreateModel( + name='DefectFault', + fields=[ + ('id', models.AutoField(primary_key=True, serialize=False)), + ('description', models.CharField(default='', max_length=300)), + ('time', models.DateTimeField(auto_now_add=True)), + ('email_sent', models.DateTimeField(blank=True, null=True)), + ('repaired', models.DateTimeField(blank=True, null=True)), + ], + ), + migrations.CreateModel( + name='Organization', + fields=[ + ('id', models.AutoField(primary_key=True, serialize=False)), + ('name', models.CharField(default='', max_length=50, unique=True)), + ('email', models.EmailField(default='', max_length=100, unique=True)), + ('homepage', models.CharField(default='', max_length=100)), + ('color', models.CharField(blank=True, max_length=7, null=True)), + ], + ), + migrations.CreateModel( + name='NightResponsibility', + fields=[ + ('id', models.BigAutoField(auto_created=True, primary_key=True, serialize=False, verbose_name='ID')), + ('responsible_for', models.CharField(default='', max_length=500)), + ('login_time', models.DateTimeField(auto_now_add=True)), + ('logout_time', models.DateTimeField(auto_now=True)), + ('present', models.BooleanField(default=True)), + ('late', models.BooleanField(default=False)), + ('created_by', models.CharField(default='', max_length=50)), + ('user', models.ForeignKey(default=0, on_delete=django.db.models.deletion.CASCADE, to=settings.AUTH_USER_MODEL)), + ('organizations', models.ManyToManyField(to='ilotalo.organization')), + ], + ), + migrations.CreateModel( + name='Event', + fields=[ + ('id', models.BigAutoField(auto_created=True, primary_key=True, serialize=False, verbose_name='ID')), + ('start', models.DateTimeField(blank=True)), + ('end', models.DateTimeField(blank=True)), + ('title', models.CharField(default='', max_length=100)), + ('description', models.TextField(default='')), + ('responsible', models.CharField(default='', max_length=100)), + ('open', models.BooleanField(default=True)), + ('room', models.CharField(default='', max_length=50)), + ('created_by', models.ForeignKey(default=0, on_delete=django.db.models.deletion.CASCADE, to=settings.AUTH_USER_MODEL)), + ('organizer', models.ForeignKey(default=0, on_delete=django.db.models.deletion.CASCADE, to='ilotalo.organization')), + ], + ), + migrations.CreateModel( + name='Cleaning', + fields=[ + ('id', models.AutoField(primary_key=True, serialize=False)), + ('week', models.IntegerField(default=0)), + ('big', models.ForeignKey(default=0, on_delete=django.db.models.deletion.CASCADE, related_name='big_orgs', to='ilotalo.organization')), + ('small', models.ForeignKey(default=0, on_delete=django.db.models.deletion.CASCADE, related_name='small_orgs', to='ilotalo.organization')), + ], + ), + migrations.AddField( + model_name='user', + name='keys', + field=models.ManyToManyField(to='ilotalo.organization'), + ), + ] diff --git a/backend/ilotalo/migrations/0002_alter_event_end_alter_event_room_alter_event_start.py b/backend/ilotalo/migrations/0002_alter_event_end_alter_event_room_alter_event_start.py new file mode 100644 index 00000000..be424010 --- /dev/null +++ b/backend/ilotalo/migrations/0002_alter_event_end_alter_event_room_alter_event_start.py @@ -0,0 +1,28 @@ +# Generated by Django 5.0.1 on 2026-02-02 13:40 + +from django.db import migrations, models + + +class Migration(migrations.Migration): + + dependencies = [ + ('ilotalo', '0001_initial'), + ] + + operations = [ + migrations.AlterField( + model_name='event', + name='end', + field=models.DateTimeField(blank=True, db_index=True), + ), + migrations.AlterField( + model_name='event', + name='room', + field=models.CharField(db_index=True, default='', max_length=50), + ), + migrations.AlterField( + model_name='event', + name='start', + field=models.DateTimeField(blank=True, db_index=True), + ), + ] diff --git a/backend/ilotalo/models.py b/backend/ilotalo/models.py index 55b9c030..8425727f 100644 --- a/backend/ilotalo/models.py +++ b/backend/ilotalo/models.py @@ -85,18 +85,20 @@ class Event(models.Model): # Fields for event attributes start = models.DateTimeField( - blank = True + blank = True, + db_index=True ) end = models.DateTimeField( blank = True, + db_index=True ) title = models.CharField(max_length=100, default="") # Name of the event organizer = models.ForeignKey(Organization, on_delete=models.CASCADE, default=0) # Organization responsible for the event - description = models.CharField(max_length=500, default="") # Description of the event + description = models.TextField(default="") # Description of the event responsible = models.CharField(max_length=100, default="") # Person responsible for the event created_by = models.ForeignKey(User, on_delete=models.CASCADE, default=0) open = models.BooleanField(default=True) # Indicates whether the event is open or not - room = models.CharField(max_length=50, default="") # Room where the event takes place + room = models.CharField(max_length=50, default="", db_index=True) # Room where the event takes place class NightResponsibility(models.Model): """ diff --git a/backend/ilotalo/serializers.py b/backend/ilotalo/serializers.py index 55e6d4a0..8c18a95d 100644 --- a/backend/ilotalo/serializers.py +++ b/backend/ilotalo/serializers.py @@ -43,6 +43,12 @@ def validate_size(self, size): raise serializers.ValidationError("Organization size must be 0 or 1 (small or large).") return size +class OrganizationNameSerializer(serializers.ModelSerializer): + """Minimal serializer for organization name and ID only to boost performance""" + class Meta: + model = Organization + fields = ('id', 'name') + class UserSerializer(serializers.ModelSerializer): keys = OrganizationSerializer(many=True, read_only=True) @@ -118,12 +124,6 @@ def validate_username(self, username): raise serializers.ValidationError("Username cannot contain @ symbol") return username - def validate_username(self, username): - """Validates that the username does not contain @ symbol so it doesn't mess with the email login""" - if "@" in username: - raise serializers.ValidationError("Username cannot contain @ symbol") - return username - def validate_role(self, role): """Validates role when updating a user. Limits: 1 <= role <= 7.""" if int(role) < 1: @@ -169,7 +169,7 @@ class Meta: exclude = ('password',) class EventSerializer(serializers.ModelSerializer): - """Serializes an Event object as JSON""" + """Serializes an Event object as JSON - Full version""" organizer = OrganizationSerializer(read_only=True) created_by = UserNoPasswordSerializer(read_only=True) @@ -179,8 +179,16 @@ class Meta: model = Event fields = '__all__' +class EventListSerializer(serializers.ModelSerializer): + """Lightweight serializer for calendar and list views - Nested object for frontend compatibility""" + organizer = OrganizationNameSerializer(read_only=True) + + class Meta: + model = Event + fields = ('id', 'start', 'end', 'title', 'organizer', 'responsible', 'open', 'room') + class CreateEventSerializer(serializers.ModelSerializer): - """Serializes an Event object as JSON""" + """Used for creating an event""" class Meta: model = Event @@ -270,4 +278,4 @@ class CleaningSuppliesSerializer(serializers.ModelSerializer): class Meta: model = CleaningSupplies - fields = '__all__' \ No newline at end of file + fields = '__all__' diff --git a/backend/ilotalo/views.py b/backend/ilotalo/views.py index 992a672f..504c7ea9 100644 --- a/backend/ilotalo/views.py +++ b/backend/ilotalo/views.py @@ -12,13 +12,15 @@ UserNoPasswordSerializer, UserUpdateSerializer, EventSerializer, + EventListSerializer, CreateEventSerializer, NightResponsibilitySerializer, CreateNightResponsibilitySerializer, DefectFaultSerializer, CleaningSerializer, CreateCleaningSerializer, - CleaningSuppliesSerializer + CleaningSuppliesSerializer, + OrganizationNameSerializer ) from .models import User, Organization, Event, NightResponsibility, DefectFault, Cleaning, CleaningSupplies from .config import Role @@ -51,6 +53,7 @@ class UserView(viewsets.ReadOnlyModelViewSet): serializer_class = UserNoPasswordSerializer queryset = User.objects.all() + pagination_class = None class OrganizationView(viewsets.ReadOnlyModelViewSet): @@ -61,6 +64,7 @@ class OrganizationView(viewsets.ReadOnlyModelViewSet): serializer_class = OrganizationSerializer queryset = Organization.objects.all() + pagination_class = None class RegisterView(APIView): @@ -381,8 +385,40 @@ class EventView(viewsets.ReadOnlyModelViewSet): Only supports list and retrieve actions (read-only) """ - serializer_class = EventSerializer - queryset = Event.objects.all() + def get_serializer_class(self): + if self.action == 'list': + return EventListSerializer + return EventSerializer + + def get_queryset(self): + queryset = Event.objects.all().select_related('organizer', 'created_by') + start_date = self.request.query_params.get('start') + end_date = self.request.query_params.get('end') + all_time = self.request.query_params.get('all') + + if start_date: + queryset = queryset.filter(start__gte=start_date) + if end_date: + queryset = queryset.filter(end__lte=end_date) + + # Default to current month if no filters are provided and 'all' is not requested. + # This prevents loading thousands of historical events by accident. + if not start_date and not end_date and not all_time: + now = datetime.now() + queryset = queryset.filter(start__year=now.year, start__month=now.month) + + return queryset.order_by('start') + + def paginate_queryset(self, queryset): + """ + Pagination is enabled by default for the list view (e.g., /events/). + It is disabled if: + 1. 'all' is provided (CSV exports). + 2. 'start' or 'end' is provided (Calendar view, which handles its own data slicing). + """ + if 'all' in self.request.query_params or 'start' in self.request.query_params or 'end' in self.request.query_params: + return None + return super().paginate_queryset(queryset) class CreateEventView(APIView): """View for creating a new event /api/events/create_event""" @@ -484,6 +520,7 @@ class NightResponsibilityView(viewsets.ReadOnlyModelViewSet): serializer_class = NightResponsibilitySerializer queryset = NightResponsibility.objects.all() + pagination_class = None class CreateNightResponsibilityView(APIView): """View for creating a new ykv /api/ykv/create_responsibility""" diff --git a/backend/poetry.lock b/backend/poetry.lock index 53bd40fe..e9e23cee 100644 --- a/backend/poetry.lock +++ b/backend/poetry.lock @@ -1,4 +1,4 @@ -# This file is automatically @generated by Poetry 1.7.1 and should not be changed by hand. +# This file is automatically @generated by Poetry 2.3.2 and should not be changed by hand. [[package]] name = "aenum" @@ -6,6 +6,7 @@ version = "3.1.15" description = "Advanced Enumerations (compatible with Python's stdlib Enum), NamedTuples, and NamedConstants" optional = false python-versions = "*" +groups = ["main"] files = [ {file = "aenum-3.1.15-py2-none-any.whl", hash = "sha256:27b1710b9d084de6e2e695dab78fe9f269de924b51ae2850170ee7e1ca6288a5"}, {file = "aenum-3.1.15-py3-none-any.whl", hash = "sha256:e0dfaeea4c2bd362144b87377e2c61d91958c5ed0b4daf89cb6f45ae23af6288"}, @@ -18,6 +19,7 @@ version = "3.10.4" description = "In-process task scheduler with Cron-like capabilities" optional = false python-versions = ">=3.6" +groups = ["main"] files = [ {file = "APScheduler-3.10.4-py3-none-any.whl", hash = "sha256:fb91e8a768632a4756a585f79ec834e0e27aad5860bac7eaa523d9ccefd87661"}, {file = "APScheduler-3.10.4.tar.gz", hash = "sha256:e6df071b27d9be898e486bc7940a7be50b4af2e9da7c08f0744a96d4bd4cef4a"}, @@ -46,6 +48,7 @@ version = "3.7.2" description = "ASGI specs, helper code, and adapters" optional = false python-versions = ">=3.7" +groups = ["main"] files = [ {file = "asgiref-3.7.2-py3-none-any.whl", hash = "sha256:89b2ef2247e3b562a16eef663bc0e2e703ec6468e2fa8a5cd61cd449786d4f6e"}, {file = "asgiref-3.7.2.tar.gz", hash = "sha256:9e0ce3aa93a819ba5b45120216b23878cf6e8525eb3848653452b4192b92afed"}, @@ -63,6 +66,7 @@ version = "3.0.3" description = "An abstract syntax tree for Python with inference support." optional = false python-versions = ">=3.8.0" +groups = ["dev"] files = [ {file = "astroid-3.0.3-py3-none-any.whl", hash = "sha256:92fcf218b89f449cdf9f7b39a269f8d5d617b27be68434912e11e79203963a17"}, {file = "astroid-3.0.3.tar.gz", hash = "sha256:4148645659b08b70d72460ed1921158027a9e53ae8b7234149b1400eddacbb93"}, @@ -71,12 +75,78 @@ files = [ [package.dependencies] typing-extensions = {version = ">=4.0.0", markers = "python_version < \"3.11\""} +[[package]] +name = "bcrypt" +version = "4.3.0" +description = "Modern password hashing for your software and your servers" +optional = false +python-versions = ">=3.8" +groups = ["main"] +files = [ + {file = "bcrypt-4.3.0-cp313-cp313t-macosx_10_12_universal2.whl", hash = "sha256:f01e060f14b6b57bbb72fc5b4a83ac21c443c9a2ee708e04a10e9192f90a6281"}, + {file = "bcrypt-4.3.0-cp313-cp313t-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:c5eeac541cefd0bb887a371ef73c62c3cd78535e4887b310626036a7c0a817bb"}, + {file = "bcrypt-4.3.0-cp313-cp313t-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:59e1aa0e2cd871b08ca146ed08445038f42ff75968c7ae50d2fdd7860ade2180"}, + {file = "bcrypt-4.3.0-cp313-cp313t-manylinux_2_28_aarch64.whl", hash = "sha256:0042b2e342e9ae3d2ed22727c1262f76cc4f345683b5c1715f0250cf4277294f"}, + {file = "bcrypt-4.3.0-cp313-cp313t-manylinux_2_28_armv7l.manylinux_2_31_armv7l.whl", hash = "sha256:74a8d21a09f5e025a9a23e7c0fd2c7fe8e7503e4d356c0a2c1486ba010619f09"}, + {file = "bcrypt-4.3.0-cp313-cp313t-manylinux_2_28_x86_64.whl", hash = "sha256:0142b2cb84a009f8452c8c5a33ace5e3dfec4159e7735f5afe9a4d50a8ea722d"}, + {file = "bcrypt-4.3.0-cp313-cp313t-manylinux_2_34_aarch64.whl", hash = "sha256:12fa6ce40cde3f0b899729dbd7d5e8811cb892d31b6f7d0334a1f37748b789fd"}, + {file = "bcrypt-4.3.0-cp313-cp313t-manylinux_2_34_x86_64.whl", hash = "sha256:5bd3cca1f2aa5dbcf39e2aa13dd094ea181f48959e1071265de49cc2b82525af"}, + {file = "bcrypt-4.3.0-cp313-cp313t-musllinux_1_1_aarch64.whl", hash = "sha256:335a420cfd63fc5bc27308e929bee231c15c85cc4c496610ffb17923abf7f231"}, + {file = "bcrypt-4.3.0-cp313-cp313t-musllinux_1_1_x86_64.whl", hash = "sha256:0e30e5e67aed0187a1764911af023043b4542e70a7461ad20e837e94d23e1d6c"}, + {file = "bcrypt-4.3.0-cp313-cp313t-musllinux_1_2_aarch64.whl", hash = "sha256:3b8d62290ebefd49ee0b3ce7500f5dbdcf13b81402c05f6dafab9a1e1b27212f"}, + {file = "bcrypt-4.3.0-cp313-cp313t-musllinux_1_2_x86_64.whl", hash = "sha256:2ef6630e0ec01376f59a006dc72918b1bf436c3b571b80fa1968d775fa02fe7d"}, + {file = "bcrypt-4.3.0-cp313-cp313t-win32.whl", hash = "sha256:7a4be4cbf241afee43f1c3969b9103a41b40bcb3a3f467ab19f891d9bc4642e4"}, + {file = "bcrypt-4.3.0-cp313-cp313t-win_amd64.whl", hash = "sha256:5c1949bf259a388863ced887c7861da1df681cb2388645766c89fdfd9004c669"}, + {file = "bcrypt-4.3.0-cp38-abi3-macosx_10_12_universal2.whl", hash = "sha256:f81b0ed2639568bf14749112298f9e4e2b28853dab50a8b357e31798686a036d"}, + {file = "bcrypt-4.3.0-cp38-abi3-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:864f8f19adbe13b7de11ba15d85d4a428c7e2f344bac110f667676a0ff84924b"}, + {file = "bcrypt-4.3.0-cp38-abi3-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:3e36506d001e93bffe59754397572f21bb5dc7c83f54454c990c74a468cd589e"}, + {file = "bcrypt-4.3.0-cp38-abi3-manylinux_2_28_aarch64.whl", hash = "sha256:842d08d75d9fe9fb94b18b071090220697f9f184d4547179b60734846461ed59"}, + {file = "bcrypt-4.3.0-cp38-abi3-manylinux_2_28_armv7l.manylinux_2_31_armv7l.whl", hash = "sha256:7c03296b85cb87db865d91da79bf63d5609284fc0cab9472fdd8367bbd830753"}, + {file = "bcrypt-4.3.0-cp38-abi3-manylinux_2_28_x86_64.whl", hash = "sha256:62f26585e8b219cdc909b6a0069efc5e4267e25d4a3770a364ac58024f62a761"}, + {file = "bcrypt-4.3.0-cp38-abi3-manylinux_2_34_aarch64.whl", hash = "sha256:beeefe437218a65322fbd0069eb437e7c98137e08f22c4660ac2dc795c31f8bb"}, + {file = "bcrypt-4.3.0-cp38-abi3-manylinux_2_34_x86_64.whl", hash = "sha256:97eea7408db3a5bcce4a55d13245ab3fa566e23b4c67cd227062bb49e26c585d"}, + {file = "bcrypt-4.3.0-cp38-abi3-musllinux_1_1_aarch64.whl", hash = "sha256:191354ebfe305e84f344c5964c7cd5f924a3bfc5d405c75ad07f232b6dffb49f"}, + {file = "bcrypt-4.3.0-cp38-abi3-musllinux_1_1_x86_64.whl", hash = "sha256:41261d64150858eeb5ff43c753c4b216991e0ae16614a308a15d909503617732"}, + {file = "bcrypt-4.3.0-cp38-abi3-musllinux_1_2_aarch64.whl", hash = "sha256:33752b1ba962ee793fa2b6321404bf20011fe45b9afd2a842139de3011898fef"}, + {file = "bcrypt-4.3.0-cp38-abi3-musllinux_1_2_x86_64.whl", hash = "sha256:50e6e80a4bfd23a25f5c05b90167c19030cf9f87930f7cb2eacb99f45d1c3304"}, + {file = "bcrypt-4.3.0-cp38-abi3-win32.whl", hash = "sha256:67a561c4d9fb9465ec866177e7aebcad08fe23aaf6fbd692a6fab69088abfc51"}, + {file = "bcrypt-4.3.0-cp38-abi3-win_amd64.whl", hash = "sha256:584027857bc2843772114717a7490a37f68da563b3620f78a849bcb54dc11e62"}, + {file = "bcrypt-4.3.0-cp39-abi3-macosx_10_12_universal2.whl", hash = "sha256:0d3efb1157edebfd9128e4e46e2ac1a64e0c1fe46fb023158a407c7892b0f8c3"}, + {file = "bcrypt-4.3.0-cp39-abi3-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:08bacc884fd302b611226c01014eca277d48f0a05187666bca23aac0dad6fe24"}, + {file = "bcrypt-4.3.0-cp39-abi3-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:f6746e6fec103fcd509b96bacdfdaa2fbde9a553245dbada284435173a6f1aef"}, + {file = "bcrypt-4.3.0-cp39-abi3-manylinux_2_28_aarch64.whl", hash = "sha256:afe327968aaf13fc143a56a3360cb27d4ad0345e34da12c7290f1b00b8fe9a8b"}, + {file = "bcrypt-4.3.0-cp39-abi3-manylinux_2_28_armv7l.manylinux_2_31_armv7l.whl", hash = "sha256:d9af79d322e735b1fc33404b5765108ae0ff232d4b54666d46730f8ac1a43676"}, + {file = "bcrypt-4.3.0-cp39-abi3-manylinux_2_28_x86_64.whl", hash = "sha256:f1e3ffa1365e8702dc48c8b360fef8d7afeca482809c5e45e653af82ccd088c1"}, + {file = "bcrypt-4.3.0-cp39-abi3-manylinux_2_34_aarch64.whl", hash = "sha256:3004df1b323d10021fda07a813fd33e0fd57bef0e9a480bb143877f6cba996fe"}, + {file = "bcrypt-4.3.0-cp39-abi3-manylinux_2_34_x86_64.whl", hash = "sha256:531457e5c839d8caea9b589a1bcfe3756b0547d7814e9ce3d437f17da75c32b0"}, + {file = "bcrypt-4.3.0-cp39-abi3-musllinux_1_1_aarch64.whl", hash = "sha256:17a854d9a7a476a89dcef6c8bd119ad23e0f82557afbd2c442777a16408e614f"}, + {file = "bcrypt-4.3.0-cp39-abi3-musllinux_1_1_x86_64.whl", hash = "sha256:6fb1fd3ab08c0cbc6826a2e0447610c6f09e983a281b919ed721ad32236b8b23"}, + {file = "bcrypt-4.3.0-cp39-abi3-musllinux_1_2_aarch64.whl", hash = "sha256:e965a9c1e9a393b8005031ff52583cedc15b7884fce7deb8b0346388837d6cfe"}, + {file = "bcrypt-4.3.0-cp39-abi3-musllinux_1_2_x86_64.whl", hash = "sha256:79e70b8342a33b52b55d93b3a59223a844962bef479f6a0ea318ebbcadf71505"}, + {file = "bcrypt-4.3.0-cp39-abi3-win32.whl", hash = "sha256:b4d4e57f0a63fd0b358eb765063ff661328f69a04494427265950c71b992a39a"}, + {file = "bcrypt-4.3.0-cp39-abi3-win_amd64.whl", hash = "sha256:e53e074b120f2877a35cc6c736b8eb161377caae8925c17688bd46ba56daaa5b"}, + {file = "bcrypt-4.3.0-pp310-pypy310_pp73-manylinux_2_28_aarch64.whl", hash = "sha256:c950d682f0952bafcceaf709761da0a32a942272fad381081b51096ffa46cea1"}, + {file = "bcrypt-4.3.0-pp310-pypy310_pp73-manylinux_2_28_x86_64.whl", hash = "sha256:107d53b5c67e0bbc3f03ebf5b030e0403d24dda980f8e244795335ba7b4a027d"}, + {file = "bcrypt-4.3.0-pp310-pypy310_pp73-manylinux_2_34_aarch64.whl", hash = "sha256:b693dbb82b3c27a1604a3dff5bfc5418a7e6a781bb795288141e5f80cf3a3492"}, + {file = "bcrypt-4.3.0-pp310-pypy310_pp73-manylinux_2_34_x86_64.whl", hash = "sha256:b6354d3760fcd31994a14c89659dee887f1351a06e5dac3c1142307172a79f90"}, + {file = "bcrypt-4.3.0-pp311-pypy311_pp73-manylinux_2_28_aarch64.whl", hash = "sha256:a839320bf27d474e52ef8cb16449bb2ce0ba03ca9f44daba6d93fa1d8828e48a"}, + {file = "bcrypt-4.3.0-pp311-pypy311_pp73-manylinux_2_28_x86_64.whl", hash = "sha256:bdc6a24e754a555d7316fa4774e64c6c3997d27ed2d1964d55920c7c227bc4ce"}, + {file = "bcrypt-4.3.0-pp311-pypy311_pp73-manylinux_2_34_aarch64.whl", hash = "sha256:55a935b8e9a1d2def0626c4269db3fcd26728cbff1e84f0341465c31c4ee56d8"}, + {file = "bcrypt-4.3.0-pp311-pypy311_pp73-manylinux_2_34_x86_64.whl", hash = "sha256:57967b7a28d855313a963aaea51bf6df89f833db4320da458e5b3c5ab6d4c938"}, + {file = "bcrypt-4.3.0.tar.gz", hash = "sha256:3a3fd2204178b6d2adcf09cb4f6426ffef54762577a7c9b54c159008cb288c18"}, +] + +[package.extras] +tests = ["pytest (>=3.2.1,!=3.3.0)"] +typecheck = ["mypy"] + [[package]] name = "certifi" version = "2024.6.2" description = "Python package for providing Mozilla's CA Bundle." optional = false python-versions = ">=3.6" +groups = ["main"] files = [ {file = "certifi-2024.6.2-py3-none-any.whl", hash = "sha256:ddc6c8ce995e6987e7faf5e3f1b02b302836a0e5d98ece18392cb1a36c72ad56"}, {file = "certifi-2024.6.2.tar.gz", hash = "sha256:3cd43f1c6fa7dedc5899d69d3ad0398fd018ad1a17fba83ddaf78aa46c747516"}, @@ -88,6 +158,7 @@ version = "3.3.2" description = "The Real First Universal Charset Detector. Open, modern and actively maintained alternative to Chardet." optional = false python-versions = ">=3.7.0" +groups = ["main"] files = [ {file = "charset-normalizer-3.3.2.tar.gz", hash = "sha256:f30c3cb33b24454a82faecaf01b19c18562b1e89558fb6c56de4d9118a032fd5"}, {file = "charset_normalizer-3.3.2-cp310-cp310-macosx_10_9_universal2.whl", hash = "sha256:25baf083bf6f6b341f4121c2f3c548875ee6f5339300e08be3f2b2ba1721cdd3"}, @@ -187,6 +258,8 @@ version = "0.4.6" description = "Cross-platform colored terminal text." optional = false python-versions = "!=3.0.*,!=3.1.*,!=3.2.*,!=3.3.*,!=3.4.*,!=3.5.*,!=3.6.*,>=2.7" +groups = ["main", "dev"] +markers = "sys_platform == \"win32\"" files = [ {file = "colorama-0.4.6-py2.py3-none-any.whl", hash = "sha256:4f1d9991f5acc0ca119f9d443620b77f9d6b33703e51011c16baf57afb285fc6"}, {file = "colorama-0.4.6.tar.gz", hash = "sha256:08695f5cb7ed6e0531a20572697297273c47b8cae5a63ffc6d6ed5c201be6e44"}, @@ -198,6 +271,7 @@ version = "7.4.1" description = "Code coverage measurement for Python" optional = false python-versions = ">=3.8" +groups = ["dev"] files = [ {file = "coverage-7.4.1-cp310-cp310-macosx_10_9_x86_64.whl", hash = "sha256:077d366e724f24fc02dbfe9d946534357fda71af9764ff99d73c3c596001bbd7"}, {file = "coverage-7.4.1-cp310-cp310-macosx_11_0_arm64.whl", hash = "sha256:0193657651f5399d433c92f8ae264aff31fc1d066deee4b831549526433f3f61"}, @@ -254,7 +328,7 @@ files = [ ] [package.extras] -toml = ["tomli"] +toml = ["tomli ; python_full_version <= \"3.11.0a6\""] [[package]] name = "dill" @@ -262,6 +336,7 @@ version = "0.3.8" description = "serialize all of Python" optional = false python-versions = ">=3.8" +groups = ["dev"] files = [ {file = "dill-0.3.8-py3-none-any.whl", hash = "sha256:c36ca9ffb54365bdd2f8eb3eff7d2a21237f8452b57ace88b1ac615b7e815bd7"}, {file = "dill-0.3.8.tar.gz", hash = "sha256:3ebe3c479ad625c4553aca177444d89b486b1d84982eeacded644afc0cf797ca"}, @@ -277,6 +352,7 @@ version = "5.0.1" description = "A high-level Python web framework that encourages rapid development and clean, pragmatic design." optional = false python-versions = ">=3.10" +groups = ["main"] files = [ {file = "Django-5.0.1-py3-none-any.whl", hash = "sha256:f47a37a90b9bbe2c8ec360235192c7fddfdc832206fcf618bb849b39256affc1"}, {file = "Django-5.0.1.tar.gz", hash = "sha256:8c8659665bc6e3a44fefe1ab0a291e5a3fb3979f9a8230be29de975e57e8f854"}, @@ -297,6 +373,7 @@ version = "0.6.2" description = "APScheduler for Django" optional = false python-versions = "*" +groups = ["main"] files = [ {file = "django-apscheduler-0.6.2.tar.gz", hash = "sha256:60ced2c07b531b0477076dfef2d6de4790a983a4bbd75fae5e387ac360e9c8fd"}, {file = "django_apscheduler-0.6.2-py3-none-any.whl", hash = "sha256:d30a05be98e01c12aa50d0a7afd8d0fd529c874d1c75af20891b6417d2f36475"}, @@ -312,6 +389,7 @@ version = "4.3.1" description = "django-cors-headers is a Django application for handling the server headers required for Cross-Origin Resource Sharing (CORS)." optional = false python-versions = ">=3.8" +groups = ["main"] files = [ {file = "django-cors-headers-4.3.1.tar.gz", hash = "sha256:0bf65ef45e606aff1994d35503e6b677c0b26cafff6506f8fd7187f3be840207"}, {file = "django_cors_headers-4.3.1-py3-none-any.whl", hash = "sha256:0b1fd19297e37417fc9f835d39e45c8c642938ddba1acce0c1753d3edef04f36"}, @@ -327,6 +405,7 @@ version = "3.14.0" description = "Web APIs for Django, made easy." optional = false python-versions = ">=3.6" +groups = ["main"] files = [ {file = "djangorestframework-3.14.0-py3-none-any.whl", hash = "sha256:eb63f58c9f218e1a7d064d17a70751f528ed4e1d35547fdade9aaf4cd103fd08"}, {file = "djangorestframework-3.14.0.tar.gz", hash = "sha256:579a333e6256b09489cbe0a067e66abe55c6595d8926be6b99423786334350c8"}, @@ -342,6 +421,7 @@ version = "5.3.1" description = "A minimal JSON Web Token authentication plugin for Django REST Framework" optional = false python-versions = ">=3.8" +groups = ["main"] files = [ {file = "djangorestframework_simplejwt-5.3.1-py3-none-any.whl", hash = "sha256:381bc966aa46913905629d472cd72ad45faa265509764e20ffd440164c88d220"}, {file = "djangorestframework_simplejwt-5.3.1.tar.gz", hash = "sha256:6c4bd37537440bc439564ebf7d6085e74c5411485197073f508ebdfa34bc9fae"}, @@ -366,6 +446,7 @@ version = "2.5.0" description = "DNS toolkit" optional = false python-versions = ">=3.8" +groups = ["main"] files = [ {file = "dnspython-2.5.0-py3-none-any.whl", hash = "sha256:6facdf76b73c742ccf2d07add296f178e629da60be23ce4b0a9c927b1e02c3a6"}, {file = "dnspython-2.5.0.tar.gz", hash = "sha256:a0034815a59ba9ae888946be7ccca8f7c157b286f8455b379c692efb51022a15"}, @@ -386,6 +467,8 @@ version = "1.2.0" description = "Backport of PEP 654 (exception groups)" optional = false python-versions = ">=3.7" +groups = ["main", "dev"] +markers = "python_version == \"3.10\"" files = [ {file = "exceptiongroup-1.2.0-py3-none-any.whl", hash = "sha256:4bfd3996ac73b41e9b9628b04e079f193850720ea5945fc96a08633c66912f14"}, {file = "exceptiongroup-1.2.0.tar.gz", hash = "sha256:91f5c769735f051a4290d52edd0858999b57e5876e9f85937691bd4c9fa3ed68"}, @@ -400,6 +483,7 @@ version = "3.7" description = "Internationalized Domain Names in Applications (IDNA)" optional = false python-versions = ">=3.5" +groups = ["main"] files = [ {file = "idna-3.7-py3-none-any.whl", hash = "sha256:82fee1fc78add43492d3a1898bfa6d8a904cc97d8427f683ed8e798d07761aa0"}, {file = "idna-3.7.tar.gz", hash = "sha256:028ff3aadf0609c1fd278d8ea3089299412a7a8b9bd005dd08b9f8285bcb5cfc"}, @@ -411,6 +495,7 @@ version = "2.0.0" description = "brain-dead simple config-ini parsing" optional = false python-versions = ">=3.7" +groups = ["main", "dev"] files = [ {file = "iniconfig-2.0.0-py3-none-any.whl", hash = "sha256:b6a85871a79d2e3b22d2d1b94ac2824226a63c6b741c88f7ae975f18b6778374"}, {file = "iniconfig-2.0.0.tar.gz", hash = "sha256:2d91e135bf72d31a410b17c16da610a82cb55f6b0477d1a902134b24a455b8b3"}, @@ -422,6 +507,7 @@ version = "5.13.2" description = "A Python utility / library to sort Python imports." optional = false python-versions = ">=3.8.0" +groups = ["dev"] files = [ {file = "isort-5.13.2-py3-none-any.whl", hash = "sha256:8ca5e72a8d85860d5a3fa69b8745237f2939afe12dbf656afbcb47fe72d947a6"}, {file = "isort-5.13.2.tar.gz", hash = "sha256:48fdfcb9face5d58a4f6dde2e72a1fb8dcaf8ab26f95ab49fab84c2ddefb0109"}, @@ -436,6 +522,7 @@ version = "0.7.0" description = "McCabe checker, plugin for flake8" optional = false python-versions = ">=3.6" +groups = ["dev"] files = [ {file = "mccabe-0.7.0-py2.py3-none-any.whl", hash = "sha256:6c2d30ab6be0e4a46919781807b4f0d834ebdd6c6e3dca0bda5a15f863427b6e"}, {file = "mccabe-0.7.0.tar.gz", hash = "sha256:348e0240c33b60bbdf4e523192ef919f28cb2c3d7d5c7794f74009290f236325"}, @@ -447,6 +534,7 @@ version = "23.2" description = "Core utilities for Python packages" optional = false python-versions = ">=3.7" +groups = ["main", "dev"] files = [ {file = "packaging-23.2-py3-none-any.whl", hash = "sha256:8c491190033a9af7e1d931d0b5dacc2ef47509b34dd0de67ed209b5203fc88c7"}, {file = "packaging-23.2.tar.gz", hash = "sha256:048fb0e9405036518eaaf48a55953c750c11e1a1b68e0dd1a9d62ed0c092cfc5"}, @@ -458,6 +546,7 @@ version = "4.2.0" description = "A small Python package for determining appropriate platform-specific dirs, e.g. a \"user data dir\"." optional = false python-versions = ">=3.8" +groups = ["dev"] files = [ {file = "platformdirs-4.2.0-py3-none-any.whl", hash = "sha256:0614df2a2f37e1a662acbd8e2b25b92ccf8632929bc6d43467e17fe89c75e068"}, {file = "platformdirs-4.2.0.tar.gz", hash = "sha256:ef0cc731df711022c174543cb70a9b5bd22e5a9337c8624ef2c2ceb8ddad8768"}, @@ -473,6 +562,7 @@ version = "1.4.0" description = "plugin and hook calling mechanisms for python" optional = false python-versions = ">=3.8" +groups = ["main", "dev"] files = [ {file = "pluggy-1.4.0-py3-none-any.whl", hash = "sha256:7db9f7b503d67d1c5b95f59773ebb58a8c1c288129a88665838012cfb07b8981"}, {file = "pluggy-1.4.0.tar.gz", hash = "sha256:8c85c2876142a764e5b7548e7d9a0e0ddb46f5185161049a79b7e974454223be"}, @@ -488,6 +578,7 @@ version = "2.9.9" description = "psycopg2 - Python-PostgreSQL Database Adapter" optional = false python-versions = ">=3.7" +groups = ["main"] files = [ {file = "psycopg2-2.9.9-cp310-cp310-win32.whl", hash = "sha256:38a8dcc6856f569068b47de286b472b7c473ac7977243593a288ebce0dc89516"}, {file = "psycopg2-2.9.9-cp310-cp310-win_amd64.whl", hash = "sha256:426f9f29bde126913a20a96ff8ce7d73fd8a216cfb323b1f04da402d452853c3"}, @@ -510,6 +601,7 @@ version = "2.8.0" description = "JSON Web Token implementation in Python" optional = false python-versions = ">=3.7" +groups = ["main"] files = [ {file = "PyJWT-2.8.0-py3-none-any.whl", hash = "sha256:59127c392cc44c2da5bb3192169a91f429924e17aff6534d70fdc02ab3e04320"}, {file = "PyJWT-2.8.0.tar.gz", hash = "sha256:57e28d156e3d5c10088e0c68abb90bfac3df82b40a71bd0daa20c65ccd5c23de"}, @@ -527,18 +619,19 @@ version = "3.0.3" description = "python code static checker" optional = false python-versions = ">=3.8.0" +groups = ["dev"] files = [ {file = "pylint-3.0.3-py3-none-any.whl", hash = "sha256:7a1585285aefc5165db81083c3e06363a27448f6b467b3b0f30dbd0ac1f73810"}, {file = "pylint-3.0.3.tar.gz", hash = "sha256:58c2398b0301e049609a8429789ec6edf3aabe9b6c5fec916acd18639c16de8b"}, ] [package.dependencies] -astroid = ">=3.0.1,<=3.1.0-dev0" +astroid = ">=3.0.1,<=3.1.0.dev0" colorama = {version = ">=0.4.5", markers = "sys_platform == \"win32\""} dill = [ {version = ">=0.2", markers = "python_version < \"3.11\""}, {version = ">=0.3.7", markers = "python_version >= \"3.12\""}, - {version = ">=0.3.6", markers = "python_version >= \"3.11\" and python_version < \"3.12\""}, + {version = ">=0.3.6", markers = "python_version == \"3.11\""}, ] isort = ">=4.2.5,<5.13.0 || >5.13.0,<6" mccabe = ">=0.6,<0.8" @@ -556,6 +649,7 @@ version = "4.6.1" description = "Python driver for MongoDB " optional = false python-versions = ">=3.7" +groups = ["main"] files = [ {file = "pymongo-4.6.1-cp310-cp310-macosx_10_9_universal2.whl", hash = "sha256:4344c30025210b9fa80ec257b0e0aab5aa1d5cca91daa70d82ab97b482cc038e"}, {file = "pymongo-4.6.1-cp310-cp310-manylinux1_i686.whl", hash = "sha256:1c5654bb8bb2bdb10e7a0bc3c193dd8b49a960b9eebc4381ff5a2043f4c3c441"}, @@ -646,9 +740,9 @@ dnspython = ">=1.16.0,<3.0.0" [package.extras] aws = ["pymongo-auth-aws (<2.0.0)"] -encryption = ["certifi", "pymongo[aws]", "pymongocrypt (>=1.6.0,<2.0.0)"] -gssapi = ["pykerberos", "winkerberos (>=0.5.0)"] -ocsp = ["certifi", "cryptography (>=2.5)", "pyopenssl (>=17.2.0)", "requests (<3.0.0)", "service-identity (>=18.1.0)"] +encryption = ["certifi ; os_name == \"nt\" or sys_platform == \"darwin\"", "pymongo[aws]", "pymongocrypt (>=1.6.0,<2.0.0)"] +gssapi = ["pykerberos ; os_name != \"nt\"", "winkerberos (>=0.5.0) ; os_name == \"nt\""] +ocsp = ["certifi ; os_name == \"nt\" or sys_platform == \"darwin\"", "cryptography (>=2.5)", "pyopenssl (>=17.2.0)", "requests (<3.0.0)", "service-identity (>=18.1.0)"] snappy = ["python-snappy"] test = ["pytest (>=7)"] zstd = ["zstandard"] @@ -659,6 +753,7 @@ version = "8.0.0" description = "pytest: simple powerful testing with Python" optional = false python-versions = ">=3.8" +groups = ["main", "dev"] files = [ {file = "pytest-8.0.0-py3-none-any.whl", hash = "sha256:50fb9cbe836c3f20f0dfa99c565201fb75dc54c8d76373cd1bde06b06657bdb6"}, {file = "pytest-8.0.0.tar.gz", hash = "sha256:249b1b0864530ba251b7438274c4d251c58d868edaaec8762893ad4a0d71c36c"}, @@ -681,6 +776,7 @@ version = "4.8.0" description = "A Django plugin for pytest." optional = false python-versions = ">=3.8" +groups = ["main"] files = [ {file = "pytest-django-4.8.0.tar.gz", hash = "sha256:5d054fe011c56f3b10f978f41a8efb2e5adfc7e680ef36fb571ada1f24779d90"}, {file = "pytest_django-4.8.0-py3-none-any.whl", hash = "sha256:ca1ddd1e0e4c227cf9e3e40a6afc6d106b3e70868fd2ac5798a22501271cd0c7"}, @@ -699,6 +795,7 @@ version = "1.0.1" description = "Read key-value pairs from a .env file and set them as environment variables" optional = false python-versions = ">=3.8" +groups = ["main"] files = [ {file = "python-dotenv-1.0.1.tar.gz", hash = "sha256:e324ee90a023d808f1959c46bcbc04446a10ced277783dc6ee09987c37ec10ca"}, {file = "python_dotenv-1.0.1-py3-none-any.whl", hash = "sha256:f7b63ef50f1b690dddf550d03497b66d609393b40b564ed0d674909a68ebf16a"}, @@ -713,6 +810,7 @@ version = "2023.3.post1" description = "World timezone definitions, modern and historical" optional = false python-versions = "*" +groups = ["main"] files = [ {file = "pytz-2023.3.post1-py2.py3-none-any.whl", hash = "sha256:ce42d816b81b68506614c11e8937d3aa9e41007ceb50bfdcb0749b921bf646c7"}, {file = "pytz-2023.3.post1.tar.gz", hash = "sha256:7b4fddbeb94a1eba4b557da24f19fdf9db575192544270a9101d8509f9f43d7b"}, @@ -724,6 +822,7 @@ version = "2.32.3" description = "Python HTTP for Humans." optional = false python-versions = ">=3.8" +groups = ["main"] files = [ {file = "requests-2.32.3-py3-none-any.whl", hash = "sha256:70761cfe03c773ceb22aa2f671b4757976145175cdfca038c02654d061d6dcc6"}, {file = "requests-2.32.3.tar.gz", hash = "sha256:55365417734eb18255590a9ff9eb97e9e1da868d4ccd6402399eaf68af20a760"}, @@ -745,6 +844,7 @@ version = "1.16.0" description = "Python 2 and 3 compatibility utilities" optional = false python-versions = ">=2.7, !=3.0.*, !=3.1.*, !=3.2.*" +groups = ["main"] files = [ {file = "six-1.16.0-py2.py3-none-any.whl", hash = "sha256:8abb2f1d86890a2dfb989f9a77cfcfd3e47c2a354b01111771326f8aa26e0254"}, {file = "six-1.16.0.tar.gz", hash = "sha256:1e61c37477a1626458e36f7b1d82aa5c9b094fa4802892072e49de9c60c4c926"}, @@ -756,6 +856,7 @@ version = "0.4.4" description = "A non-validating SQL parser." optional = false python-versions = ">=3.5" +groups = ["main"] files = [ {file = "sqlparse-0.4.4-py3-none-any.whl", hash = "sha256:5430a4fe2ac7d0f93e66f1efc6e1338a41884b7ddf2a350cedd20ccc4d9d28f3"}, {file = "sqlparse-0.4.4.tar.gz", hash = "sha256:d446183e84b8349fa3061f0fe7f06ca94ba65b426946ffebe6e3e8295332420c"}, @@ -772,6 +873,8 @@ version = "2.0.1" description = "A lil' TOML parser" optional = false python-versions = ">=3.7" +groups = ["main", "dev"] +markers = "python_version == \"3.10\"" files = [ {file = "tomli-2.0.1-py3-none-any.whl", hash = "sha256:939de3e7a6161af0c887ef91b7d41a53e7c5a1ca976325f429cb46ea9bc30ecc"}, {file = "tomli-2.0.1.tar.gz", hash = "sha256:de526c12914f0c550d15924c62d72abc48d6fe7364aa87328337a31007fe8a4f"}, @@ -783,6 +886,7 @@ version = "0.12.3" description = "Style preserving TOML library" optional = false python-versions = ">=3.7" +groups = ["dev"] files = [ {file = "tomlkit-0.12.3-py3-none-any.whl", hash = "sha256:b0a645a9156dc7cb5d3a1f0d4bab66db287fcb8e0430bdd4664a095ea16414ba"}, {file = "tomlkit-0.12.3.tar.gz", hash = "sha256:75baf5012d06501f07bee5bf8e801b9f343e7aac5a92581f20f80ce632e6b5a4"}, @@ -794,6 +898,8 @@ version = "4.9.0" description = "Backported and Experimental Type Hints for Python 3.8+" optional = false python-versions = ">=3.8" +groups = ["main", "dev"] +markers = "python_version == \"3.10\"" files = [ {file = "typing_extensions-4.9.0-py3-none-any.whl", hash = "sha256:af72aea155e91adfc61c3ae9e0e342dbc0cba726d6cba4b6c72c1f34e47291cd"}, {file = "typing_extensions-4.9.0.tar.gz", hash = "sha256:23478f88c37f27d76ac8aee6c905017a143b0b1b886c3c9f66bc2fd94f9f5783"}, @@ -805,6 +911,8 @@ version = "2023.4" description = "Provider of IANA time zone data" optional = false python-versions = ">=2" +groups = ["main"] +markers = "sys_platform == \"win32\" or platform_system == \"Windows\"" files = [ {file = "tzdata-2023.4-py2.py3-none-any.whl", hash = "sha256:aa3ace4329eeacda5b7beb7ea08ece826c28d761cda36e747cfbf97996d39bf3"}, {file = "tzdata-2023.4.tar.gz", hash = "sha256:dd54c94f294765522c77399649b4fefd95522479a664a0cec87f41bebc6148c9"}, @@ -816,6 +924,7 @@ version = "5.2" description = "tzinfo object for the local timezone" optional = false python-versions = ">=3.8" +groups = ["main"] files = [ {file = "tzlocal-5.2-py3-none-any.whl", hash = "sha256:49816ef2fe65ea8ac19d19aa7a1ae0551c834303d5014c6d5a62e4cbda8047b8"}, {file = "tzlocal-5.2.tar.gz", hash = "sha256:8d399205578f1a9342816409cc1e46a93ebd5755e39ea2d85334bea911bf0e6e"}, @@ -833,18 +942,19 @@ version = "2.2.1" description = "HTTP library with thread-safe connection pooling, file post, and more." optional = false python-versions = ">=3.8" +groups = ["main"] files = [ {file = "urllib3-2.2.1-py3-none-any.whl", hash = "sha256:450b20ec296a467077128bff42b73080516e71b56ff59a60a02bef2232c4fa9d"}, {file = "urllib3-2.2.1.tar.gz", hash = "sha256:d0570876c61ab9e520d776c38acbbb5b05a776d3f9ff98a5c8fd5162a444cf19"}, ] [package.extras] -brotli = ["brotli (>=1.0.9)", "brotlicffi (>=0.8.0)"] +brotli = ["brotli (>=1.0.9) ; platform_python_implementation == \"CPython\"", "brotlicffi (>=0.8.0) ; platform_python_implementation != \"CPython\""] h2 = ["h2 (>=4,<5)"] socks = ["pysocks (>=1.5.6,!=1.5.7,<2.0)"] zstd = ["zstandard (>=0.18.0)"] [metadata] -lock-version = "2.0" +lock-version = "2.1" python-versions = "^3.10" -content-hash = "105128493fba0d423e8d7c55814405166dbd8f92407fc74c0abbfa7b922a7ce3" +content-hash = "e00af61ead528f6088e7c5565e0507a2129edc203e40ecc9d69a02172238c762" diff --git a/backend/pyproject.toml b/backend/pyproject.toml index 9e73a701..c02a0f20 100644 --- a/backend/pyproject.toml +++ b/backend/pyproject.toml @@ -17,6 +17,7 @@ djangorestframework-simplejwt = "^5.3.1" python-dotenv = "^1.0.1" aenum = "^3.1.15" psycopg2 = "^2.9.9" +bcrypt = "^4.1.2" django-apscheduler = "^0.6.2" requests = "^2.32.3" diff --git a/docker-compose-dev.yml b/docker-compose-dev.yml new file mode 100644 index 00000000..75b54e83 --- /dev/null +++ b/docker-compose-dev.yml @@ -0,0 +1,47 @@ +services: + api: + build: + context: "./backend" + dockerfile: Dockerfile + ports: + - "8000:8000" + depends_on: + - db + env_file: + - .env + environment: + - DJANGO_SECRET_KEY=${DJANGO_SECRET_KEY} + - TEST_DB_NAME=${POSTGRES_DB:-ilotalo_dev} + - TEST_DB_USER=${POSTGRES_USER:-user} + - TEST_DB_PASSWORD=${POSTGRES_PASSWORD:-password} + - TEST_DB_HOST=db + - TEST_DB_PORT=5432 + + frontend: + build: + context: "./frontend" + dockerfile: Dockerfile + ports: + - "5173:5173" + depends_on: + - api + env_file: + - .env + environment: + - SITE_KEY=${SITE_KEY} + + db: + image: postgres:18 + volumes: + - postgres_data_dev:/var/lib/postgresql + env_file: + - .env + environment: + - POSTGRES_DB=${POSTGRES_DB:-ilotalo_dev} + - POSTGRES_USER=${POSTGRES_USER:-user} + - POSTGRES_PASSWORD=${POSTGRES_PASSWORD:-password} + ports: + - "5433:5432" + +volumes: + postgres_data_dev: diff --git a/docker-compose-prod.yml b/docker-compose-prod.yml new file mode 100644 index 00000000..ef578a06 --- /dev/null +++ b/docker-compose-prod.yml @@ -0,0 +1,29 @@ +services: + api: + build: + context: "./backend" + dockerfile: Dockerfile + ports: + - "8000:8000" + env_file: + - .env + environment: + - DJANGO_SECRET_KEY=${DJANGO_SECRET_KEY} + - TEST_DB_NAME=${POSTGRES_DB} + - TEST_DB_USER=${POSTGRES_USER} + - TEST_DB_PASSWORD=${POSTGRES_PASSWORD} + - TEST_DB_HOST=${POSTGRES_HOST} + - TEST_DB_PORT=${POSTGRES_PORT:-5432} + + frontend: + build: + context: "./frontend" + dockerfile: Dockerfile + ports: + - "5173:5173" + depends_on: + - api + env_file: + - .env + environment: + - SITE_KEY=${SITE_KEY} \ No newline at end of file diff --git a/frontend/src/axios.js b/frontend/src/axios.js index 925e5a53..c074d129 100644 --- a/frontend/src/axios.js +++ b/frontend/src/axios.js @@ -8,12 +8,14 @@ const axiosClient = axios.create({ }); // Checks the authorization of the user using axios - axiosClient.interceptors.request.use((config) => { const token = localStorage.getItem("ACCESS_TOKEN"); - //const refreshtoken = localStorage.getItem('REFRESH_TOKEN') - config.headers = config.headers || {}; - config.headers.Authorization = `Bearer ${token}`; + + // Only add header if token exists and isn't "undefined" or "null" string + if (token && token !== "undefined" && token !== "null") { + config.headers.Authorization = `Bearer ${token}`; + } + return config; }); @@ -23,15 +25,20 @@ axiosClient.interceptors.response.use( }, (error) => { const { response } = error; - if (response.status === 401) { + + // If token is invalid or expired (401), clear local storage + if (response && response.status === 401) { localStorage.removeItem("ACCESS_TOKEN"); - // window.location.reload() - } else if (response.status === 404) { - //Show not found + localStorage.removeItem("loggedUser"); + + // Optional: Redirect to login if not already there + if (!window.location.pathname.includes('/login')) { + window.location.href = "/login"; + } } throw error; }, ); -export default axiosClient; +export default axiosClient; \ No newline at end of file diff --git a/frontend/src/components/ReservationsView.jsx b/frontend/src/components/ReservationsView.jsx index 179ffc25..9c87c060 100644 --- a/frontend/src/components/ReservationsView.jsx +++ b/frontend/src/components/ReservationsView.jsx @@ -24,6 +24,7 @@ const ReservationsView = ({ handleAddNewEventClick, handleSelectSlot, handleSelectEvent, + onNavigate, showCreateModal, handleCloseModal, handleInputChange, @@ -158,6 +159,7 @@ const ReservationsView = ({ selectable onSelectSlot={handleSelectSlot} onSelectEvent={handleSelectEvent} + onNavigate={onNavigate} firstDay={1} eventPropGetter={(event) => ({ style: { diff --git a/frontend/src/components/YkvLogoutFunction.jsx b/frontend/src/components/YkvLogoutFunction.jsx index 281738f8..44a8db64 100644 --- a/frontend/src/components/YkvLogoutFunction.jsx +++ b/frontend/src/components/YkvLogoutFunction.jsx @@ -157,7 +157,8 @@ const YkvLogoutFunction = ({ const fetchResponsibilities = async () => { try { const res = await axiosClient.get("/listobjects/nightresponsibilities/"); - const userData = res.data.map((u) => ({ + const rawData = res.data.results || res.data; + const userData = rawData.map((u) => ({ id: u.id, // DataGrid requires a unique 'id' for each row Vastuuhenkilö: u.user.username, Vastuussa: u.responsible_for, diff --git a/frontend/src/pages/cleaningschedulepage.jsx b/frontend/src/pages/cleaningschedulepage.jsx index 31d6ef9e..97fd793d 100644 --- a/frontend/src/pages/cleaningschedulepage.jsx +++ b/frontend/src/pages/cleaningschedulepage.jsx @@ -117,9 +117,10 @@ const CleaningSchedule = ({ } function getOrgId(orgName) { - for (let i = 0; i < orgdata.data.length; i++) { - if (orgdata.data[i].name === orgName) { - return orgdata.data[i].id; + const orgs = orgdata.data.results || orgdata.data; + for (let i = 0; i < orgs.length; i++) { + if (orgs[i].name === orgName) { + return orgs[i].id; } } }; @@ -164,9 +165,10 @@ const CleaningSchedule = ({ axiosClient .get("/listobjects/cleaning/") .then((res) => { - setRawCleaningData(res.data); + const rawData = res.data.results || res.data; + setRawCleaningData(rawData); - const cleaningData = res.data.map((u, index) => ({ + const cleaningData = rawData.map((u, index) => ({ id: u.week, week: u.week, date: moment().day("Monday").week(u.week), diff --git a/frontend/src/pages/cleaningsuppliespage.jsx b/frontend/src/pages/cleaningsuppliespage.jsx index ce8143c1..7fdf29ca 100644 --- a/frontend/src/pages/cleaningsuppliespage.jsx +++ b/frontend/src/pages/cleaningsuppliespage.jsx @@ -125,7 +125,8 @@ const CleaningSupplies = ({ axiosClient .get("/listobjects/cleaningsupplies/") .then((res) => { - const suppliesData = res.data.map((u, index) => ({ + const rawData = res.data.results || res.data; + const suppliesData = rawData.map((u, index) => ({ id: u.id, // DataGrid requires a unique 'id' for each row tool: u.tool, })); diff --git a/frontend/src/pages/defectfaultpage.jsx b/frontend/src/pages/defectfaultpage.jsx index 5c20455c..40954896 100644 --- a/frontend/src/pages/defectfaultpage.jsx +++ b/frontend/src/pages/defectfaultpage.jsx @@ -149,7 +149,8 @@ const DefectFault = ({ axiosClient .get("/listobjects/defects/") .then((res) => { - const defectData = res.data.map((u, index) => ({ + const rawData = res.data.results || res.data; + const defectData = rawData.map((u, index) => ({ id: u.id, // DataGrid requires a unique 'id' for each row description: u.description, time: new Date(u.time), diff --git a/frontend/src/pages/frontpage.jsx b/frontend/src/pages/frontpage.jsx index 281fdaab..17d11129 100644 --- a/frontend/src/pages/frontpage.jsx +++ b/frontend/src/pages/frontpage.jsx @@ -14,10 +14,21 @@ const FrontPage = () => { // Fetch the events to be shown from the backend useEffect(() => { + const now = new Date(); + const futureLimit = new Date(); + futureLimit.setDate(now.getDate() + 30); // Fetch next 30 days + axios - .get(`${API_URL}/api/listobjects/events/`) + .get(`${API_URL}/api/listobjects/events/`, { + params: { + start: now.toISOString(), + end: futureLimit.toISOString() + } + }) .then((response) => { - const events = response.data + // Handle both paginated and non-paginated responses + const rawData = response.data.results || response.data; + const events = rawData .filter( (event) => new Date() < new Date(event.start) && event.open == true, ) diff --git a/frontend/src/pages/ownkeys.jsx b/frontend/src/pages/ownkeys.jsx index 2fa98667..1c8087af 100644 --- a/frontend/src/pages/ownkeys.jsx +++ b/frontend/src/pages/ownkeys.jsx @@ -96,7 +96,8 @@ const OwnKeys = ({ const loginTime = getCurrentDateTime(); const userdata = await axiosClient.get("/listobjects/users/"); - const user = userdata.data.find((user) => user.id === user_id); + const rawUsers = userdata.data.results || userdata.data; + const user = rawUsers.find((user) => user.id === user_id); const user_orgs = user.keys.map((key) => key.id); const responsibilityObject = { @@ -157,8 +158,9 @@ const OwnKeys = ({ const getResponsibility = async () => { try { const response = await axiosClient.get(`listobjects/nightresponsibilities/`); - setAllResponsibilities(response.data); - const filteredResponsibilities = response.data.filter( + const rawData = response.data.results || response.data; + setAllResponsibilities(rawData); + const filteredResponsibilities = rawData.filter( (item) => item.email === email || (loggedUser && item.created_by === loggedUser.username), @@ -172,8 +174,9 @@ const OwnKeys = ({ const getActiveResponsibilities = async () => { try { const response = await axiosClient.get(`listobjects/nightresponsibilities/`); - setAllResponsibilities(response.data); - const active = response.data.filter((item) => item.present === true); + const rawData = response.data.results || response.data; + setAllResponsibilities(rawData); + const active = rawData.filter((item) => item.present === true); setActiveResponsibilities(active); } catch (error) { console.error("Error fetching responsibilities", error); diff --git a/frontend/src/pages/ownpage.jsx b/frontend/src/pages/ownpage.jsx index b80b1724..129fd62d 100644 --- a/frontend/src/pages/ownpage.jsx +++ b/frontend/src/pages/ownpage.jsx @@ -261,7 +261,8 @@ const OwnPage = ({ isLoggedIn: propIsLoggedIn }) => { try { const res = await axiosClient.get("listobjects/organizations/") - const orgData = res.data.map((u) => ({ + const rawData = res.data.results || res.data; + const orgData = rawData.map((u) => ({ id: u.id, Organisaatio: u.name, email: u.email, @@ -363,7 +364,8 @@ const OwnPage = ({ isLoggedIn: propIsLoggedIn }) => { setError(t("emailinuse")); handleSnackbar(t("emailinuse"), "error"); setTimeout(() => setError(""), 5000); - } else { + } + else { const organizationObject = { name: organization_name, email: organization_email, @@ -395,7 +397,8 @@ const OwnPage = ({ isLoggedIn: propIsLoggedIn }) => { const getAllUsers = async () => { try { const response = await axiosClient.get("listobjects/users/"); - const userData = response.data.map((u) => ({ + const rawData = response.data.results || response.data; + const userData = rawData.map((u) => ({ id: u.id, Käyttäjänimi: u.username, email: u.email, diff --git a/frontend/src/pages/reservations.jsx b/frontend/src/pages/reservations.jsx index 682dfb30..c40e65b6 100644 --- a/frontend/src/pages/reservations.jsx +++ b/frontend/src/pages/reservations.jsx @@ -25,6 +25,7 @@ moment.locale("fi"); const MyCalendar = () => { // State variables for event data and modals const [events, setEvents] = useState([]); + const [loadedRanges, setLoadedRanges] = useState([]); // Track ranges already fetched const [organizations, setOrganizations] = useState([]); const [selectedSlot, setSelectedSlot] = useState(null); const [selectedEvent, setSelectedEvent] = useState(null); @@ -46,10 +47,67 @@ const MyCalendar = () => { const { t } = useTranslation(); - // Calls getEvents() to fetch events when starting the page + // Calls getEvents() to fetch events when starting the page or view changes + const [viewDate, setViewDate] = useState(new Date()); + useEffect(() => { - getEvents(); - }, []); + getEvents(viewDate); + }, [viewDate]); + + // Gets events for the current view from backend + const getEvents = (date, isPrefetch = false) => { + const startRange = isPrefetch + ? moment(date).subtract(1, 'months').startOf('month') + : moment(date).startOf('month').subtract(7, 'days'); + + const endRange = isPrefetch + ? moment(date).add(1, 'months').endOf('month') + : moment(date).endOf('month').add(7, 'days'); + + // If prefetching, we only care if the WHOLE range is already loaded. + // If not prefetching, we check if the requested month is already loaded. + const isLoaded = loadedRanges.some(range => + startRange.isSameOrAfter(range.start) && endRange.isSameOrBefore(range.end) + ); + + if (isLoaded) return; + + axiosClient + .get("/listobjects/events/", { + params: { + start: startRange.toISOString(), + end: endRange.toISOString() + } + }) + .then((response) => { + const rawData = response.data.results || response.data; + const newEventsList = rawData.map((event) => ({ + ...event, + start: new Date(event.start), + end: new Date(event.end), + })); + + setEvents(prevEvents => { + const existingIds = new Set(prevEvents.map(e => e.id)); + const uniqueNewEvents = newEventsList.filter(e => !existingIds.has(e.id)); + return [...prevEvents, ...uniqueNewEvents]; + }); + + setLoadedRanges(prev => [...prev, { start: startRange, end: endRange }]); + + // If we just finished loading the current month, now trigger the background prefetch + if (!isPrefetch) { + getEvents(date, true); + } + }) + .catch((error) => { + console.error(t("errorfetchevents"), error); + }); + }; + + const handleNavigate = (newDate) => { + setViewDate(newDate); + }; const startRef = useRef(0); const endRef = useRef(0); @@ -71,23 +129,6 @@ const MyCalendar = () => { } }, [endRef.current.value]); - // Gets all created events from backend - const getEvents = () => { - axios - .get(`${API_URL}/api/listobjects/events/`) - .then((response) => { - const events = response.data.map((event) => ({ - ...event, - start: new Date(event.start), - end: new Date(event.end), - })); - setEvents(events); - }) - .catch((error) => { - console.error(t("errorfetchevents"), error); - }); - }; - useEffect(() => { getOrganizations(); }, []); @@ -96,7 +137,7 @@ const MyCalendar = () => { axios .get(`${API_URL}/api/listobjects/organizations/`) .then((response) => { - const organizations = response.data; + const organizations = response.data.results || response.data; setOrganizations(organizations); }) .catch((error) => { @@ -118,8 +159,8 @@ const MyCalendar = () => { useEffect(() => { if (showCreateModal && selectedSlot) { if (!startRef.current || !endRef.current) { - startRef.current = { value: "" }; - endRef.current = { value: "" }; + startRef.current = { value: "" }; + endRef.current = { value: "" }; } startRef.current.value = moment(selectedSlot.start).format( "YYYY-MM-DDTHH:mm", @@ -280,6 +321,7 @@ const MyCalendar = () => { handleAddNewEventClick={handleAddNewEventClick} handleSelectSlot={handleSelectSlot} handleSelectEvent={handleSelectEvent} + onNavigate={handleNavigate} showCreateModal={showCreateModal} handleCloseModal={handleCloseModal} handleInputChange={handleInputChange} @@ -298,4 +340,4 @@ const MyCalendar = () => { ); }; -export default MyCalendar; +export default MyCalendar; \ No newline at end of file diff --git a/frontend/src/pages/statistics.jsx b/frontend/src/pages/statistics.jsx index 4358d8ae..29ece4a1 100644 --- a/frontend/src/pages/statistics.jsx +++ b/frontend/src/pages/statistics.jsx @@ -6,25 +6,40 @@ import axiosClient from "../axios"; import { PieChart } from "@mui/x-charts/PieChart"; import { BarChart } from "@mui/x-charts/BarChart"; import { LineChart } from "@mui/x-charts/LineChart"; -import { Grid } from "@mui/material"; +import { Grid, Box, Typography, TextField, Radio, RadioGroup, FormControlLabel, FormControl, FormLabel, Stack } from "@mui/material"; import { CSVLink } from "react-csv"; import { getCurrentDateTime } from "../utils/timehelpers"; import Button from "@mui/material/Button"; -import Radio from "@mui/material/Radio"; -import RadioGroup from "@mui/material/RadioGroup"; -import FormControlLabel from "@mui/material/FormControlLabel"; -import FormControl from "@mui/material/FormControl"; -import FormLabel from "@mui/material/FormLabel"; import DownloadIcon from "@mui/icons-material/Download"; import { useTranslation } from "react-i18next"; const API_URL = process.env.VITE_API_URL; -// This page is used to display statistics about users and organizations +// Color palette for organizations +const ORG_COLORS = [ + "#2196f3", "#4caf50", "#ff9800", "#f44336", "#9c27b0", + "#00bcd4", "#ffeb3b", "#795548", "#607d8b", "#e91e63", + "#3f51b5", "#009688", "#8bc34a", "#cddc39", "#ffc107", + "#ff5722", "#9e9e9e", "#03a9f4", "#43a047", "#d81b60" +]; + +const generateRandomColor = (seed) => { + let hash = 0; + for (let i = 0; i < seed.length; i++) { + hash = seed.charCodeAt(i) + ((hash << 5) - hash); + } + let color = '#'; + for (let i = 0; i < 3; i++) { + const value = (hash >> (i * 8)) & 0xFF; + color += ('00' + value.toString(16)).substr(-2); + } + return color; +}; + const Statistics = () => { const [username, setUsername] = useState(null); const [userRole, setUserRole] = useState(null); - + const [orgColorMap, setOrgColorMap] = useState({}); // YKV by organization const [orgStatsData, setOrgStatsData] = useState([]); @@ -36,19 +51,22 @@ const Statistics = () => { const [shouldDownload, setShouldDownload] = useState(false); // Time filters - const [minFilter, setMinFilter] = useState(""); - const [maxFilter, setMaxFilter] = useState(""); + // Default filters to the last 24 hours + const now = new Date(); + const yesterday = new Date(now.getTime() - 24 * 60 * 60 * 1000); + const formatForInput = (date) => { + const tzoffset = (new Date()).getTimezoneOffset() * 60000; //offset in milliseconds + return (new Date(date - tzoffset)).toISOString().slice(0, 16); + }; + + const [minFilter, setMinFilter] = useState(formatForInput(yesterday)); + const [maxFilter, setMaxFilter] = useState(formatForInput(now)); // Fetched data from the backend const [fetchedData, setFetchedData] = useState(null); // YKV login and logout times per hour data const [logTimesData, setLogTimesData] = useState(null); - - // Window height and width in px & grid column width - const [winHeight, setWinHeight] = useState(window.innerHeight); - const [winWidth, setWinWidth] = useState(window.innerWidth); - const [columnWidth, setColumnWidth] = useState(6); const [widthDivider, setWidthDivider] = useState(2.5); // YKV per weekday data @@ -66,147 +84,124 @@ const Statistics = () => { // Gets the user's role from backend and fetches data. Also adjusts the grid column width if the user device is mobile useEffect(() => { - getPermission(); - if (fetchedData) { - processOrgStats( - fetchedData.orgResponse.data, - fetchedData.responsibilitiesResponse.data, - ); - processAllUserStats( - fetchedData.userResponse.data, - fetchedData.responsibilitiesResponse.data, - fetchedData.orgResponse.data, - ); - } else if (localStorage.getItem("ACCESS_TOKEN")) { - fetchData().then(setFetchedData); - } - if (window.innerWidth <= window.innerHeight) { - setColumnWidth(12); - setWidthDivider(1.2); - } + const init = async () => { + await getPermission(); + if (localStorage.getItem("ACCESS_TOKEN")) { + fetchData().then(setFetchedData); + } + }; + init(); }, []); // Updates the data when the filters change useEffect(() => { - if (fetchedData) { - processOrgStats( - fetchedData.orgResponse.data, - fetchedData.responsibilitiesResponse.data, - ); - processAllUserStats( - fetchedData.userResponse.data, - fetchedData.responsibilitiesResponse.data, - fetchedData.orgResponse.data, - ); + // Changes the grid column widths when the window is resized + if (fetchedData && userRole !== null) { + const orgs = fetchedData.orgResponse.data.results || fetchedData.orgResponse.data; + const resps = fetchedData.responsibilitiesResponse.data.results || fetchedData.responsibilitiesResponse.data; + const users = fetchedData.userResponse.data.results || fetchedData.userResponse.data; + processOrgStats(orgs, resps); + processAllUserStats(users, resps, orgs); } - }, [minFilter, maxFilter, fetchedData]); + }, [fetchedData, userRole, minFilter, maxFilter, selectedPie]); - // Changes the grid column widths when the window is resized useEffect(() => { - const handleResize = () => { - setWinWidth(window.innerWidth); - setWinHeight(window.innerHeight); - if (window.innerWidth <= window.innerHeight) { - setColumnWidth(12); - setWidthDivider(1.2); - } else { - setColumnWidth(6); - setWidthDivider(2.5); - } - }; - - window.addEventListener("resize", handleResize); - - return () => { - window.removeEventListener("resize", handleResize); + const updateWidth = () => { + setWidthDivider(window.innerWidth <= window.innerHeight ? 1.2 : 2.5); }; + updateWidth(); + window.addEventListener("resize", updateWidth); + return () => window.removeEventListener("resize", updateWidth); }, []); const { t } = useTranslation(); const fetchData = async () => { try { - const [orgResponse, userResponse, responsibilitiesResponse] = - await Promise.all([ - axiosClient.get("listobjects/organizations/"), - axiosClient.get("listobjects/users/"), - axiosClient.get("listobjects/nightresponsibilities/"), - ]); + const [orgResponse, userResponse, responsibilitiesResponse] = await Promise.all([ + axiosClient.get("listobjects/organizations/"), + axiosClient.get("listobjects/users/"), + axiosClient.get("listobjects/nightresponsibilities/"), + ]); return { orgResponse, userResponse, responsibilitiesResponse }; - } catch (error) { - console.error("Error fetching data", error); - } + } catch (error) { console.error("Error fetching data", error); } }; const getPermission = async () => { const accessToken = localStorage.getItem("ACCESS_TOKEN"); if (accessToken) { - await axios - .get(`${API_URL}/api/users/userinfo`, { - headers: { - Authorization: `Bearer ${accessToken}`, - }, - }) - .then((response) => { - setUsername(response.data.username); - setUserRole(response.data.role); + try { + const response = await axios.get(`${API_URL}/api/users/userinfo`, { + headers: { Authorization: `Bearer ${accessToken}` }, }); + setUsername(response.data.username); + setUserRole(response.data.role); + } catch (e) { + console.error(e); + setUserRole(5); // Fallback to basic role if info fetch fails but token exists + } } }; function filtering(login_time, logout_time) { - return ( - (Date.parse(login_time) >= Number(Date.parse(minFilter)) && - Date.parse(login_time) <= Number(Date.parse(maxFilter))) || - (Date.parse(logout_time) <= Number(Date.parse(maxFilter)) && - Date.parse(logout_time) >= Number(Date.parse(minFilter))) || - (Date.parse(login_time) >= Number(Date.parse(minFilter)) && - maxFilter === "") || - (Date.parse(logout_time) <= Number(Date.parse(maxFilter)) && - minFilter === "") || - (minFilter === "" && maxFilter === "") - ); + // Sets the organization data and the organization member data + const login = Date.parse(login_time); + const logout = Date.parse(logout_time); + const min = minFilter ? Date.parse(minFilter) : -Infinity; + const max = maxFilter ? Date.parse(maxFilter) : Infinity; + return (login >= min && login <= max) || (logout <= max && logout >= min); } - // Sets the organization data and the organization member data const processOrgStats = (orgData, responsibilities) => { const orgdata = {}; - const orgmemdata = {}; - - orgData.forEach((org) => { - orgdata[org.name] = { - value: 0, - label: org.name, - ...(org.color ? { color: org.color } : {}), - }; - orgmemdata[org.name] = { - value: org.user_set.length, - label: org.name, - ...(org.color ? { color: org.color } : {}), - }; + const newColorMap = { ...orgColorMap }; + const usedColors = new Set(Object.values(newColorMap)); + + const finalOrgData = orgData.map((org, index) => { + let color = org.color; + // Treat null, empty, or black as "missing color" + if (!color || color === "#000000" || color === "null") { + if (newColorMap[org.name]) { + color = newColorMap[org.name]; + } else { + const paletteColor = ORG_COLORS[index % ORG_COLORS.length]; + color = usedColors.has(paletteColor) ? generateRandomColor(org.name) : paletteColor; + newColorMap[org.name] = color; + usedColors.add(color); + } + } else { + usedColors.add(color); + } + return { ...org, assignedColor: color }; }); - setOrgMembersData(Object.values(orgmemdata)); + if (Object.keys(newColorMap).length > Object.keys(orgColorMap).length) { + setOrgColorMap(newColorMap); + } + + finalOrgData.forEach((org) => { + const baseObj = { id: org.id, label: org.name, color: org.assignedColor }; + orgdata[org.name] = { ...baseObj, value: 0 }; + orgmemdata[org.name] = { ...baseObj, value: org.user_set ? org.user_set.length : 0 }; + }); responsibilities.forEach((resp) => { resp.organizations.forEach((org) => { - if (filtering(resp.login_time, resp.logout_time)) { - orgdata[org.name] = { - ...orgdata[org.name], - value: orgdata[org.name].value + 1, - }; + if (filtering(resp.login_time, resp.logout_time) && orgdata[org.name]) { + orgdata[org.name].value += 1; } }); }); - const realdata = Object.values(orgdata); - setOrgStatsData(realdata); - if (selectedPie == 1) { - setPieChartData(Object.values(orgmemdata)); - } else if (selectedPie == 2) { - setPieChartData(Object.values(orgdata)); - } + const mData = Object.values(orgmemdata).sort((a, b) => b.value - a.value); + const sData = Object.values(orgdata).sort((a, b) => b.value - a.value); + setOrgMembersData(mData); + setOrgStatsData(sData); + + if (selectedPie === 1) setPieChartData(mData); + + else if (selectedPie === 2) setPieChartData(sData); }; // Sets the user data and ykv data @@ -222,283 +217,171 @@ const Statistics = () => { const numberdayweek = [6, 0, 1, 2, 3, 4, 5]; - orgdata.forEach((org) => { - latedata[org.name] = { - value: 0, - label: org.name, - ...(org.color ? { color: org.color } : {}), - }; - }); - users.forEach((usr) => { - userdata[usr.username] = { data: [0], label: usr.username }; + orgdata.forEach((org, index) => { + latedata[org.name] = { id: org.id, label: org.name, value: 0, color: org.color || orgColorMap[org.name] || ORG_COLORS[index % ORG_COLORS.length] }; }); + users.forEach((usr) => { userdata[usr.username] = { id: usr.id, data: [0], label: usr.username }; }); responsibilities.forEach((resp) => { - if (userdata[resp.user.username]) { - if (filtering(resp.login_time, resp.logout_time)) { - userdata[resp.user.username] = { - ...userdata[resp.user.username], - data: [userdata[resp.user.username].data[0] + 1], - }; - const loginhours = new Date(resp.login_time).getHours(); - const logouthours = new Date(resp.logout_time).getHours(); - logintimesdata[loginhours] += 1; - logouttimesdata[logouthours] += 1; - - const day = new Date(resp.login_time).getDay(); - lpddata[numberdayweek[day]] += 1; - - if (resp.late) { - resp.organizations.forEach((org) => { + if (userdata[resp.user.username] && filtering(resp.login_time, resp.logout_time)) { + userdata[resp.user.username].data[0] += 1; + logintimesdata[new Date(resp.login_time).getHours()] += 1; + if (resp.logout_time) logouttimesdata[new Date(resp.logout_time).getHours()] += 1; + lpddata[numberdayweek[new Date(resp.login_time).getDay()]] += 1; + if (resp.late) { + resp.organizations.forEach((org) => { + if (latedata[org.name]) { latedata[org.name].value += 1; - }); - } + } + }); } } }); - - setOrgLateData(Object.values(latedata)); - - Object.values(userdata).forEach((usr) => { - if (usr.data.reduce((partialSum, a) => partialSum + a, 0) === 0) { - delete userdata[usr.label]; - } - }); - const logs = [ - { - data: logintimesdata, - label: t("statslogin"), - color: "lightGreen", - showMark: ({ index }) => index === -1, - }, - { - data: logouttimesdata, - label: t("statslogout"), - color: "red", - showMark: ({ index }) => index === -1, - }, - ]; - setLogTimesData(logs); - - const logsperday = [{ data: lpddata }]; - setLogsPerWeekDayData(logsperday); - - const realdata = Object.values(userdata); - realdata.sort( - (a, b) => - parseFloat(b.data.reduce((partialSum, b) => partialSum + b, 0)) - - parseFloat(a.data.reduce((partialSum, a) => partialSum + a, 0)), - ); - setAllUserStatsData(realdata); - - if (selectedPie == 3) { - setPieChartData(Object.values(latedata)); - } + // Handles the creation of the event CSV file + const lateArr = Object.values(latedata).sort((a, b) => b.value - a.value); + setOrgLateData(lateArr); + setLogTimesData([ + { data: logintimesdata, label: t("statslogin"), color: "#4caf50", showMark: () => false }, + { data: logouttimesdata, label: t("statslogout"), color: "#f44336", showMark: () => false } + ]); + setLogsPerWeekDayData([{ data: lpddata, color: "#2196f3" }]); + setAllUserStatsData(Object.values(userdata) + .filter(u => u.data[0] > 0) + .sort((a, b) => b.data[0] - a.data[0])); + if (selectedPie === 3) setPieChartData(lateArr); }; - // Handles the creation of the event CSV file const handleCSV = async () => { try { - const events = await axiosClient.get("listobjects/events/"); - if (events.data) { - const data = [ - [ - "START", - "END", - "ORGANIZER", - "TITLE", - "DESCRIPTION", - "RESPONSIBLE", - "ROOM", - "OPEN", - ], - ]; - events.data.forEach((e) => { - if (filtering(e.start, e.end)) { - data.push([ - e.start, - e.end, - e.organizer.name, - e.title, - e.description, - e.responsible, - e.room, - e.open, - ]); - } - }); - setCSVdata(data); - setShouldDownload(true); - } + // Use current filters for the CSV download + const params = new URLSearchParams(); + if (minFilter) params.append("start", minFilter); + if (maxFilter) params.append("end", maxFilter); + + // If no filters are set, we explicitly ask for 'all' to ensure the backend + // doesn't just return the current month, but the full history. + // (The filtering(e.start, e.end) logic below will still apply if dates are set) + if (!minFilter && !maxFilter) params.append("all", "true"); + + const response = await axiosClient.get(`listobjects/events/?${params.toString()}`); + const rawData = response.data.results || response.data; + const data = [[ + "START", + "END", + "ORGANIZER", + "TITLE", + "DESCRIPTION", + "RESPONSIBLE", + "ROOM", + "OPEN" + ]]; + rawData.forEach((e) => { + if (filtering(e.start, e.end)) { + data.push([ + e.start, + e.end, + e.organizer ? e.organizer.name : "", + e.title, + e.description, + e.responsible, + e.room, + e.open + ]); + } + }); + setCSVdata(data); + setShouldDownload(true); } catch (error) { - console.error("Error fetching data", error); + console.error(error); } }; - const CSVDownload = (props) => { - const btnRef = useRef(null); - useEffect(() => { - if (btnRef.current) { - btnRef.current.click(); - setShouldDownload(false); - } - }, [btnRef]); - - return ( - - - - ); - }; - - const handleMaxFilterChange = (event) => { - setMaxFilter(event.target.value); - }; - const handleMinFilterChange = (event) => { - setMinFilter(event.target.value); - }; const date = getCurrentDateTime(); - // Handles the change of the pie chart data const handleChange = (event) => { - if (event.target.value == 1) { - setPieChartData(orgMembersData); - setSelectedPie(1); - } else if (event.target.value == 2) { - setPieChartData(orgStatsData); - setSelectedPie(2); - } else if (event.target.value == 3) { - setPieChartData(orgLateData); - setSelectedPie(3); - } + const val = parseInt(event.target.value); + setSelectedPie(val); }; - - if (userRole == null) { + if (userRole === null) { return

{t("login")}

; } return ( -
- - -
-

{t("timefilter")}

- - - -
+ + + + + {t("timefilter")} + + setMinFilter(e.target.value)} InputLabelProps={{ shrink: true }} fullWidth /> + setMaxFilter(e.target.value)} InputLabelProps={{ shrink: true }} fullWidth /> + + - -
- - {shouldDownload && CSVdata && ( - - )} -
+ + + {shouldDownload && CSVdata && } - -

{t("orgstats")}

- - - - } - label={t("orgstats_1")} - /> - } - label={t("orgstats_2")} - /> - } - label={t("orgstats_3")} - /> - - - + + + + {t("orgstats")} + + + } label={t("orgstats_1")} /> + } label={t("orgstats_2")} /> + } label={t("orgstats_3")} /> + + + + + d.value > 0), + innerRadius: 40, outerRadius: 130, paddingAngle: 2, cornerRadius: 5, + arcLabel: (item) => `${item.value}`, + }]} + width={400} height={350} slotProps={{ legend: { hidden: true } }} + /> + + + + {pieChartData.sort((a, b) => b.value - a.value).map((item, i) => ( + 0 ? 1 : 0.5 }}> + + 0 ? 'bold' : 'normal', flex: 1 }}>{item.label} + {item.value} + + ))} + + + + - -

{t("userstats_1")}

- + + + + {t("userstats_1")} + + - -

{t("userstats_2")}

- + + + + {t("userstats_2")} + + - -

{t("userstats_3")}

- + + + + {t("userstats_3")} + +
-
+ ); }; -export default Statistics; +export default Statistics; \ No newline at end of file diff --git a/frontend/src/utils/keyuserhelpers.js b/frontend/src/utils/keyuserhelpers.js index 602732c2..34e4e509 100644 --- a/frontend/src/utils/keyuserhelpers.js +++ b/frontend/src/utils/keyuserhelpers.js @@ -36,8 +36,8 @@ export const fetchAllUsersWithKeys = async ({ }) => { try { const response = await axios.get(`${API_URL}/api/listobjects/users/`); - const allUsers = response.data; - const filteredUsers = allUsers.filter((user) => + const rawData = response.data.results || response.data; + const filteredUsers = rawData.filter((user) => checkUser(user, loggedUser, allResponsibilities), ); setAllUsersWithKeys(filteredUsers); From 8ed501f67e9f942c40ff0577438b5e11ba1e92e8 Mon Sep 17 00:00:00 2001 From: iPegii <51372604+iPegii@users.noreply.github.com> Date: Mon, 2 Feb 2026 20:15:50 +0200 Subject: [PATCH 002/149] refactor: fix Gemini CLI's weird quirks --- frontend/src/axios.js | 9 ++- frontend/src/components/YkvLogoutFunction.jsx | 2 +- frontend/src/pages/cleaningschedulepage.jsx | 32 ++++----- frontend/src/pages/cleaningsuppliespage.jsx | 10 +-- frontend/src/pages/defectfaultpage.jsx | 16 ++--- frontend/src/pages/frontpage.jsx | 2 +- frontend/src/pages/ownkeys.jsx | 6 +- frontend/src/pages/ownpage.jsx | 69 +++++++++---------- frontend/src/pages/reservations.jsx | 52 +++++++------- frontend/src/pages/statistics.jsx | 11 +-- frontend/src/utils/keyuserhelpers.js | 2 +- 11 files changed, 105 insertions(+), 106 deletions(-) diff --git a/frontend/src/axios.js b/frontend/src/axios.js index c074d129..2b2ef146 100644 --- a/frontend/src/axios.js +++ b/frontend/src/axios.js @@ -8,14 +8,13 @@ const axiosClient = axios.create({ }); // Checks the authorization of the user using axios + axiosClient.interceptors.request.use((config) => { const token = localStorage.getItem("ACCESS_TOKEN"); - - // Only add header if token exists and isn't "undefined" or "null" string + if (token && token !== "undefined" && token !== "null") { config.headers.Authorization = `Bearer ${token}`; } - return config; }); @@ -25,12 +24,12 @@ axiosClient.interceptors.response.use( }, (error) => { const { response } = error; - + // If token is invalid or expired (401), clear local storage if (response && response.status === 401) { localStorage.removeItem("ACCESS_TOKEN"); localStorage.removeItem("loggedUser"); - + // Optional: Redirect to login if not already there if (!window.location.pathname.includes('/login')) { window.location.href = "/login"; diff --git a/frontend/src/components/YkvLogoutFunction.jsx b/frontend/src/components/YkvLogoutFunction.jsx index 44a8db64..e1d2873b 100644 --- a/frontend/src/components/YkvLogoutFunction.jsx +++ b/frontend/src/components/YkvLogoutFunction.jsx @@ -157,7 +157,7 @@ const YkvLogoutFunction = ({ const fetchResponsibilities = async () => { try { const res = await axiosClient.get("/listobjects/nightresponsibilities/"); - const rawData = res.data.results || res.data; + const rawData = res.data; const userData = rawData.map((u) => ({ id: u.id, // DataGrid requires a unique 'id' for each row Vastuuhenkilö: u.user.username, diff --git a/frontend/src/pages/cleaningschedulepage.jsx b/frontend/src/pages/cleaningschedulepage.jsx index 97fd793d..b0a49238 100644 --- a/frontend/src/pages/cleaningschedulepage.jsx +++ b/frontend/src/pages/cleaningschedulepage.jsx @@ -91,14 +91,14 @@ const CleaningSchedule = ({ const handleSaveClose = () => { setSaveDialogOpen(false); }; - + const handleFormSubmit = async (json) => { const orgdata = await axiosClient.get("/listobjects/organizations/"); if (allCleaning.length > 0) { - setError(t("cleaningerrorold")); - handleSnackbar(t("cleaningerrorold"), "error"); - return; + setError(t("cleaningerrorold")); + handleSnackbar(t("cleaningerrorold"), "error"); + return; } iterateThroughJSON(json); @@ -117,7 +117,7 @@ const CleaningSchedule = ({ } function getOrgId(orgName) { - const orgs = orgdata.data.results || orgdata.data; + const orgs = orgdata.data; for (let i = 0; i < orgs.length; i++) { if (orgs[i].name === orgName) { return orgs[i].id; @@ -165,7 +165,7 @@ const CleaningSchedule = ({ axiosClient .get("/listobjects/cleaning/") .then((res) => { - const rawData = res.data.results || res.data; + const rawData = res.data; setRawCleaningData(rawData); const cleaningData = rawData.map((u, index) => ({ @@ -201,9 +201,9 @@ const CleaningSchedule = ({ {loggedUser && loggedUser.role === 1 && ( handleFormSubmit(newData)} /> - + - -
+ {t("userrole")}: {ROLE_DESCRIPTIONS[role]}
diff --git a/frontend/src/pages/statistics.jsx b/frontend/src/pages/statistics.jsx index ef366855..19298d55 100644 --- a/frontend/src/pages/statistics.jsx +++ b/frontend/src/pages/statistics.jsx @@ -35,11 +35,11 @@ const generateRandomColor = (seed) => { // This page is used to display statistics about users and organizations const Statistics = () => { - const [username, setUsername] = useState(null); + const [, setUsername] = useState(null); const [userRole, setUserRole] = useState(null); const [orgColorMap, setOrgColorMap] = useState({}); // YKV by organization - const [orgStatsData, setOrgStatsData] = useState([]); + const [, setOrgStatsData] = useState([]); // YKV count by user const [allUserStatsData, setAllUserStatsData] = useState([]); @@ -65,16 +65,16 @@ const Statistics = () => { // YKV login and logout times per hour data const [logTimesData, setLogTimesData] = useState(null); - const [widthDivider, setWidthDivider] = useState(2.5); + const [, setWidthDivider] = useState(2.5); // YKV per weekday data const [logsPerWeekDayData, setLogsPerWeekDayData] = useState([]); // Keys by organization - const [orgMembersData, setOrgMembersData] = useState([]); + const [, setOrgMembersData] = useState([]); // Late YKV logouts by organization - const [orgLateData, setOrgLateData] = useState([]); + const [, setOrgLateData] = useState([]); // Data to be displayed in the pie chart and the selected pie chart option const [pieChartData, setPieChartData] = useState([]); @@ -311,8 +311,30 @@ const Statistics = () => { {t("timefilter")} - setMinFilter(e.target.value)} InputLabelProps={{ shrink: true }} fullWidth /> - setMaxFilter(e.target.value)} InputLabelProps={{ shrink: true }} fullWidth /> + setMinFilter(e.target.value)} + InputLabelProps={{ shrink: true }} + fullWidth + sx={{ + "& .MuiInputBase-input": { fontSize: "0.875rem" }, + "& .MuiInputLabel-root": { fontSize: "0.875rem" } + }} + /> + setMaxFilter(e.target.value)} + InputLabelProps={{ shrink: true }} + fullWidth + sx={{ + "& .MuiInputBase-input": { fontSize: "0.875rem" }, + "& .MuiInputLabel-root": { fontSize: "0.875rem" } + }} + /> diff --git a/frontend/vite.config.js b/frontend/vite.config.js index dc671c3f..72275ce1 100644 --- a/frontend/vite.config.js +++ b/frontend/vite.config.js @@ -6,6 +6,16 @@ export default defineConfig({ plugins: [react(), envCompatible()], server: { host: true, + port: 5173, + watch: { + usePolling: true, + }, + proxy: { + "/api": { + target: "http://api:8000", + changeOrigin: true, + }, + }, }, define: { "process.env.VITE_API_URL": JSON.stringify( From 49eb7f184bd832951c914c837577b285ce0ce5f2 Mon Sep 17 00:00:00 2001 From: iPegii <51372604+iPegii@users.noreply.github.com> Date: Sat, 7 Feb 2026 00:48:04 +0200 Subject: [PATCH 062/149] adding our testing domain to CORS --- backend/backend/settings.py | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/backend/backend/settings.py b/backend/backend/settings.py index e3d2e02b..035ead04 100644 --- a/backend/backend/settings.py +++ b/backend/backend/settings.py @@ -191,7 +191,8 @@ "https://klusteri-website-front-matlury-test.apps.ocp-test-0.k8s.it.helsinki.fi", "https://klusteri-website-frontend-test-matlury-test.apps.ocp-test-0.k8s.it.helsinki.fi", "https://klusteri-website-front-matlury-test.apps.ocp-prod-0.k8s.it.helsinki.fi", - "https://klusteri.ext.ocp-prod-0.k8s.it.helsinki.fi" + "https://klusteri.ext.ocp-prod-0.k8s.it.helsinki.fi", + "https://ilotalo-new-test-v2.matlu.fi/" ] # Logging configuration From 465760668602ec65e72362d9c7a271921934e1b4 Mon Sep 17 00:00:00 2001 From: iPegii <51372604+iPegii@users.noreply.github.com> Date: Sat, 7 Feb 2026 00:56:58 +0200 Subject: [PATCH 063/149] fix path for cors --- backend/backend/settings.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/backend/backend/settings.py b/backend/backend/settings.py index 035ead04..8be4c4c3 100644 --- a/backend/backend/settings.py +++ b/backend/backend/settings.py @@ -192,7 +192,7 @@ "https://klusteri-website-frontend-test-matlury-test.apps.ocp-test-0.k8s.it.helsinki.fi", "https://klusteri-website-front-matlury-test.apps.ocp-prod-0.k8s.it.helsinki.fi", "https://klusteri.ext.ocp-prod-0.k8s.it.helsinki.fi", - "https://ilotalo-new-test-v2.matlu.fi/" + "https://ilotalo-new-test-v2.matlu.fi" ] # Logging configuration From 853b5ca78eaa186aff36e0cb5f3a84876bd3fa76 Mon Sep 17 00:00:00 2001 From: iPegii <51372604+iPegii@users.noreply.github.com> Date: Sat, 7 Feb 2026 15:02:57 +0200 Subject: [PATCH 064/149] fix frontend build for prod --- frontend/Dockerfile | 4 +- frontend/src/components/ReservationsView.jsx | 46 ++++++++++---------- 2 files changed, 26 insertions(+), 24 deletions(-) diff --git a/frontend/Dockerfile b/frontend/Dockerfile index 576e0824..21ac6a66 100644 --- a/frontend/Dockerfile +++ b/frontend/Dockerfile @@ -28,7 +28,9 @@ COPY --from=build /frontend/dist /usr/share/nginx/html COPY nginx.conf /etc/nginx/nginx.conf # Ensure proper permissions for Nginx directories -RUN chmod g+rwx /var/cache/nginx /var/run /var/log/nginx +RUN mkdir -p /var/cache/nginx /var/run /var/log/nginx && \ + chmod -R g+rwx /var/cache/nginx /var/run /var/log/nginx && \ + chown -R nginx:nginx /var/cache/nginx /var/run /var/log/nginx # Expose the necessary port EXPOSE 5173 diff --git a/frontend/src/components/ReservationsView.jsx b/frontend/src/components/ReservationsView.jsx index 9c87c060..d227d2e3 100644 --- a/frontend/src/components/ReservationsView.jsx +++ b/frontend/src/components/ReservationsView.jsx @@ -135,20 +135,20 @@ const ReservationsView = ({ )}

{t("reservations_res")}

- {res_rights && + {res_rights &&
- -
+ + } {t("Kokoushuone")} {t("Kerhotila")} {t("Oleskelutila")} - ChristinaRegina + Christina Regina @@ -332,15 +332,15 @@ const ReservationsView = ({ )} - {(selectedEvent && (selectedEvent.created_by.username === username || admin)) && - + {(selectedEvent && (selectedEvent.created_by.username === username || admin)) && + }
- ), - }, - ]; - - const columns_buttonless = [ - { field: "tool", headerName: t("cleaningtool"), width: 400 }, - ]; - - if (loggedUser && loggedUser.role === 1) { - return ( - - ); - } else { - return ( - - ); - } - }; - - export default CleaningSuppliesList; +const CleaningSuppliesList = ({ allCleaningSupplies, handleDeleteClick }) => { + const { user: loggedUser } = useStateContext(); + const { t } = useTranslation(); + + const columns = [ + { field: "tool", headerName: t("cleaningtool"), width: 150 }, + { + field: "delete", + headerName: t("delete"), + width: 90, + renderCell: (params) => ( + + ), + }, + ]; + + const columns_buttonless = [ + { field: "tool", headerName: t("cleaningtool"), width: 400 }, + ]; + + if (loggedUser && loggedUser.role === 1) { + return ( + + ); + } else { + return ( + + ); + } +}; + +export default CleaningSuppliesList; diff --git a/frontend/src/components/DefectList.jsx b/frontend/src/components/DefectList.jsx index e6225e8e..93beff35 100644 --- a/frontend/src/components/DefectList.jsx +++ b/frontend/src/components/DefectList.jsx @@ -4,10 +4,12 @@ import { DataGrid } from "@mui/x-data-grid"; import { Button } from "@mui/material"; import CheckIcon from "@mui/icons-material/Check"; import { useTranslation } from "react-i18next"; +import { useStateContext } from "@context/ContextProvider"; -const DefectList = ({ loggedUser, allDefects, activeDefects, handleRepairClick, handleEmailClick }) => { +const DefectList = ({ allDefects, activeDefects, handleRepairClick, handleEmailClick }) => { const { t } = useTranslation(); - + const { user: loggedUser } = useStateContext(); + const columns = [ { field: "description", headerName: t("desc"), width: 400 }, { field: "time", headerName: t("time"), width: 200 }, diff --git a/frontend/src/tests/cleaningsuppliespage.test.js b/frontend/src/tests/cleaningsuppliespage.test.js index ee6968f5..9b90eced 100644 --- a/frontend/src/tests/cleaningsuppliespage.test.js +++ b/frontend/src/tests/cleaningsuppliespage.test.js @@ -3,13 +3,11 @@ import { fireEvent, waitFor, screen, - within, } from "@testing-library/react"; import "@testing-library/jest-dom"; import CleaningSupplies from "../../src/pages/cleaningsuppliespage.jsx"; import mockAxios from "../../__mocks__/axios"; -import i18n from "../i18n.js"; -import { ContextProvider } from "../../src/context/ContextProvider"; +import { ContextProvider } from "@context/ContextProvider"; localStorage.setItem("lang", "fi"); diff --git a/frontend/src/tests/defectfaultpage.test.js b/frontend/src/tests/defectfaultpage.test.js index 22ef454f..e047fd2b 100644 --- a/frontend/src/tests/defectfaultpage.test.js +++ b/frontend/src/tests/defectfaultpage.test.js @@ -7,8 +7,7 @@ import { import "@testing-library/jest-dom"; import DefectFault from "../../src/pages/defectfaultpage"; import mockAxios from "../../__mocks__/axios"; -import i18n from "../i18n.js"; -import { ContextProvider } from "../../src/context/ContextProvider"; +import { ContextProvider } from "@context/ContextProvider"; localStorage.setItem("lang", "fi") @@ -45,7 +44,7 @@ describe("DefectFault Component", () => { render( - + ); @@ -76,7 +75,7 @@ describe("DefectFault Component", () => { await waitFor(() => { // Mock the axios post request mockAxios.mockResponseFor({ url: "defects/create_defect" }, responseObj); - + expect(mockAxios.post).toHaveBeenCalledWith( "defects/create_defect", { diff --git a/frontend/src/tests/setupTests.js b/frontend/src/tests/setupTests.js new file mode 100644 index 00000000..8f284b80 --- /dev/null +++ b/frontend/src/tests/setupTests.js @@ -0,0 +1,5 @@ +import '../i18n'; +import '@testing-library/jest-dom'; + +// Mock scrollTo since it's not implemented in JSDOM +window.scrollTo = jest.fn(); diff --git a/frontend/vite.config.js b/frontend/vite.config.js index acb99322..273a1394 100644 --- a/frontend/vite.config.js +++ b/frontend/vite.config.js @@ -1,9 +1,16 @@ import { defineConfig } from "vite"; import react from "@vitejs/plugin-react"; import envCompatible from 'vite-plugin-env-compatible'; +import path from 'path'; + // https://vitejs.dev/config/ export default defineConfig({ plugins: [react(), envCompatible()], + resolve: { + alias: { + '@context': path.resolve(__dirname, './src/context'), + }, + }, server: { host: true, port: 5173, From f521e591c568a37c54dda344f18658c4076ab189 Mon Sep 17 00:00:00 2001 From: iPegii <51372604+iPegii@users.noreply.github.com> Date: Sun, 8 Feb 2026 01:35:05 +0200 Subject: [PATCH 087/149] use relative route for context in all places --- frontend/src/App.jsx | 2 +- frontend/src/components/YkvLogoutFunction.jsx | 2 +- frontend/src/main.jsx | 2 +- frontend/src/pages/cleaningschedulepage.jsx | 2 +- frontend/src/pages/cleaningsuppliespage.jsx | 2 +- frontend/src/pages/defectfaultpage.jsx | 2 +- frontend/src/pages/loginpage.jsx | 2 +- frontend/src/pages/ownkeys.jsx | 2 +- frontend/src/pages/ownpage.jsx | 2 +- frontend/src/pages/reservations.jsx | 2 +- frontend/src/tests/cleaning.test.js | 2 +- frontend/src/tests/loginpage.test.js | 2 +- frontend/src/tests/ownkeys.test.js | 2 +- frontend/src/tests/ownpage.test.js | 2 +- frontend/src/tests/reservations.test.js | 22 +++++++++---------- 15 files changed, 24 insertions(+), 26 deletions(-) diff --git a/frontend/src/App.jsx b/frontend/src/App.jsx index 0e4f1388..6823ba6e 100644 --- a/frontend/src/App.jsx +++ b/frontend/src/App.jsx @@ -1,5 +1,5 @@ import * as React from "react"; -import { useStateContext } from "./context/ContextProvider.jsx"; +import { useStateContext } from "@context/ContextProvider"; import { BrowserRouter as Router, Route, diff --git a/frontend/src/components/YkvLogoutFunction.jsx b/frontend/src/components/YkvLogoutFunction.jsx index 43e32bee..80275064 100644 --- a/frontend/src/components/YkvLogoutFunction.jsx +++ b/frontend/src/components/YkvLogoutFunction.jsx @@ -15,7 +15,7 @@ import AccessTimeIcon from "@mui/icons-material/AccessTime"; import { lighten, styled } from "@mui/material/styles"; import CheckIcon from "@mui/icons-material/Check"; import { useTranslation } from "react-i18next"; -import { useStateContext } from "../context/ContextProvider"; +import { useStateContext } from "@context/ContextProvider"; const YkvLogoutFunction = ({ handleYkvLogin, handleYkvLogout, diff --git a/frontend/src/main.jsx b/frontend/src/main.jsx index ca18dcac..ac2c432c 100644 --- a/frontend/src/main.jsx +++ b/frontend/src/main.jsx @@ -1,6 +1,6 @@ import React from "react"; import { createRoot } from "react-dom/client"; -import { ContextProvider } from "./context/ContextProvider"; +import { ContextProvider } from "@context/ContextProvider"; import App from "./App"; // Use createRoot instead of ReactDOM.render diff --git a/frontend/src/pages/cleaningschedulepage.jsx b/frontend/src/pages/cleaningschedulepage.jsx index aa2f3c98..e442c409 100644 --- a/frontend/src/pages/cleaningschedulepage.jsx +++ b/frontend/src/pages/cleaningschedulepage.jsx @@ -1,5 +1,5 @@ import React, { useState, useEffect } from "react"; -import { useStateContext } from "../context/ContextProvider.jsx"; +import { useStateContext } from "@context/ContextProvider"; import { organizationsAPI, cleaningAPI } from "../api/api.ts"; import { Button, Snackbar, Alert } from "@mui/material"; import CleanersList from "../components/CleanersList.jsx"; diff --git a/frontend/src/pages/cleaningsuppliespage.jsx b/frontend/src/pages/cleaningsuppliespage.jsx index 9491544e..452e8a0d 100644 --- a/frontend/src/pages/cleaningsuppliespage.jsx +++ b/frontend/src/pages/cleaningsuppliespage.jsx @@ -1,5 +1,5 @@ import React, { useState, useEffect } from "react"; -import { useStateContext } from "../context/ContextProvider.jsx"; +import { useStateContext } from "@context/ContextProvider"; import { cleaningSuppliesAPI } from "../api/api.ts"; import { Button } from "@mui/material"; import CleaningToolForm from "../components/CleaningToolForm.jsx"; diff --git a/frontend/src/pages/defectfaultpage.jsx b/frontend/src/pages/defectfaultpage.jsx index 9fcf19cd..ad6f6731 100644 --- a/frontend/src/pages/defectfaultpage.jsx +++ b/frontend/src/pages/defectfaultpage.jsx @@ -6,7 +6,7 @@ import DefectList from "../components/DefectList"; import RepairConfirmDialog from "../components/RepairConfirmDialog.jsx"; import EmailConfirmDialog from "../components/EmailConfirmDialog.jsx"; import { useTranslation } from "react-i18next"; -import { useStateContext } from "../context/ContextProvider"; +import { useStateContext } from "@context/ContextProvider"; const DefectFault = () => { const { user } = useStateContext(); diff --git a/frontend/src/pages/loginpage.jsx b/frontend/src/pages/loginpage.jsx index a06d3f8a..c44e4817 100644 --- a/frontend/src/pages/loginpage.jsx +++ b/frontend/src/pages/loginpage.jsx @@ -1,6 +1,6 @@ import React, { useState, useEffect } from "react"; import NewAccountPage from "./createpage"; -import { useStateContext } from "../context/ContextProvider.jsx"; +import { useStateContext } from "@context/ContextProvider"; import login from "../utils/login.js"; import LoginForm from "../components/LoginForm.jsx"; import { useTranslation } from "react-i18next"; diff --git a/frontend/src/pages/ownkeys.jsx b/frontend/src/pages/ownkeys.jsx index 3c93ccc1..779787ce 100644 --- a/frontend/src/pages/ownkeys.jsx +++ b/frontend/src/pages/ownkeys.jsx @@ -1,5 +1,5 @@ import { useState, useEffect } from "react"; -import { useStateContext } from "../context/ContextProvider.jsx"; +import { useStateContext } from "@context/ContextProvider"; import { usersAPI, nightResponsibilitiesAPI, ykvAPI } from "../api/api.ts"; import { getCurrentDateTime } from "../utils/timehelpers.js"; import { diff --git a/frontend/src/pages/ownpage.jsx b/frontend/src/pages/ownpage.jsx index 0fc3f9e8..6debf30b 100644 --- a/frontend/src/pages/ownpage.jsx +++ b/frontend/src/pages/ownpage.jsx @@ -1,5 +1,5 @@ import React, { useState, useEffect } from "react"; -import { useStateContext } from "../context/ContextProvider"; +import { useStateContext } from "@context/ContextProvider"; import { usersAPI, organizationsAPI, keysAPI, authAPI } from "../api/api.ts"; import UserPage from "../components/UserPage.jsx"; import OrganisationPage from "../components/OrganisationPage.jsx"; diff --git a/frontend/src/pages/reservations.jsx b/frontend/src/pages/reservations.jsx index c2a70730..f4d7bf98 100644 --- a/frontend/src/pages/reservations.jsx +++ b/frontend/src/pages/reservations.jsx @@ -2,7 +2,7 @@ import React, { useState, useEffect, useRef } from "react"; import { momentLocalizer } from "react-big-calendar"; import moment from "moment"; import "moment/locale/fi"; -import { useStateContext } from "../context/ContextProvider.jsx"; +import { useStateContext } from "@context/ContextProvider"; import { organizationsAPI, eventsAPI } from "../api/api.ts"; import ReservationsView from "../components/ReservationsView.jsx"; import { useTranslation } from "react-i18next"; diff --git a/frontend/src/tests/cleaning.test.js b/frontend/src/tests/cleaning.test.js index b69e14d6..dc349b52 100644 --- a/frontend/src/tests/cleaning.test.js +++ b/frontend/src/tests/cleaning.test.js @@ -4,7 +4,7 @@ import CleaningSchedule from '../pages/cleaningschedulepage.jsx'; import CleanersList from '../components/CleanersList.jsx'; import axiosClient from '../axios.js'; import mockAxios from "../../__mocks__/axios"; -import { ContextProvider } from "../../src/context/ContextProvider"; +import { ContextProvider } from "@context/ContextProvider"; import "@testing-library/jest-dom"; localStorage.setItem("lang", "fi") diff --git a/frontend/src/tests/loginpage.test.js b/frontend/src/tests/loginpage.test.js index 79e7067d..854b7171 100644 --- a/frontend/src/tests/loginpage.test.js +++ b/frontend/src/tests/loginpage.test.js @@ -1,7 +1,7 @@ import "@testing-library/jest-dom"; import { render, fireEvent, waitFor } from "@testing-library/react"; import LoginPage from "../pages/loginpage"; -import { ContextProvider } from "../context/ContextProvider"; +import { ContextProvider } from "@context/ContextProvider"; import axiosClient from "../axios.js"; import i18n from "../i18n.js"; diff --git a/frontend/src/tests/ownkeys.test.js b/frontend/src/tests/ownkeys.test.js index f5e34761..364fbcb3 100644 --- a/frontend/src/tests/ownkeys.test.js +++ b/frontend/src/tests/ownkeys.test.js @@ -6,7 +6,7 @@ import { } from "@testing-library/react"; import "@testing-library/jest-dom"; import OwnKeys from "../../src/pages/ownkeys"; -import { ContextProvider } from "../../src/context/ContextProvider"; +import { ContextProvider } from "@context/ContextProvider"; import mockAxios from "../../__mocks__/axios"; localStorage.setItem("lang", "fi") diff --git a/frontend/src/tests/ownpage.test.js b/frontend/src/tests/ownpage.test.js index 18d61443..9ffccf24 100644 --- a/frontend/src/tests/ownpage.test.js +++ b/frontend/src/tests/ownpage.test.js @@ -8,7 +8,7 @@ import { } from "@testing-library/react"; import OwnPage from "../pages/ownpage"; import mockAxios from "../../__mocks__/axios"; -import { ContextProvider } from "../../src/context/ContextProvider"; +import { ContextProvider } from "@context/ContextProvider"; localStorage.setItem("lang", "fi"); diff --git a/frontend/src/tests/reservations.test.js b/frontend/src/tests/reservations.test.js index dfac7bac..8e2930c5 100644 --- a/frontend/src/tests/reservations.test.js +++ b/frontend/src/tests/reservations.test.js @@ -3,15 +3,12 @@ import { fireEvent, waitFor, screen, -} from "@testing-library/react";import "@testing-library/jest-dom"; +} from "@testing-library/react"; import "@testing-library/jest-dom"; import Reservations from "../../src/pages/reservations"; import mockAxios from "../../__mocks__/axios.js"; -import i18n from "../i18n.js"; -import { ContextProvider } from "../../src/context/ContextProvider"; +import { ContextProvider } from "@context/ContextProvider"; localStorage.setItem("lang", "fi") -import { momentLocalizer } from "react-big-calendar"; -import moment from "moment"; afterEach(() => { // cleaning up the mess left behind the previous test @@ -58,7 +55,7 @@ describe("Reservations component", () => { fireEvent.change(startTimeField, { target: { value: "2024-06-11T10:00" } }); fireEvent.change(endTimeField, { target: { value: "2024-06-11T12:00" } }); - + expect(startTimeField.value).toBe("2024-06-11T10:00"); expect(endTimeField.value).toBe("2024-06-11T12:00"); @@ -82,8 +79,9 @@ describe("Reservations component", () => { ); - const response = {data: [ - { + const response = { + data: [ + { "id": 1, "start": "2024-06-03T07:26:24.237284Z", "end": "2024-06-03T07:26:24.237298Z", @@ -93,8 +91,8 @@ describe("Reservations component", () => { "responsible": "Vastuuhenkilö", "open": true, "room": "Kokoushuone" - }, - { + }, + { "id": 2, "start": "2024-06-03T07:30:22.141739Z", "end": "2024-06-03T07:30:22.141755Z", @@ -104,7 +102,7 @@ describe("Reservations component", () => { "responsible": "Vastuuhenkilö", "open": false, "room": "Kerhotila" - }] + }] } await waitFor(() => { @@ -115,7 +113,7 @@ describe("Reservations component", () => { const reservationButton = getByText("Lataa tapahtumat CSV-muodossa"); fireEvent.click(reservationButton); }) - }) + }) // it('booking with role 1', async () => { // const { getByText, getByPlaceholderText, queryByText } = render() From 11ce6a6c2523153c9567c2666a44f48f48924782 Mon Sep 17 00:00:00 2001 From: iPegii <51372604+iPegii@users.noreply.github.com> Date: Sun, 8 Feb 2026 01:51:02 +0200 Subject: [PATCH 088/149] use 'act' in tests where React state is activated to remove warnings --- frontend/src/tests/cleaning.test.js | 37 ++++--- .../src/tests/cleaningsuppliespage.test.js | 9 +- frontend/src/tests/createpage.test.js | 17 +-- frontend/src/tests/defectfaultpage.test.js | 5 +- frontend/src/tests/ownkeys.test.js | 45 ++++++-- frontend/src/tests/ownpage.test.js | 101 ++++++++++++------ frontend/src/tests/reservations.test.js | 8 +- 7 files changed, 156 insertions(+), 66 deletions(-) diff --git a/frontend/src/tests/cleaning.test.js b/frontend/src/tests/cleaning.test.js index dc349b52..36f2091a 100644 --- a/frontend/src/tests/cleaning.test.js +++ b/frontend/src/tests/cleaning.test.js @@ -1,8 +1,7 @@ -import React from 'react'; +import React, { act } from 'react'; import { render, screen, waitFor } from '@testing-library/react'; import CleaningSchedule from '../pages/cleaningschedulepage.jsx'; import CleanersList from '../components/CleanersList.jsx'; -import axiosClient from '../axios.js'; import mockAxios from "../../__mocks__/axios"; import { ContextProvider } from "@context/ContextProvider"; import "@testing-library/jest-dom"; @@ -20,13 +19,11 @@ const user = { const mockCleaningData = [ { - id: 1, week: 1, big: { name: 'Matrix' }, small: { name: 'Vasara' }, }, { - id: 2, week: 2, big: { name: 'TKO-äly' }, small: { name: 'Synop' }, @@ -36,11 +33,11 @@ const mockCleaningData = [ describe('CleaningSchedule Component', () => { beforeEach(() => { mockAxios.reset(); - localStorage.setItem("loggedUser", JSON.stringify(user)); + localStorage.clear(); + localStorage.setItem("lang", "fi") }); test('renders login prompt if not logged in', () => { - localStorage.removeItem("loggedUser"); render( @@ -50,16 +47,25 @@ describe('CleaningSchedule Component', () => { }); test('fetches and displays cleaning schedule when logged in', async () => { - axiosClient.get.mockResolvedValueOnce({ data: mockCleaningData }); + // CleanersList expects processed data (with id and string names) + const processedData = mockCleaningData.map(item => ({ + id: item.week, + week: item.week, + big: item.big.name, + small: item.small.name, + date: "2024-01-01" // date is handled by moment in real component + })); - render(); + render( + + + + ); - await waitFor(() => { - expect(screen.findByText('Matrix')).resolves.toBeInTheDocument(); - }); + expect(await screen.findByText('Matrix')).toBeInTheDocument(); }); - test('renders all content when logged as leppispj', () => { + test('renders all content when logged as leppispj', async () => { window.confirm = jest.fn(() => true); localStorage.setItem("ACCESS_TOKEN", "example_token"); localStorage.setItem("loggedUser", JSON.stringify(user)); @@ -69,6 +75,13 @@ describe('CleaningSchedule Component', () => { ); + + await waitFor(() => expect(mockAxios.get).toHaveBeenCalledWith("listobjects/cleaning/")); + act(() => { + // Here we provide the raw API format + mockAxios.mockResponse({ data: mockCleaningData }); + }); + expect(screen.getByText('Siivousvuorot')).toBeInTheDocument(); expect(screen.getByText('Tuo lista')).toBeInTheDocument(); expect(screen.getByText('Vie lista')).toBeInTheDocument(); diff --git a/frontend/src/tests/cleaningsuppliespage.test.js b/frontend/src/tests/cleaningsuppliespage.test.js index 9b90eced..5e4a5a96 100644 --- a/frontend/src/tests/cleaningsuppliespage.test.js +++ b/frontend/src/tests/cleaningsuppliespage.test.js @@ -4,6 +4,7 @@ import { waitFor, screen, } from "@testing-library/react"; +import { act } from "react"; import "@testing-library/jest-dom"; import CleaningSupplies from "../../src/pages/cleaningsuppliespage.jsx"; import mockAxios from "../../__mocks__/axios"; @@ -71,7 +72,9 @@ describe("Cleaningsupplies Component", () => { // Wait for the axios requests to complete await waitFor(() => { // Mock the axios post request - mockAxios.mockResponseFor({ url: "cleaningsupplies/create_tool" }, responseObj); + act(() => { + mockAxios.mockResponseFor({ url: "cleaningsupplies/create_tool" }, responseObj); + }); expect(mockAxios.post).toHaveBeenCalledWith( "cleaningsupplies/create_tool", @@ -121,7 +124,9 @@ describe("Cleaningsupplies Component", () => { await waitFor(() => { // Mock the axios get request - mockAxios.mockResponseFor({ url: "listobjects/cleaningsupplies/" }, responseObj); + act(() => { + mockAxios.mockResponseFor({ url: "listobjects/cleaningsupplies/" }, responseObj); + }); }) await waitFor(() => { diff --git a/frontend/src/tests/createpage.test.js b/frontend/src/tests/createpage.test.js index a82fa60e..dd3ff845 100644 --- a/frontend/src/tests/createpage.test.js +++ b/frontend/src/tests/createpage.test.js @@ -1,4 +1,5 @@ import { render, fireEvent, waitFor } from "@testing-library/react"; +import { act } from "react"; import NewAccountPage from "../../src/pages/createpage"; import mockAxios from "../../__mocks__/axios"; import "@testing-library/jest-dom"; @@ -193,7 +194,9 @@ describe("Createpage", () => { })); }); - mockAxios.mockResponse({ data: { message: "Success" } }); + act(() => { + mockAxios.mockResponse({ data: { message: "Success" } }); + }); await waitFor(() => { expect(getByText("Käyttäjä luotu onnistuneesti")).toBeInTheDocument(); @@ -221,11 +224,13 @@ describe("Createpage", () => { })); }); - mockAxios.mockError({ - response: { - status: 400, - data: { email: ["Sähköposti on jo käytössä."] } - } + act(() => { + mockAxios.mockError({ + response: { + status: 400, + data: { email: ["Sähköposti on jo käytössä."] } + } + }); }); await waitFor(() => { diff --git a/frontend/src/tests/defectfaultpage.test.js b/frontend/src/tests/defectfaultpage.test.js index e047fd2b..ccf468d2 100644 --- a/frontend/src/tests/defectfaultpage.test.js +++ b/frontend/src/tests/defectfaultpage.test.js @@ -4,6 +4,7 @@ import { waitFor, screen, } from "@testing-library/react"; +import { act } from "react"; import "@testing-library/jest-dom"; import DefectFault from "../../src/pages/defectfaultpage"; import mockAxios from "../../__mocks__/axios"; @@ -74,7 +75,9 @@ describe("DefectFault Component", () => { // Wait for the axios requests to complete await waitFor(() => { // Mock the axios post request - mockAxios.mockResponseFor({ url: "defects/create_defect" }, responseObj); + act(() => { + mockAxios.mockResponseFor({ url: "defects/create_defect" }, responseObj); + }); expect(mockAxios.post).toHaveBeenCalledWith( "defects/create_defect", diff --git a/frontend/src/tests/ownkeys.test.js b/frontend/src/tests/ownkeys.test.js index 364fbcb3..0a36d6da 100644 --- a/frontend/src/tests/ownkeys.test.js +++ b/frontend/src/tests/ownkeys.test.js @@ -4,6 +4,7 @@ import { waitFor, within, } from "@testing-library/react"; +import { act } from "react"; import "@testing-library/jest-dom"; import OwnKeys from "../../src/pages/ownkeys"; import { ContextProvider } from "@context/ContextProvider"; @@ -48,12 +49,18 @@ describe("OwnKeys Component", () => { // mock permission and responsibilities responses (respond after requests issued) await waitFor(() => expect(mockAxios.get).toHaveBeenCalled()); - mockAxios.mockResponseFor({ url: "users/userinfo" }, { data: user }); + act(() => { + mockAxios.mockResponseFor({ url: "users/userinfo" }, { data: user }); + }); await waitFor(() => expect(mockAxios.get).toHaveBeenCalled()); // respond to eligible users request (ykv) which happens after permission is set - mockAxios.mockResponseFor({ url: "users/ykv/" }, { data: [] }); + act(() => { + mockAxios.mockResponseFor({ url: "users/ykv/" }, { data: [] }); + }); await waitFor(() => expect(mockAxios.get).toHaveBeenCalled()); - mockAxios.mockResponseFor({ url: "listobjects/nightresponsibilities/" }, { data: [] }); + act(() => { + mockAxios.mockResponseFor({ url: "listobjects/nightresponsibilities/" }, { data: [] }); + }); // open the create dialog and assert fields inside const createBtn = getByTestId("opencreateform"); @@ -87,11 +94,17 @@ describe("OwnKeys Component", () => { ); // respond to initial requests before interacting with the UI await waitFor(() => expect(mockAxios.get).toHaveBeenCalled()); - mockAxios.mockResponseFor({ url: "users/userinfo" }, { data: user }); + act(() => { + mockAxios.mockResponseFor({ url: "users/userinfo" }, { data: user }); + }); await waitFor(() => expect(mockAxios.get).toHaveBeenCalled()); - mockAxios.mockResponseFor({ url: "users/ykv/" }, { data: [] }); + act(() => { + mockAxios.mockResponseFor({ url: "users/ykv/" }, { data: [] }); + }); await waitFor(() => expect(mockAxios.get).toHaveBeenCalled()); - mockAxios.mockResponseFor({ url: "listobjects/nightresponsibilities/" }, { data: [] }); + act(() => { + mockAxios.mockResponseFor({ url: "listobjects/nightresponsibilities/" }, { data: [] }); + }); const create_form = getByTestId("opencreateform"); fireEvent.click(create_form); const resp_field_input = await findByRole("textbox", { name: "Kenestä otat vastuun?" }); @@ -100,9 +113,13 @@ describe("OwnKeys Component", () => { fireEvent.click(respButton); await waitFor(() => expect(mockAxios.post).toHaveBeenCalled()); - mockAxios.mockResponseFor({ url: "ykv/create_responsibility" }, { data: {} }); + act(() => { + mockAxios.mockResponseFor({ url: "ykv/create_responsibility" }, { data: {} }); + }); await waitFor(() => expect(mockAxios.get).toHaveBeenCalledWith("listobjects/nightresponsibilities/")); - mockAxios.mockResponseFor({ url: "listobjects/nightresponsibilities/" }, { data: [] }); + act(() => { + mockAxios.mockResponseFor({ url: "listobjects/nightresponsibilities/" }, { data: [] }); + }); await waitFor(() => { const snackbar = getByTestId("snackbar"); expect(snackbar).toBeInTheDocument(); @@ -295,11 +312,17 @@ describe("OwnKeys Component", () => { ], }; await waitFor(() => expect(mockAxios.get).toHaveBeenCalled()); - mockAxios.mockResponseFor({ url: "users/userinfo" }, responsedata); + act(() => { + mockAxios.mockResponseFor({ url: "users/userinfo" }, responsedata); + }); await waitFor(() => expect(mockAxios.get).toHaveBeenCalled()); - mockAxios.mockResponseFor({ url: "users/ykv/" }, { data: [] }); + act(() => { + mockAxios.mockResponseFor({ url: "users/ykv/" }, { data: [] }); + }); await waitFor(() => expect(mockAxios.get).toHaveBeenCalled()); - mockAxios.mockResponseFor({ url: "listobjects/nightresponsibilities/" }, response); + act(() => { + mockAxios.mockResponseFor({ url: "listobjects/nightresponsibilities/" }, response); + }); await waitFor(() => { expect(mockAxios.get).toHaveBeenCalledWith("users/userinfo"); diff --git a/frontend/src/tests/ownpage.test.js b/frontend/src/tests/ownpage.test.js index 9ffccf24..71e88fff 100644 --- a/frontend/src/tests/ownpage.test.js +++ b/frontend/src/tests/ownpage.test.js @@ -6,6 +6,7 @@ import { within, screen, } from "@testing-library/react"; +import { act } from "react"; import OwnPage from "../pages/ownpage"; import mockAxios from "../../__mocks__/axios"; import { ContextProvider } from "@context/ContextProvider"; @@ -51,13 +52,19 @@ it("opens with role 5", async () => { // Mock initial requests await waitFor(() => { - mockAxios.mockResponseFor({ url: "users/userinfo" }, { data: user }); + act(() => { + mockAxios.mockResponseFor({ url: "users/userinfo" }, { data: user }); + }); }); await waitFor(() => { - mockAxios.mockResponseFor({ url: "listobjects/organizations/?include_user_count=true" }, { data: [] }); + act(() => { + mockAxios.mockResponseFor({ url: "listobjects/organizations/?include_user_count=true" }, { data: [] }); + }); }); await waitFor(() => { - mockAxios.mockResponseFor({ url: "listobjects/users/" }, { data: [] }); + act(() => { + mockAxios.mockResponseFor({ url: "listobjects/users/" }, { data: [] }); + }); }); expect(getByLabelText("Käyttäjänimi")).toBeInTheDocument(); @@ -156,13 +163,19 @@ it("User updating works", async () => { // Mock initial requests await waitFor(() => { - mockAxios.mockResponseFor({ url: "users/userinfo" }, { data: user }); + act(() => { + mockAxios.mockResponseFor({ url: "users/userinfo" }, { data: user }); + }); }); await waitFor(() => { - mockAxios.mockResponseFor({ url: "listobjects/organizations/?include_user_count=true" }, { data: [] }); + act(() => { + mockAxios.mockResponseFor({ url: "listobjects/organizations/?include_user_count=true" }, { data: [] }); + }); }); await waitFor(() => { - mockAxios.mockResponseFor({ url: "listobjects/users/" }, { data: [] }); + act(() => { + mockAxios.mockResponseFor({ url: "listobjects/users/" }, { data: [] }); + }); }); const username_field = getByLabelText("Käyttäjänimi"); @@ -215,7 +228,9 @@ it("User updating works", async () => { fireEvent.click(saveButton); await waitFor(() => expect(mockAxios.put).toHaveBeenCalled()); - mockAxios.mockResponse(resp_updated); + act(() => { + mockAxios.mockResponse(resp_updated); + }); await waitFor(() => { expect(mockAxios.put).toHaveBeenCalledWith("users/update/1/", { @@ -256,13 +271,19 @@ describe("User updating errors", () => { // Mock initial requests await waitFor(() => { - mockAxios.mockResponseFor({ url: "users/userinfo" }, { data: user }); + act(() => { + mockAxios.mockResponseFor({ url: "users/userinfo" }, { data: user }); + }); }); await waitFor(() => { - mockAxios.mockResponseFor({ url: "listobjects/organizations/?include_user_count=true" }, { data: [] }); + act(() => { + mockAxios.mockResponseFor({ url: "listobjects/organizations/?include_user_count=true" }, { data: [] }); + }); }); await waitFor(() => { - mockAxios.mockResponseFor({ url: "listobjects/users/" }, { data: [] }); + act(() => { + mockAxios.mockResponseFor({ url: "listobjects/users/" }, { data: [] }); + }); }); const username_field = getByLabelText("Käyttäjänimi"); @@ -303,13 +324,19 @@ describe("User updating errors", () => { // Mock initial requests await waitFor(() => { - mockAxios.mockResponseFor({ url: "users/userinfo" }, { data: user }); + act(() => { + mockAxios.mockResponseFor({ url: "users/userinfo" }, { data: user }); + }); }); await waitFor(() => { - mockAxios.mockResponseFor({ url: "listobjects/organizations/?include_user_count=true" }, { data: [] }); + act(() => { + mockAxios.mockResponseFor({ url: "listobjects/organizations/?include_user_count=true" }, { data: [] }); + }); }); await waitFor(() => { - mockAxios.mockResponseFor({ url: "listobjects/users/" }, { data: [] }); + act(() => { + mockAxios.mockResponseFor({ url: "listobjects/users/" }, { data: [] }); + }); }); const telegram = getByLabelText("Telegram"); @@ -356,13 +383,19 @@ describe("Organizations", () => { // Mock 3 initial requests for role 1 (register mocks sequentially) await waitFor(() => { - mockAxios.mockResponseFor({ url: "listobjects/organizations/?include_user_count=true" }, { data: [] }); + act(() => { + mockAxios.mockResponseFor({ url: "listobjects/organizations/?include_user_count=true" }, { data: [] }); + }); }); await waitFor(() => { - mockAxios.mockResponseFor({ url: "listobjects/users/" }, { data: [] }); + act(() => { + mockAxios.mockResponseFor({ url: "listobjects/users/" }, { data: [] }); + }); }); await waitFor(() => { - mockAxios.mockResponseFor({ url: "users/userinfo" }, { data: user }); + act(() => { + mockAxios.mockResponseFor({ url: "users/userinfo" }, { data: user }); + }); }); const resp = { @@ -405,26 +438,30 @@ describe("Organizations", () => { const submit = modal.getByText("Luo järjestö"); fireEvent.click(submit); - mockAxios.mockResponseFor( - { url: "listobjects/organizations/?email=tko@aly.com" }, - { - data: [ - { - id: 1, - user_set: [], - name: "matrix", - email: "mat@rix.com", - homepage: "matrix.org", - color: "", - }, - ], - }, - ); + act(() => { + mockAxios.mockResponseFor( + { url: "listobjects/organizations/?email=tko@aly.com" }, + { + data: [ + { + id: 1, + user_set: [], + name: "matrix", + email: "mat@rix.com", + homepage: "matrix.org", + color: "", + }, + ], + }, + ); + }); expect(mockAxios.get).toHaveBeenCalledWith( "listobjects/organizations/?email=tko@aly.com", ); - mockAxios.mockResponseFor({ url: "organizations/create" }, resp); + act(() => { + mockAxios.mockResponseFor({ url: "organizations/create" }, resp); + }); expect(mockAxios.post).toHaveBeenCalledWith("organizations/create", { color: "", diff --git a/frontend/src/tests/reservations.test.js b/frontend/src/tests/reservations.test.js index 8e2930c5..0b7d7529 100644 --- a/frontend/src/tests/reservations.test.js +++ b/frontend/src/tests/reservations.test.js @@ -3,7 +3,9 @@ import { fireEvent, waitFor, screen, -} from "@testing-library/react"; import "@testing-library/jest-dom"; +} from "@testing-library/react"; +import { act } from "react"; + import "@testing-library/jest-dom"; import Reservations from "../../src/pages/reservations"; import mockAxios from "../../__mocks__/axios.js"; import { ContextProvider } from "@context/ContextProvider"; @@ -106,7 +108,9 @@ describe("Reservations component", () => { } await waitFor(() => { - mockAxios.mockResponse(response); + act(() => { + mockAxios.mockResponse(response); + }); }) await waitFor(() => { From 765f697d3b33bd96bc7762df21133170ff7ed04d Mon Sep 17 00:00:00 2001 From: iPegii <51372604+iPegii@users.noreply.github.com> Date: Sun, 8 Feb 2026 01:51:38 +0200 Subject: [PATCH 089/149] disable cypress tests from CI/CD for now --- .github/workflows/main.yml | 41 ++++++++++++++++++++++---------------- 1 file changed, 24 insertions(+), 17 deletions(-) diff --git a/.github/workflows/main.yml b/.github/workflows/main.yml index 595c88ec..8ae7afa4 100644 --- a/.github/workflows/main.yml +++ b/.github/workflows/main.yml @@ -65,31 +65,38 @@ jobs: poetry run coverage xml -o coverage.xml - name: Run frontend tests + if: success() || failure() working-directory: ./frontend run: npm run test - - name: Create frontend coverage report - working-directory: ./frontend - run: npx jest --coverage + # Cypress tests are disabled for now + # - name: Start backend + # run: | + # cd backend + # poetry run python manage.py runserver & - #Cypress tests require backend to be running - - name: Start backend - run: | - cd backend - poetry run python manage.py runserver & - - - name: Run Cypress tests - uses: cypress-io/github-action@v3 - with: - working-directory: ./frontend - project: ./ - browser: chrome - build: npm run build - start: npm run dev + # - name: Run Cypress tests + # uses: cypress-io/github-action@v3 + # with: + # working-directory: ./frontend + # project: ./ + # browser: chrome + # build: npm run build + # start: npm run dev - name: Upload coverage reports to Codecov + if: always() uses: codecov/codecov-action@v3 with: files: ./backend/coverage.xml,./frontend/coverage/clover.xml env: CODECOV_TOKEN: ${{ secrets.CODECOV_TOKEN }} + + - name: Upload Test Coverage Artifacts + if: always() + uses: actions/upload-artifact@v4 + with: + name: coverage-reports + path: | + backend/coverage.xml + frontend/coverage/ From f747057f85ecb5ce34730840641ba3d472ff79d0 Mon Sep 17 00:00:00 2001 From: iPegii <51372604+iPegii@users.noreply.github.com> Date: Sun, 8 Feb 2026 02:04:44 +0200 Subject: [PATCH 090/149] remove Express as unused --- frontend/package-lock.json | 702 ++----------------------------------- frontend/package.json | 3 - frontend/server.js | 57 --- 3 files changed, 23 insertions(+), 739 deletions(-) delete mode 100644 frontend/server.js diff --git a/frontend/package-lock.json b/frontend/package-lock.json index 5517bb3c..d717eea3 100644 --- a/frontend/package-lock.json +++ b/frontend/package-lock.json @@ -16,8 +16,6 @@ "@mui/x-data-grid": "^6.20.0", "axios": "^1.6.7", "eslint-plugin-jest": "^27.8.0", - "express": "^5.2.1", - "http-proxy-middleware": "^3.0.5", "moment": "^2.30.1", "react": "^18.2.0", "react-big-calendar": "^1.11.2", @@ -4500,15 +4498,6 @@ "@types/node": "*" } }, - "node_modules/@types/http-proxy": { - "version": "1.17.17", - "resolved": "https://registry.npmjs.org/@types/http-proxy/-/http-proxy-1.17.17.tgz", - "integrity": "sha512-ED6LB+Z1AVylNTu7hdzuBqOgMnvG/ld6wGCG8wFnAzKX5uyW2K3WD52v0gnLCTK/VLpXtKckgWuyScYK6cSPaw==", - "license": "MIT", - "dependencies": { - "@types/node": "*" - } - }, "node_modules/@types/istanbul-lib-coverage": { "version": "2.0.6", "resolved": "https://registry.npmjs.org/@types/istanbul-lib-coverage/-/istanbul-lib-coverage-2.0.6.tgz", @@ -4553,6 +4542,7 @@ "version": "20.14.1", "resolved": "https://registry.npmjs.org/@types/node/-/node-20.14.1.tgz", "integrity": "sha512-T2MzSGEu+ysB/FkWfqmhV3PLyQlowdptmmgD20C6QxsS8Fmv5SjpZ1ayXaEC0S21/h5UJ9iA6W/5vSNU5l00OA==", + "devOptional": true, "dependencies": { "undici-types": "~5.26.4" } @@ -4821,44 +4811,6 @@ "deprecated": "Use your platform's native atob() and btoa() methods instead", "dev": true }, - "node_modules/accepts": { - "version": "2.0.0", - "resolved": "https://registry.npmjs.org/accepts/-/accepts-2.0.0.tgz", - "integrity": "sha512-5cvg6CtKwfgdmVqY1WIiXKc3Q1bkRqGLi+2W/6ao+6Y7gu/RCwRuAhGEzh5B4KlszSuTLgZYuqFqo5bImjNKng==", - "license": "MIT", - "dependencies": { - "mime-types": "^3.0.0", - "negotiator": "^1.0.0" - }, - "engines": { - "node": ">= 0.6" - } - }, - "node_modules/accepts/node_modules/mime-db": { - "version": "1.54.0", - "resolved": "https://registry.npmjs.org/mime-db/-/mime-db-1.54.0.tgz", - "integrity": "sha512-aU5EJuIN2WDemCcAp2vFBfp/m4EAhWJnUNSSw0ixs7/kXbd6Pg64EmwJkNdFhB8aWt1sH2CTXrLxo/iAGV3oPQ==", - "license": "MIT", - "engines": { - "node": ">= 0.6" - } - }, - "node_modules/accepts/node_modules/mime-types": { - "version": "3.0.2", - "resolved": "https://registry.npmjs.org/mime-types/-/mime-types-3.0.2.tgz", - "integrity": "sha512-Lbgzdk0h4juoQ9fCKXW4by0UJqj+nOOrI9MJ1sSj4nI8aI2eo1qmvQEie4VD1glsS250n15LsWsYtCugiStS5A==", - "license": "MIT", - "dependencies": { - "mime-db": "^1.54.0" - }, - "engines": { - "node": ">=18" - }, - "funding": { - "type": "opencollective", - "url": "https://opencollective.com/express" - } - }, "node_modules/acorn": { "version": "8.11.3", "resolved": "https://registry.npmjs.org/acorn/-/acorn-8.11.3.tgz", @@ -5519,61 +5471,6 @@ "integrity": "sha512-XpNj6GDQzdfW+r2Wnn7xiSAd7TM3jzkxGXBGTtWKuSXv1xUV+azxAm8jdWZN06QTQk+2N2XB9jRDkvbmQmcRtg==", "dev": true }, - "node_modules/body-parser": { - "version": "2.2.2", - "resolved": "https://registry.npmjs.org/body-parser/-/body-parser-2.2.2.tgz", - "integrity": "sha512-oP5VkATKlNwcgvxi0vM0p/D3n2C3EReYVX+DNYs5TjZFn/oQt2j+4sVJtSMr18pdRr8wjTcBl6LoV+FUwzPmNA==", - "license": "MIT", - "dependencies": { - "bytes": "^3.1.2", - "content-type": "^1.0.5", - "debug": "^4.4.3", - "http-errors": "^2.0.0", - "iconv-lite": "^0.7.0", - "on-finished": "^2.4.1", - "qs": "^6.14.1", - "raw-body": "^3.0.1", - "type-is": "^2.0.1" - }, - "engines": { - "node": ">=18" - }, - "funding": { - "type": "opencollective", - "url": "https://opencollective.com/express" - } - }, - "node_modules/body-parser/node_modules/iconv-lite": { - "version": "0.7.2", - "resolved": "https://registry.npmjs.org/iconv-lite/-/iconv-lite-0.7.2.tgz", - "integrity": "sha512-im9DjEDQ55s9fL4EYzOAv0yMqmMBSZp6G0VvFyTMPKWxiSBHUj9NW/qqLmXUwXrrM7AvqSlTCfvqRb0cM8yYqw==", - "license": "MIT", - "dependencies": { - "safer-buffer": ">= 2.1.2 < 3.0.0" - }, - "engines": { - "node": ">=0.10.0" - }, - "funding": { - "type": "opencollective", - "url": "https://opencollective.com/express" - } - }, - "node_modules/body-parser/node_modules/qs": { - "version": "6.14.1", - "resolved": "https://registry.npmjs.org/qs/-/qs-6.14.1.tgz", - "integrity": "sha512-4EK3+xJl8Ts67nLYNwqw/dsFVnCf+qR7RgXSK9jEEm9unao3njwMDdmsdvoKBKHzxd7tCYz5e5M+SnMjdtXGQQ==", - "license": "BSD-3-Clause", - "dependencies": { - "side-channel": "^1.1.0" - }, - "engines": { - "node": ">=0.6" - }, - "funding": { - "url": "https://github.com/sponsors/ljharb" - } - }, "node_modules/brace-expansion": { "version": "1.1.11", "resolved": "https://registry.npmjs.org/brace-expansion/-/brace-expansion-1.1.11.tgz", @@ -5675,15 +5572,6 @@ "integrity": "sha512-E+XQCRwSbaaiChtv6k6Dwgc+bx+Bs6vuKJHHl5kox/BaKbhiXzqQOwK4cO22yElGp2OCmjwVhT3HmxgyPGnJfQ==", "devOptional": true }, - "node_modules/bytes": { - "version": "3.1.2", - "resolved": "https://registry.npmjs.org/bytes/-/bytes-3.1.2.tgz", - "integrity": "sha512-/Nf7TyzTx6S3yRJObOAV7956r8cr2+Oj8AC5dt8wSP3BQAoeX58NoHyCU8P8zGkNXStjTSi6fzO6F0pBdcYbEg==", - "license": "MIT", - "engines": { - "node": ">= 0.8" - } - }, "node_modules/cachedir": { "version": "2.4.0", "resolved": "https://registry.npmjs.org/cachedir/-/cachedir-2.4.0.tgz", @@ -5716,6 +5604,7 @@ "version": "1.0.2", "resolved": "https://registry.npmjs.org/call-bind-apply-helpers/-/call-bind-apply-helpers-1.0.2.tgz", "integrity": "sha512-Sp1ablJ0ivDkSzjcaJdxEunN5/XvksFJ2sMBFfq6x0ryhQV/2b/KwFe21cMpmHtPOSij8K99/wSfoEuTObmuMQ==", + "dev": true, "license": "MIT", "dependencies": { "es-errors": "^1.3.0", @@ -5729,6 +5618,7 @@ "version": "1.0.4", "resolved": "https://registry.npmjs.org/call-bound/-/call-bound-1.0.4.tgz", "integrity": "sha512-+ys997U96po4Kx/ABpBCqhA9EuxJaQWDQg7295H4hBphv3IZg0boBKuwYpt4YXp6MZ5AmZQnU/tyMTlRpaSejg==", + "dev": true, "license": "MIT", "dependencies": { "call-bind-apply-helpers": "^1.0.2", @@ -5953,52 +5843,12 @@ "resolved": "https://registry.npmjs.org/concat-map/-/concat-map-0.0.1.tgz", "integrity": "sha512-/Srv4dswyQNBfohGpz9o6Yb3Gz3SrUDqBH5rTuhGR7ahtlbYKnVxw2bCFMRljaA7EXHaXZ8wsHdodFvbkhKmqg==" }, - "node_modules/content-disposition": { - "version": "1.0.1", - "resolved": "https://registry.npmjs.org/content-disposition/-/content-disposition-1.0.1.tgz", - "integrity": "sha512-oIXISMynqSqm241k6kcQ5UwttDILMK4BiurCfGEREw6+X9jkkpEe5T9FZaApyLGGOnFuyMWZpdolTXMtvEJ08Q==", - "license": "MIT", - "engines": { - "node": ">=18" - }, - "funding": { - "type": "opencollective", - "url": "https://opencollective.com/express" - } - }, - "node_modules/content-type": { - "version": "1.0.5", - "resolved": "https://registry.npmjs.org/content-type/-/content-type-1.0.5.tgz", - "integrity": "sha512-nTjqfcBFEipKdXCv4YDQWCfmcLZKm81ldF0pAopTvyrFGVbcR6P/VAAd5G7N+0tTr8QqiU0tFadD6FK4NtJwOA==", - "license": "MIT", - "engines": { - "node": ">= 0.6" - } - }, "node_modules/convert-source-map": { "version": "2.0.0", "resolved": "https://registry.npmjs.org/convert-source-map/-/convert-source-map-2.0.0.tgz", "integrity": "sha512-Kvp459HrV2FEJ1CAsi1Ku+MY3kasH19TFykTz2xWmMeq6bk2NU3XXvfJ+Q61m0xktWwt+1HSYf3JZsTms3aRJg==", "devOptional": true }, - "node_modules/cookie": { - "version": "0.7.2", - "resolved": "https://registry.npmjs.org/cookie/-/cookie-0.7.2.tgz", - "integrity": "sha512-yki5XnKuf750l50uGTllt6kKILY4nQ1eNIQatoXEByZ5dWgnKqbnqmTrBE5B4N7lrMJKQ2ytWMiTO2o0v6Ew/w==", - "license": "MIT", - "engines": { - "node": ">= 0.6" - } - }, - "node_modules/cookie-signature": { - "version": "1.2.2", - "resolved": "https://registry.npmjs.org/cookie-signature/-/cookie-signature-1.2.2.tgz", - "integrity": "sha512-D76uU73ulSXrD1UXF4KE2TMxVVwhsnCgfAyTg9k8P6KGZjlXKrOLe4dJQKI3Bxi5wjesZoFXJWElNWBjPZMbhg==", - "license": "MIT", - "engines": { - "node": ">=6.6.0" - } - }, "node_modules/core-js-compat": { "version": "3.37.1", "resolved": "https://registry.npmjs.org/core-js-compat/-/core-js-compat-3.37.1.tgz", @@ -6720,15 +6570,6 @@ "node": ">=0.4.0" } }, - "node_modules/depd": { - "version": "2.0.0", - "resolved": "https://registry.npmjs.org/depd/-/depd-2.0.0.tgz", - "integrity": "sha512-g7nH6P6dyDioJogAAGprGpCtVImJhpPk/roCzdb3fIh61/s/nPsfR6onyMwkCAR/OlC3yBC0lESvUoQEAssIrw==", - "license": "MIT", - "engines": { - "node": ">= 0.8" - } - }, "node_modules/dequal": { "version": "2.0.3", "resolved": "https://registry.npmjs.org/dequal/-/dequal-2.0.3.tgz", @@ -6824,6 +6665,7 @@ "version": "1.0.1", "resolved": "https://registry.npmjs.org/dunder-proto/-/dunder-proto-1.0.1.tgz", "integrity": "sha512-KIN/nDJBQRcXw0MLVhZE9iQHmG68qAVIBg9CqmUYjmQIhgij9U5MFvrqkUL5FbtyyzZuOeOt0zdeRe4UY7ct+A==", + "dev": true, "license": "MIT", "dependencies": { "call-bind-apply-helpers": "^1.0.1", @@ -6844,12 +6686,6 @@ "safer-buffer": "^2.1.0" } }, - "node_modules/ee-first": { - "version": "1.1.1", - "resolved": "https://registry.npmjs.org/ee-first/-/ee-first-1.1.1.tgz", - "integrity": "sha512-WMwm9LhRUo+WUaRN+vRuETqG89IgZphVSNkdFgeb6sS/E4OrDIN7t48CAewSHXc6C8lefD8KKfr5vY61brQlow==", - "license": "MIT" - }, "node_modules/electron-to-chromium": { "version": "1.4.789", "resolved": "https://registry.npmjs.org/electron-to-chromium/-/electron-to-chromium-1.4.789.tgz", @@ -6874,15 +6710,6 @@ "integrity": "sha512-MSjYzcWNOA0ewAHpz0MxpYFvwg6yjy1NG3xteoqz644VCo/RPgnr1/GGt+ic3iJTzQ8Eu3TdM14SawnVUmGE6A==", "devOptional": true }, - "node_modules/encodeurl": { - "version": "2.0.0", - "resolved": "https://registry.npmjs.org/encodeurl/-/encodeurl-2.0.0.tgz", - "integrity": "sha512-Q0n9HRi4m6JuGIV1eFlmvJB7ZEVxu93IrMyiMsGC0lrMJMWzRgx6WGquyfQgZVb31vhGgXnfmPNNXmxnOkRBrg==", - "license": "MIT", - "engines": { - "node": ">= 0.8" - } - }, "node_modules/end-of-stream": { "version": "1.4.4", "resolved": "https://registry.npmjs.org/end-of-stream/-/end-of-stream-1.4.4.tgz", @@ -6990,6 +6817,7 @@ "version": "1.0.1", "resolved": "https://registry.npmjs.org/es-define-property/-/es-define-property-1.0.1.tgz", "integrity": "sha512-e3nRfgfUZ4rNGL232gUgX06QNyyez04KdjFrF+LTRoOXmrOgFKDg4BCdsjW8EnT69eqdYGmRpJwiPVYNrCaW3g==", + "dev": true, "license": "MIT", "engines": { "node": ">= 0.4" @@ -6999,6 +6827,7 @@ "version": "1.3.0", "resolved": "https://registry.npmjs.org/es-errors/-/es-errors-1.3.0.tgz", "integrity": "sha512-Zf5H2Kxt2xjTvbJvP2ZWLEICxA6j+hAmMzIlypy4xcBg1vKVnx89Wy0GbS+kf5cwCVFFzdCFh2XSCFNULS6csw==", + "dev": true, "engines": { "node": ">= 0.4" } @@ -7052,6 +6881,7 @@ "version": "1.1.1", "resolved": "https://registry.npmjs.org/es-object-atoms/-/es-object-atoms-1.1.1.tgz", "integrity": "sha512-FGgH2h8zKNim9ljj7dankFPcICIK9Cp5bm+c2gQSYePhpaG5+esrLODihIorn+Pe6FGJzWhXQotPv73jTaldXA==", + "dev": true, "license": "MIT", "dependencies": { "es-errors": "^1.3.0" @@ -7147,12 +6977,6 @@ "node": ">=6" } }, - "node_modules/escape-html": { - "version": "1.0.3", - "resolved": "https://registry.npmjs.org/escape-html/-/escape-html-1.0.3.tgz", - "integrity": "sha512-NiSupZ4OeuGwr68lGIeym/ksIZMJodUGOSCZ/FSnTxcrekbvqrgdUxlJOMpijaKZVjAJrWrGs/6Jy8OMuyj9ow==", - "license": "MIT" - }, "node_modules/escape-string-regexp": { "version": "4.0.0", "resolved": "https://registry.npmjs.org/escape-string-regexp/-/escape-string-regexp-4.0.0.tgz", @@ -7636,27 +7460,12 @@ "node": ">=0.10.0" } }, - "node_modules/etag": { - "version": "1.8.1", - "resolved": "https://registry.npmjs.org/etag/-/etag-1.8.1.tgz", - "integrity": "sha512-aIL5Fx7mawVa300al2BnEE4iNvo1qETxLrPI/o05L7z6go7fCw1J6EQmbK4FmJ2AS7kgVF/KEZWufBfdClMcPg==", - "license": "MIT", - "engines": { - "node": ">= 0.6" - } - }, "node_modules/eventemitter2": { "version": "6.4.7", "resolved": "https://registry.npmjs.org/eventemitter2/-/eventemitter2-6.4.7.tgz", "integrity": "sha512-tYUSVOGeQPKt/eC1ABfhHy5Xd96N3oIijJvN3O9+TsC28T5V9yX9oEfEK5faP0EFSNVOG97qtAS68GBrQB2hDg==", "dev": true }, - "node_modules/eventemitter3": { - "version": "4.0.7", - "resolved": "https://registry.npmjs.org/eventemitter3/-/eventemitter3-4.0.7.tgz", - "integrity": "sha512-8guHBZCwKnFhYdHr2ysuRWErTwhoN2X8XELRlrRwpmfeY2jjuUN4taQMsULKUVo1K4DvZl+0pgfyoysHxvmvEw==", - "license": "MIT" - }, "node_modules/execa": { "version": "4.1.0", "resolved": "https://registry.npmjs.org/execa/-/execa-4.1.0.tgz", @@ -7717,89 +7526,6 @@ "node": "^14.15.0 || ^16.10.0 || >=18.0.0" } }, - "node_modules/express": { - "version": "5.2.1", - "resolved": "https://registry.npmjs.org/express/-/express-5.2.1.tgz", - "integrity": "sha512-hIS4idWWai69NezIdRt2xFVofaF4j+6INOpJlVOLDO8zXGpUVEVzIYk12UUi2JzjEzWL3IOAxcTubgz9Po0yXw==", - "license": "MIT", - "dependencies": { - "accepts": "^2.0.0", - "body-parser": "^2.2.1", - "content-disposition": "^1.0.0", - "content-type": "^1.0.5", - "cookie": "^0.7.1", - "cookie-signature": "^1.2.1", - "debug": "^4.4.0", - "depd": "^2.0.0", - "encodeurl": "^2.0.0", - "escape-html": "^1.0.3", - "etag": "^1.8.1", - "finalhandler": "^2.1.0", - "fresh": "^2.0.0", - "http-errors": "^2.0.0", - "merge-descriptors": "^2.0.0", - "mime-types": "^3.0.0", - "on-finished": "^2.4.1", - "once": "^1.4.0", - "parseurl": "^1.3.3", - "proxy-addr": "^2.0.7", - "qs": "^6.14.0", - "range-parser": "^1.2.1", - "router": "^2.2.0", - "send": "^1.1.0", - "serve-static": "^2.2.0", - "statuses": "^2.0.1", - "type-is": "^2.0.1", - "vary": "^1.1.2" - }, - "engines": { - "node": ">= 18" - }, - "funding": { - "type": "opencollective", - "url": "https://opencollective.com/express" - } - }, - "node_modules/express/node_modules/mime-db": { - "version": "1.54.0", - "resolved": "https://registry.npmjs.org/mime-db/-/mime-db-1.54.0.tgz", - "integrity": "sha512-aU5EJuIN2WDemCcAp2vFBfp/m4EAhWJnUNSSw0ixs7/kXbd6Pg64EmwJkNdFhB8aWt1sH2CTXrLxo/iAGV3oPQ==", - "license": "MIT", - "engines": { - "node": ">= 0.6" - } - }, - "node_modules/express/node_modules/mime-types": { - "version": "3.0.2", - "resolved": "https://registry.npmjs.org/mime-types/-/mime-types-3.0.2.tgz", - "integrity": "sha512-Lbgzdk0h4juoQ9fCKXW4by0UJqj+nOOrI9MJ1sSj4nI8aI2eo1qmvQEie4VD1glsS250n15LsWsYtCugiStS5A==", - "license": "MIT", - "dependencies": { - "mime-db": "^1.54.0" - }, - "engines": { - "node": ">=18" - }, - "funding": { - "type": "opencollective", - "url": "https://opencollective.com/express" - } - }, - "node_modules/express/node_modules/qs": { - "version": "6.14.1", - "resolved": "https://registry.npmjs.org/qs/-/qs-6.14.1.tgz", - "integrity": "sha512-4EK3+xJl8Ts67nLYNwqw/dsFVnCf+qR7RgXSK9jEEm9unao3njwMDdmsdvoKBKHzxd7tCYz5e5M+SnMjdtXGQQ==", - "license": "BSD-3-Clause", - "dependencies": { - "side-channel": "^1.1.0" - }, - "engines": { - "node": ">=0.6" - }, - "funding": { - "url": "https://github.com/sponsors/ljharb" - } - }, "node_modules/extend": { "version": "3.0.2", "resolved": "https://registry.npmjs.org/extend/-/extend-3.0.2.tgz", @@ -7948,27 +7674,6 @@ "node": ">=8" } }, - "node_modules/finalhandler": { - "version": "2.1.1", - "resolved": "https://registry.npmjs.org/finalhandler/-/finalhandler-2.1.1.tgz", - "integrity": "sha512-S8KoZgRZN+a5rNwqTxlZZePjT/4cnm0ROV70LedRHZ0p8u9fRID0hJUZQpkKLzro8LfmC8sx23bY6tVNxv8pQA==", - "license": "MIT", - "dependencies": { - "debug": "^4.4.0", - "encodeurl": "^2.0.0", - "escape-html": "^1.0.3", - "on-finished": "^2.4.1", - "parseurl": "^1.3.3", - "statuses": "^2.0.1" - }, - "engines": { - "node": ">= 18.0.0" - }, - "funding": { - "type": "opencollective", - "url": "https://opencollective.com/express" - } - }, "node_modules/find-root": { "version": "1.1.0", "resolved": "https://registry.npmjs.org/find-root/-/find-root-1.1.0.tgz", @@ -8055,24 +7760,6 @@ "node": ">= 6" } }, - "node_modules/forwarded": { - "version": "0.2.0", - "resolved": "https://registry.npmjs.org/forwarded/-/forwarded-0.2.0.tgz", - "integrity": "sha512-buRG0fpBtRHSTCOASe6hD258tEubFoRLb4ZNA6NxMVHNw2gOcwHo9wyablzMzOA5z9xA9L1KNjk/Nt6MT9aYow==", - "license": "MIT", - "engines": { - "node": ">= 0.6" - } - }, - "node_modules/fresh": { - "version": "2.0.0", - "resolved": "https://registry.npmjs.org/fresh/-/fresh-2.0.0.tgz", - "integrity": "sha512-Rx/WycZ60HOaqLKAi6cHRKKI7zxWbJ31MhntmtwMoaTeF7XFH9hhBp8vITaMidfljRQ6eYWCKkaTK+ykVJHP2A==", - "license": "MIT", - "engines": { - "node": ">= 0.8" - } - }, "node_modules/fs-extra": { "version": "9.1.0", "resolved": "https://registry.npmjs.org/fs-extra/-/fs-extra-9.1.0.tgz", @@ -8164,6 +7851,7 @@ "version": "1.3.0", "resolved": "https://registry.npmjs.org/get-intrinsic/-/get-intrinsic-1.3.0.tgz", "integrity": "sha512-9fSjSaos/fRIVIp+xSJlE6lfwhES7LNtKaCBIamHsjr2na1BiABJPo0mOjjz8GJDURarmCPGqaiVg5mfjb98CQ==", + "dev": true, "license": "MIT", "dependencies": { "call-bind-apply-helpers": "^1.0.2", @@ -8197,6 +7885,7 @@ "version": "1.0.1", "resolved": "https://registry.npmjs.org/get-proto/-/get-proto-1.0.1.tgz", "integrity": "sha512-sTSfBjoXBp89JvIKIefqw7U2CCebsc74kiY6awiGogKtoSGbgjYE/G/+l9sF3MWFPNc9IcoOC4ODfKHfxFmp0g==", + "dev": true, "license": "MIT", "dependencies": { "dunder-proto": "^1.0.1", @@ -8355,6 +8044,7 @@ "version": "1.2.0", "resolved": "https://registry.npmjs.org/gopd/-/gopd-1.2.0.tgz", "integrity": "sha512-ZUKRh6/kUFoAiTAtTYPZJ3hw9wNxx+BIBOijnlG9PnrJsCcSjs1wyyD6vJpaYtgnzDrKYRSqf3OO6Rfa93xsRg==", + "dev": true, "license": "MIT", "engines": { "node": ">= 0.4" @@ -8417,6 +8107,7 @@ "version": "1.1.0", "resolved": "https://registry.npmjs.org/has-symbols/-/has-symbols-1.1.0.tgz", "integrity": "sha512-1cDNdwJ2Jaohmb3sg4OmKaMBwuC48sYni5HUw2DvsC8LjGTLK9h+eb1X6RyuOHe4hT0ULCW68iomhjUoKUqlPQ==", + "dev": true, "license": "MIT", "engines": { "node": ">= 0.4" @@ -8490,40 +8181,6 @@ "void-elements": "3.1.0" } }, - "node_modules/http-errors": { - "version": "2.0.1", - "resolved": "https://registry.npmjs.org/http-errors/-/http-errors-2.0.1.tgz", - "integrity": "sha512-4FbRdAX+bSdmo4AUFuS0WNiPz8NgFt+r8ThgNWmlrjQjt1Q7ZR9+zTlce2859x4KSXrwIsaeTqDoKQmtP8pLmQ==", - "license": "MIT", - "dependencies": { - "depd": "~2.0.0", - "inherits": "~2.0.4", - "setprototypeof": "~1.2.0", - "statuses": "~2.0.2", - "toidentifier": "~1.0.1" - }, - "engines": { - "node": ">= 0.8" - }, - "funding": { - "type": "opencollective", - "url": "https://opencollective.com/express" - } - }, - "node_modules/http-proxy": { - "version": "1.18.1", - "resolved": "https://registry.npmjs.org/http-proxy/-/http-proxy-1.18.1.tgz", - "integrity": "sha512-7mz/721AbnJwIVbnaSv1Cz3Am0ZLT/UBwkC92VlxhXv/k/BBQfM2fXElQNC27BVGr0uwUpplYPQM9LnaBMR5NQ==", - "license": "MIT", - "dependencies": { - "eventemitter3": "^4.0.0", - "follow-redirects": "^1.0.0", - "requires-port": "^1.0.0" - }, - "engines": { - "node": ">=8.0.0" - } - }, "node_modules/http-proxy-agent": { "version": "5.0.0", "resolved": "https://registry.npmjs.org/http-proxy-agent/-/http-proxy-agent-5.0.0.tgz", @@ -8538,23 +8195,6 @@ "node": ">= 6" } }, - "node_modules/http-proxy-middleware": { - "version": "3.0.5", - "resolved": "https://registry.npmjs.org/http-proxy-middleware/-/http-proxy-middleware-3.0.5.tgz", - "integrity": "sha512-GLZZm1X38BPY4lkXA01jhwxvDoOkkXqjgVyUzVxiEK4iuRu03PZoYHhHRwxnfhQMDuaxi3vVri0YgSro/1oWqg==", - "license": "MIT", - "dependencies": { - "@types/http-proxy": "^1.17.15", - "debug": "^4.3.6", - "http-proxy": "^1.18.1", - "is-glob": "^4.0.3", - "is-plain-object": "^5.0.0", - "micromatch": "^4.0.8" - }, - "engines": { - "node": "^14.15.0 || ^16.10.0 || >=18.0.0" - } - }, "node_modules/http-signature": { "version": "1.3.6", "resolved": "https://registry.npmjs.org/http-signature/-/http-signature-1.3.6.tgz", @@ -8795,15 +8435,6 @@ "loose-envify": "^1.0.0" } }, - "node_modules/ipaddr.js": { - "version": "1.9.1", - "resolved": "https://registry.npmjs.org/ipaddr.js/-/ipaddr.js-1.9.1.tgz", - "integrity": "sha512-0KI/607xoxSToH7GjN1FfSbLoU0+btTicjsQSWQlh/hZykN8KpmMf7uYwPW3R+akZ6R/w18ZlXSHBYXiYUPO3g==", - "license": "MIT", - "engines": { - "node": ">= 0.10" - } - }, "node_modules/is-arguments": { "version": "1.1.1", "resolved": "https://registry.npmjs.org/is-arguments/-/is-arguments-1.1.1.tgz", @@ -9084,27 +8715,12 @@ "node": ">=8" } }, - "node_modules/is-plain-object": { - "version": "5.0.0", - "resolved": "https://registry.npmjs.org/is-plain-object/-/is-plain-object-5.0.0.tgz", - "integrity": "sha512-VRSzKkbMm5jMDoKLbltAkFQ5Qr7VDiTFGXxYFXXowVj387GeGNOCsOH6Msy00SGZ3Fp84b1Naa1psqgcCIEP5Q==", - "license": "MIT", - "engines": { - "node": ">=0.10.0" - } - }, "node_modules/is-potential-custom-element-name": { "version": "1.0.1", "resolved": "https://registry.npmjs.org/is-potential-custom-element-name/-/is-potential-custom-element-name-1.0.1.tgz", "integrity": "sha512-bCYeRA2rVibKZd+s2625gGnGF/t7DSqDs4dP7CrLA1m7jKWz6pps0LpYLJN8Q64HtmPKJ1hrN3nzPNKFEKOUiQ==", "dev": true }, - "node_modules/is-promise": { - "version": "4.0.0", - "resolved": "https://registry.npmjs.org/is-promise/-/is-promise-4.0.0.tgz", - "integrity": "sha512-hvpoI6korhJMnej285dSg6nu1+e6uxs7zG3BYAm5byqDsgJNWwxzM6z6iZiAgQR4TJ30JmBTOwqZUw3WlyH3AQ==", - "license": "MIT" - }, "node_modules/is-regex": { "version": "1.1.4", "resolved": "https://registry.npmjs.org/is-regex/-/is-regex-1.1.4.tgz", @@ -11813,37 +11429,17 @@ "version": "1.1.0", "resolved": "https://registry.npmjs.org/math-intrinsics/-/math-intrinsics-1.1.0.tgz", "integrity": "sha512-/IXtbwEk5HTPyEwyKX6hGkYXxM9nbj64B+ilVJnC/R6B0pH5G4V3b0pVbL7DBj4tkhBAppbQUlf6F6Xl9LHu1g==", + "dev": true, "license": "MIT", "engines": { "node": ">= 0.4" } }, - "node_modules/media-typer": { - "version": "1.1.0", - "resolved": "https://registry.npmjs.org/media-typer/-/media-typer-1.1.0.tgz", - "integrity": "sha512-aisnrDP4GNe06UcKFnV5bfMNPBUw4jsLGaWwWfnH3v02GnBuXX2MCVn5RbrWo0j3pczUilYblq7fQ7Nw2t5XKw==", - "license": "MIT", - "engines": { - "node": ">= 0.8" - } - }, "node_modules/memoize-one": { "version": "6.0.0", "resolved": "https://registry.npmjs.org/memoize-one/-/memoize-one-6.0.0.tgz", "integrity": "sha512-rkpe71W0N0c0Xz6QD0eJETuWAJGnJ9afsl1srmwPrI+yBCkge5EycXXbYRyvL29zZVUWQCY7InPRCv3GDXuZNw==" }, - "node_modules/merge-descriptors": { - "version": "2.0.0", - "resolved": "https://registry.npmjs.org/merge-descriptors/-/merge-descriptors-2.0.0.tgz", - "integrity": "sha512-Snk314V5ayFLhp3fkUREub6WtjBfPdCPY1Ln8/8munuLuiYhsABgBVWsozAG+MWMbVEvcdcpbi9R7ww22l9Q3g==", - "license": "MIT", - "engines": { - "node": ">=18" - }, - "funding": { - "url": "https://github.com/sponsors/sindresorhus" - } - }, "node_modules/merge-stream": { "version": "2.0.0", "resolved": "https://registry.npmjs.org/merge-stream/-/merge-stream-2.0.0.tgz", @@ -11976,15 +11572,6 @@ "resolved": "https://registry.npmjs.org/natural-compare/-/natural-compare-1.4.0.tgz", "integrity": "sha512-OWND8ei3VtNC9h7V60qff3SVobHr996CTwgxubgyQYEpg290h9J0buyECNNJexkFm5sOajh5G116RYA1c8ZMSw==" }, - "node_modules/negotiator": { - "version": "1.0.0", - "resolved": "https://registry.npmjs.org/negotiator/-/negotiator-1.0.0.tgz", - "integrity": "sha512-8Ofs/AUQh8MaEcrlq5xOX0CQ9ypTF5dl78mjlMNfOK08fzpgTHQRQPBxcPlEtIw0yRpws+Zo/3r+5WRby7u3Gg==", - "license": "MIT", - "engines": { - "node": ">= 0.6" - } - }, "node_modules/node-fetch": { "version": "2.7.0", "resolved": "https://registry.npmjs.org/node-fetch/-/node-fetch-2.7.0.tgz", @@ -12078,6 +11665,7 @@ "version": "1.13.4", "resolved": "https://registry.npmjs.org/object-inspect/-/object-inspect-1.13.4.tgz", "integrity": "sha512-W67iLl4J2EXEGTbfeHCffrjDfitvLANg0UlX3wFUUSTx92KXRFegMHUVgSqE+wvhAbi4WqjGg9czysTV2Epbew==", + "dev": true, "license": "MIT", "engines": { "node": ">= 0.4" @@ -12195,18 +11783,6 @@ "url": "https://github.com/sponsors/ljharb" } }, - "node_modules/on-finished": { - "version": "2.4.1", - "resolved": "https://registry.npmjs.org/on-finished/-/on-finished-2.4.1.tgz", - "integrity": "sha512-oVlzkg3ENAhCk2zdv7IJwd/QUD4z2RxRwpkcGY8psCVcCYZNq4wYnVWALHM+brtuJjePWiYF/ClmuDr8Ch5+kg==", - "license": "MIT", - "dependencies": { - "ee-first": "1.1.1" - }, - "engines": { - "node": ">= 0.8" - } - }, "node_modules/once": { "version": "1.4.0", "resolved": "https://registry.npmjs.org/once/-/once-1.4.0.tgz", @@ -12357,15 +11933,6 @@ "url": "https://github.com/inikulin/parse5?sponsor=1" } }, - "node_modules/parseurl": { - "version": "1.3.3", - "resolved": "https://registry.npmjs.org/parseurl/-/parseurl-1.3.3.tgz", - "integrity": "sha512-CiyeOxFT/JZyN5m0z9PfXw4SCBJ6Sygz1Dpl0wqjlhDEGGBP1GnsUVEL0p63hoG1fcj3fHynXi9NYO4nWOL+qQ==", - "license": "MIT", - "engines": { - "node": ">= 0.8" - } - }, "node_modules/path-exists": { "version": "4.0.0", "resolved": "https://registry.npmjs.org/path-exists/-/path-exists-4.0.0.tgz", @@ -12395,16 +11962,6 @@ "resolved": "https://registry.npmjs.org/path-parse/-/path-parse-1.0.7.tgz", "integrity": "sha512-LDJzPVEEEPR+y48z93A0Ed0yXb8pAByGWo/k5YYdYgpY2/2EsOsksJrq7lOHxryrVOn1ejG6oAp8ahvOIQD8sw==" }, - "node_modules/path-to-regexp": { - "version": "8.3.0", - "resolved": "https://registry.npmjs.org/path-to-regexp/-/path-to-regexp-8.3.0.tgz", - "integrity": "sha512-7jdwVIRtsP8MYpdXSwOS0YdD0Du+qOoF/AEPIt88PcCFrZCzx41oxku1jD88hZBwbNUIEfpqvuhjFaMAqMTWnA==", - "license": "MIT", - "funding": { - "type": "opencollective", - "url": "https://opencollective.com/express" - } - }, "node_modules/path-type": { "version": "4.0.0", "resolved": "https://registry.npmjs.org/path-type/-/path-type-4.0.0.tgz", @@ -12613,19 +12170,6 @@ "resolved": "https://registry.npmjs.org/react-is/-/react-is-16.13.1.tgz", "integrity": "sha512-24e6ynE2H+OKt4kqsOvNd8kBpV65zoxbA4BVsEOB3ARVWQki/DHzaUoC5KuON/BiccDaCCTZBuOcfZs70kR8bQ==" }, - "node_modules/proxy-addr": { - "version": "2.0.7", - "resolved": "https://registry.npmjs.org/proxy-addr/-/proxy-addr-2.0.7.tgz", - "integrity": "sha512-llQsMLSUDUPT44jdrU/O37qlnifitDP+ZwrmmZcoSKyLKvtZxpyV0n2/bD/N4tBAAZ/gJEdZU7KMraoK1+XYAg==", - "license": "MIT", - "dependencies": { - "forwarded": "0.2.0", - "ipaddr.js": "1.9.1" - }, - "engines": { - "node": ">= 0.10" - } - }, "node_modules/proxy-from-env": { "version": "1.1.0", "resolved": "https://registry.npmjs.org/proxy-from-env/-/proxy-from-env-1.1.0.tgz", @@ -12711,46 +12255,6 @@ } ] }, - "node_modules/range-parser": { - "version": "1.2.1", - "resolved": "https://registry.npmjs.org/range-parser/-/range-parser-1.2.1.tgz", - "integrity": "sha512-Hrgsx+orqoygnmhFbKaHE6c296J+HTAQXoxEF6gNupROmmGJRoyzfG3ccAveqCBrwr/2yxQ5BVd/GTl5agOwSg==", - "license": "MIT", - "engines": { - "node": ">= 0.6" - } - }, - "node_modules/raw-body": { - "version": "3.0.2", - "resolved": "https://registry.npmjs.org/raw-body/-/raw-body-3.0.2.tgz", - "integrity": "sha512-K5zQjDllxWkf7Z5xJdV0/B0WTNqx6vxG70zJE4N0kBs4LovmEYWJzQGxC9bS9RAKu3bgM40lrd5zoLJ12MQ5BA==", - "license": "MIT", - "dependencies": { - "bytes": "~3.1.2", - "http-errors": "~2.0.1", - "iconv-lite": "~0.7.0", - "unpipe": "~1.0.0" - }, - "engines": { - "node": ">= 0.10" - } - }, - "node_modules/raw-body/node_modules/iconv-lite": { - "version": "0.7.2", - "resolved": "https://registry.npmjs.org/iconv-lite/-/iconv-lite-0.7.2.tgz", - "integrity": "sha512-im9DjEDQ55s9fL4EYzOAv0yMqmMBSZp6G0VvFyTMPKWxiSBHUj9NW/qqLmXUwXrrM7AvqSlTCfvqRb0cM8yYqw==", - "license": "MIT", - "dependencies": { - "safer-buffer": ">= 2.1.2 < 3.0.0" - }, - "engines": { - "node": ">=0.10.0" - }, - "funding": { - "type": "opencollective", - "url": "https://opencollective.com/express" - } - }, "node_modules/react": { "version": "18.3.1", "resolved": "https://registry.npmjs.org/react/-/react-18.3.1.tgz", @@ -13096,7 +12600,8 @@ "node_modules/requires-port": { "version": "1.0.0", "resolved": "https://registry.npmjs.org/requires-port/-/requires-port-1.0.0.tgz", - "integrity": "sha512-KigOCHcocU3XODJxsu8i/j8T9tzT4adHiecwORRQ0ZZFcp7ahwXuRU1m+yuO90C5ZUyGeGfocHDI14M3L3yDAQ==" + "integrity": "sha512-KigOCHcocU3XODJxsu8i/j8T9tzT4adHiecwORRQ0ZZFcp7ahwXuRU1m+yuO90C5ZUyGeGfocHDI14M3L3yDAQ==", + "dev": true }, "node_modules/reselect": { "version": "4.1.8", @@ -13232,22 +12737,6 @@ "fsevents": "~2.3.2" } }, - "node_modules/router": { - "version": "2.2.0", - "resolved": "https://registry.npmjs.org/router/-/router-2.2.0.tgz", - "integrity": "sha512-nLTrUKm2UyiL7rlhapu/Zl45FwNgkZGaCpZbIHajDYgwlJCOzLSk+cIPAnsEqV955GjILJnKbdQC1nVPz+gAYQ==", - "license": "MIT", - "dependencies": { - "debug": "^4.4.0", - "depd": "^2.0.0", - "is-promise": "^4.0.0", - "parseurl": "^1.3.3", - "path-to-regexp": "^8.0.0" - }, - "engines": { - "node": ">= 18" - } - }, "node_modules/run-parallel": { "version": "1.2.0", "resolved": "https://registry.npmjs.org/run-parallel/-/run-parallel-1.2.0.tgz", @@ -13337,7 +12826,8 @@ "node_modules/safer-buffer": { "version": "2.1.2", "resolved": "https://registry.npmjs.org/safer-buffer/-/safer-buffer-2.1.2.tgz", - "integrity": "sha512-YZo3K82SD7Riyi0E1EQPojLz7kpepnSQI9IyPbHHg1XXXevb5dJI7tpyN2ADxGcQbHG7vcyRHk0cbwqcQriUtg==" + "integrity": "sha512-YZo3K82SD7Riyi0E1EQPojLz7kpepnSQI9IyPbHHg1XXXevb5dJI7tpyN2ADxGcQbHG7vcyRHk0cbwqcQriUtg==", + "dev": true }, "node_modules/saxes": { "version": "6.0.0", @@ -13368,76 +12858,6 @@ "semver": "bin/semver.js" } }, - "node_modules/send": { - "version": "1.2.1", - "resolved": "https://registry.npmjs.org/send/-/send-1.2.1.tgz", - "integrity": "sha512-1gnZf7DFcoIcajTjTwjwuDjzuz4PPcY2StKPlsGAQ1+YH20IRVrBaXSWmdjowTJ6u8Rc01PoYOGHXfP1mYcZNQ==", - "license": "MIT", - "dependencies": { - "debug": "^4.4.3", - "encodeurl": "^2.0.0", - "escape-html": "^1.0.3", - "etag": "^1.8.1", - "fresh": "^2.0.0", - "http-errors": "^2.0.1", - "mime-types": "^3.0.2", - "ms": "^2.1.3", - "on-finished": "^2.4.1", - "range-parser": "^1.2.1", - "statuses": "^2.0.2" - }, - "engines": { - "node": ">= 18" - }, - "funding": { - "type": "opencollective", - "url": "https://opencollective.com/express" - } - }, - "node_modules/send/node_modules/mime-db": { - "version": "1.54.0", - "resolved": "https://registry.npmjs.org/mime-db/-/mime-db-1.54.0.tgz", - "integrity": "sha512-aU5EJuIN2WDemCcAp2vFBfp/m4EAhWJnUNSSw0ixs7/kXbd6Pg64EmwJkNdFhB8aWt1sH2CTXrLxo/iAGV3oPQ==", - "license": "MIT", - "engines": { - "node": ">= 0.6" - } - }, - "node_modules/send/node_modules/mime-types": { - "version": "3.0.2", - "resolved": "https://registry.npmjs.org/mime-types/-/mime-types-3.0.2.tgz", - "integrity": "sha512-Lbgzdk0h4juoQ9fCKXW4by0UJqj+nOOrI9MJ1sSj4nI8aI2eo1qmvQEie4VD1glsS250n15LsWsYtCugiStS5A==", - "license": "MIT", - "dependencies": { - "mime-db": "^1.54.0" - }, - "engines": { - "node": ">=18" - }, - "funding": { - "type": "opencollective", - "url": "https://opencollective.com/express" - } - }, - "node_modules/serve-static": { - "version": "2.2.1", - "resolved": "https://registry.npmjs.org/serve-static/-/serve-static-2.2.1.tgz", - "integrity": "sha512-xRXBn0pPqQTVQiC8wyQrKs2MOlX24zQ0POGaj0kultvoOCstBQM5yvOhAVSUwOMjQtTvsPWoNCHfPGwaaQJhTw==", - "license": "MIT", - "dependencies": { - "encodeurl": "^2.0.0", - "escape-html": "^1.0.3", - "parseurl": "^1.3.3", - "send": "^1.2.0" - }, - "engines": { - "node": ">= 18" - }, - "funding": { - "type": "opencollective", - "url": "https://opencollective.com/express" - } - }, "node_modules/set-function-length": { "version": "1.2.2", "resolved": "https://registry.npmjs.org/set-function-length/-/set-function-length-1.2.2.tgz", @@ -13470,12 +12890,6 @@ "node": ">= 0.4" } }, - "node_modules/setprototypeof": { - "version": "1.2.0", - "resolved": "https://registry.npmjs.org/setprototypeof/-/setprototypeof-1.2.0.tgz", - "integrity": "sha512-E5LDX7Wrp85Kil5bhZv46j8jOeboKq5JMmYM3gVGdGH8xFpPWXUMsNrlODCrkoxMEeNi/XZIwuRvY4XNwYMJpw==", - "license": "ISC" - }, "node_modules/shebang-command": { "version": "2.0.0", "resolved": "https://registry.npmjs.org/shebang-command/-/shebang-command-2.0.0.tgz", @@ -13499,6 +12913,7 @@ "version": "1.1.0", "resolved": "https://registry.npmjs.org/side-channel/-/side-channel-1.1.0.tgz", "integrity": "sha512-ZX99e6tRweoUXqR+VBrslhda51Nh5MTQwou5tnUDgbtyM0dBgmhEDtWGP/xbKn6hqfPRHujUNwz5fy/wbbhnpw==", + "dev": true, "license": "MIT", "dependencies": { "es-errors": "^1.3.0", @@ -13518,6 +12933,7 @@ "version": "1.0.0", "resolved": "https://registry.npmjs.org/side-channel-list/-/side-channel-list-1.0.0.tgz", "integrity": "sha512-FCLHtRD/gnpCiCHEiJLOwdmFP+wzCmDEkc9y7NsYxeF4u7Btsn1ZuwgwJGxImImHicJArLP4R0yX4c2KCrMrTA==", + "dev": true, "license": "MIT", "dependencies": { "es-errors": "^1.3.0", @@ -13534,6 +12950,7 @@ "version": "1.0.1", "resolved": "https://registry.npmjs.org/side-channel-map/-/side-channel-map-1.0.1.tgz", "integrity": "sha512-VCjCNfgMsby3tTdo02nbjtM/ewra6jPHmpThenkTYh8pG9ucZ/1P8So4u4FGBek/BjpOVsDCMoLA/iuBKIFXRA==", + "dev": true, "license": "MIT", "dependencies": { "call-bound": "^1.0.2", @@ -13552,6 +12969,7 @@ "version": "1.0.2", "resolved": "https://registry.npmjs.org/side-channel-weakmap/-/side-channel-weakmap-1.0.2.tgz", "integrity": "sha512-WPS/HvHQTYnHisLo9McqBHOJk2FkHO/tlpvldyrnem4aeQp4hai3gythswg6p01oSoTl58rcpiFAjF2br2Ak2A==", + "dev": true, "license": "MIT", "dependencies": { "call-bound": "^1.0.2", @@ -13722,15 +13140,6 @@ "node": ">=8" } }, - "node_modules/statuses": { - "version": "2.0.2", - "resolved": "https://registry.npmjs.org/statuses/-/statuses-2.0.2.tgz", - "integrity": "sha512-DvEy55V3DB7uknRo+4iOGT5fP1slR8wQohVdknigZPMpMstaKJQWhwiYBACJE3Ul2pTnATihhBYnRhZQHGBiRw==", - "license": "MIT", - "engines": { - "node": ">= 0.8" - } - }, "node_modules/stop-iteration-iterator": { "version": "1.0.0", "resolved": "https://registry.npmjs.org/stop-iteration-iterator/-/stop-iteration-iterator-1.0.0.tgz", @@ -13985,15 +13394,6 @@ "node": ">=8.0" } }, - "node_modules/toidentifier": { - "version": "1.0.1", - "resolved": "https://registry.npmjs.org/toidentifier/-/toidentifier-1.0.1.tgz", - "integrity": "sha512-o5sSPKEkg/DIQNmH43V0/uerLrpzVedkUh8tGNvaeXpfpuwjKenlSox/2O/BTlZUtEe+JG7s5YhEz608PlAHRA==", - "license": "MIT", - "engines": { - "node": ">=0.6" - } - }, "node_modules/tough-cookie": { "version": "4.1.4", "resolved": "https://registry.npmjs.org/tough-cookie/-/tough-cookie-4.1.4.tgz", @@ -14105,45 +13505,6 @@ "url": "https://github.com/sponsors/sindresorhus" } }, - "node_modules/type-is": { - "version": "2.0.1", - "resolved": "https://registry.npmjs.org/type-is/-/type-is-2.0.1.tgz", - "integrity": "sha512-OZs6gsjF4vMp32qrCbiVSkrFmXtG/AZhY3t0iAMrMBiAZyV9oALtXO8hsrHbMXF9x6L3grlFuwW2oAz7cav+Gw==", - "license": "MIT", - "dependencies": { - "content-type": "^1.0.5", - "media-typer": "^1.1.0", - "mime-types": "^3.0.0" - }, - "engines": { - "node": ">= 0.6" - } - }, - "node_modules/type-is/node_modules/mime-db": { - "version": "1.54.0", - "resolved": "https://registry.npmjs.org/mime-db/-/mime-db-1.54.0.tgz", - "integrity": "sha512-aU5EJuIN2WDemCcAp2vFBfp/m4EAhWJnUNSSw0ixs7/kXbd6Pg64EmwJkNdFhB8aWt1sH2CTXrLxo/iAGV3oPQ==", - "license": "MIT", - "engines": { - "node": ">= 0.6" - } - }, - "node_modules/type-is/node_modules/mime-types": { - "version": "3.0.2", - "resolved": "https://registry.npmjs.org/mime-types/-/mime-types-3.0.2.tgz", - "integrity": "sha512-Lbgzdk0h4juoQ9fCKXW4by0UJqj+nOOrI9MJ1sSj4nI8aI2eo1qmvQEie4VD1glsS250n15LsWsYtCugiStS5A==", - "license": "MIT", - "dependencies": { - "mime-db": "^1.54.0" - }, - "engines": { - "node": ">=18" - }, - "funding": { - "type": "opencollective", - "url": "https://opencollective.com/express" - } - }, "node_modules/typed-array-buffer": { "version": "1.0.2", "resolved": "https://registry.npmjs.org/typed-array-buffer/-/typed-array-buffer-1.0.2.tgz", @@ -14262,7 +13623,8 @@ "node_modules/undici-types": { "version": "5.26.5", "resolved": "https://registry.npmjs.org/undici-types/-/undici-types-5.26.5.tgz", - "integrity": "sha512-JlCMO+ehdEIKqlFxk6IfVoAUVmgz7cU7zD/h9XZ0qzeosSHmUJVOzSQvvYSYWXkFXC+IfLKSIffhv0sVZup6pA==" + "integrity": "sha512-JlCMO+ehdEIKqlFxk6IfVoAUVmgz7cU7zD/h9XZ0qzeosSHmUJVOzSQvvYSYWXkFXC+IfLKSIffhv0sVZup6pA==", + "devOptional": true }, "node_modules/unicode-canonical-property-names-ecmascript": { "version": "2.0.0", @@ -14313,15 +13675,6 @@ "node": ">= 10.0.0" } }, - "node_modules/unpipe": { - "version": "1.0.0", - "resolved": "https://registry.npmjs.org/unpipe/-/unpipe-1.0.0.tgz", - "integrity": "sha512-pjy2bYhSsufwWlKwPc+l3cN7+wuJlK6uz0YdJEOlQDbl6jo/YlPi4mb8agUkVC8BF7V8NuzeyPNqRksA3hztKQ==", - "license": "MIT", - "engines": { - "node": ">= 0.8" - } - }, "node_modules/untildify": { "version": "4.0.0", "resolved": "https://registry.npmjs.org/untildify/-/untildify-4.0.0.tgz", @@ -14402,15 +13755,6 @@ "node": ">=10.12.0" } }, - "node_modules/vary": { - "version": "1.1.2", - "resolved": "https://registry.npmjs.org/vary/-/vary-1.1.2.tgz", - "integrity": "sha512-BNGbWLfd0eUPabhkXUVm0j8uuvREyTh5ovRa/dyow/BqAbZJyC+5fU+IzQOzmAKzYqYRAISoRhdQr3eIZ/PXqg==", - "license": "MIT", - "engines": { - "node": ">= 0.8" - } - }, "node_modules/verror": { "version": "1.10.0", "resolved": "https://registry.npmjs.org/verror/-/verror-1.10.0.tgz", diff --git a/frontend/package.json b/frontend/package.json index dff5684c..a2b492bc 100644 --- a/frontend/package.json +++ b/frontend/package.json @@ -12,8 +12,6 @@ "@mui/x-data-grid": "^6.20.0", "axios": "^1.6.7", "eslint-plugin-jest": "^27.8.0", - "express": "^5.2.1", - "http-proxy-middleware": "^3.0.5", "moment": "^2.30.1", "react": "^18.2.0", "react-big-calendar": "^1.11.2", @@ -79,7 +77,6 @@ "build": "vite build", "lint": "eslint . --ext js,jsx --report-unused-disable-directives --max-warnings 0", "preview": "vite preview", - "start": "node server.js", "test": "jest --coverage", "cypress": "cypress open", "format": "prettier --write ." diff --git a/frontend/server.js b/frontend/server.js deleted file mode 100644 index 702203af..00000000 --- a/frontend/server.js +++ /dev/null @@ -1,57 +0,0 @@ -import express from 'express'; -import path from 'path'; -import { fileURLToPath } from 'url'; -import { createProxyMiddleware } from 'http-proxy-middleware'; - -const __filename = fileURLToPath(import.meta.url); -const __dirname = path.dirname(__filename); - -const app = express(); -const PORT = process.env.PORT || 5173; -const API_URL = process.env.API_URL || 'http://localhost:8000'; - -// Proxy API requests -app.use( - '/api', - createProxyMiddleware({ - target: API_URL, - changeOrigin: true, - timeout: 10000, // 10 second timeout - proxyTimeout: 10000, - logLevel: process.env.NODE_ENV !== 'production' && 'debug', - onProxyReq: (proxyReq, req, res) => { - console.log(`[${new Date().toISOString()}] Proxying: ${req.method} ${req.url} -> ${API_URL}${req.url}`); - }, - onProxyRes: (proxyRes, req, res) => { - console.log(`[${new Date().toISOString()}] Response: ${proxyRes.statusCode} in ${Date.now() - req._startTime}ms`); - proxyRes.headers['access-control-allow-origin'] = '*'; - proxyRes.headers['access-control-allow-methods'] = - 'GET, POST, OPTIONS, PUT, DELETE'; - proxyRes.headers['access-control-allow-headers'] = - 'DNT,User-Agent,X-Requested-With,If-Modified-Since,Cache-Control,Content-Type,Range,Authorization'; - }, - onError: (err, req, res) => { - console.error(`[${new Date().toISOString()}] Proxy error:`, err.message); - res.status(502).json({ error: 'Bad Gateway', message: err.message }); - }, - }) -); - -// Add timing middleware before proxy -app.use('/api', (req, res, next) => { - req._startTime = Date.now(); - next(); -}); - -// Serve static files -app.use(express.static(path.join(__dirname, 'dist'))); - -// Handle SPA routing - fallback middleware -app.use((req, res, next) => { - res.sendFile(path.join(__dirname, 'dist', 'index.html')); -}); - -app.listen(PORT, '0.0.0.0', () => { - console.log(`Server is running on port ${PORT}`); - console.log(`Proxying /api to ${API_URL}`); -}); \ No newline at end of file From 811f8e2f17e40e4dfd35b1fec225a2fe0e3a781d Mon Sep 17 00:00:00 2001 From: iPegii <51372604+iPegii@users.noreply.github.com> Date: Sun, 8 Feb 2026 12:43:18 +0200 Subject: [PATCH 091/149] fix: showing users in ownpage --- frontend/src/components/AllUsers.jsx | 20 ++++++++++---------- frontend/src/pages/ownpage.jsx | 2 +- 2 files changed, 11 insertions(+), 11 deletions(-) diff --git a/frontend/src/components/AllUsers.jsx b/frontend/src/components/AllUsers.jsx index b9e7d7e6..1ac2b6be 100644 --- a/frontend/src/components/AllUsers.jsx +++ b/frontend/src/components/AllUsers.jsx @@ -239,7 +239,7 @@ const AllUsers = ({ variant="contained" className="submit-key-button" data-testid="submit-key-button" - onClick={() => {handleKeyForm(userDetailsId, selectedOrganization.Organisaatio)}} + onClick={() => { handleKeyForm(userDetailsId, selectedOrganization.Organisaatio) }} > {t("givekey")} @@ -256,18 +256,18 @@ const AllUsers = ({ {userDetailsResRights ? ( - ): - } + ) : + } {/* Dialog actions */} diff --git a/frontend/src/pages/ownpage.jsx b/frontend/src/pages/ownpage.jsx index 6debf30b..b0d76459 100644 --- a/frontend/src/pages/ownpage.jsx +++ b/frontend/src/pages/ownpage.jsx @@ -338,7 +338,7 @@ const OwnPage = () => { email: u.email, Telegram: u.telegram, Rooli: ROLE_DESCRIPTIONS[u.role], - Jäsenyydet: u.keys.map((organization) => organization.name), + Jäsenyydet: u.keys ? u.keys.map((organization) => organization.name) : [], resrights: u.rights_for_reservation, })); setAllUsers(userData); From 4ddaf72f286e55c7100644b2d75c0724d571631b Mon Sep 17 00:00:00 2001 From: iPegii <51372604+iPegii@users.noreply.github.com> Date: Sun, 8 Feb 2026 12:55:21 +0200 Subject: [PATCH 092/149] Fix issues with pagination --- backend/ilotalo/views.py | 5 ++++- frontend/src/pages/defectfaultpage.jsx | 4 ++-- frontend/src/pages/ownpage.jsx | 1 - 3 files changed, 6 insertions(+), 4 deletions(-) diff --git a/backend/ilotalo/views.py b/backend/ilotalo/views.py index 6a038223..910d628a 100644 --- a/backend/ilotalo/views.py +++ b/backend/ilotalo/views.py @@ -476,7 +476,7 @@ def post(self, request): if not serializer.is_valid(): return Response(serializer.errors, status=status.HTTP_400_BAD_REQUEST) - + serializer.save(created_by=request.user) return Response(serializer.data, status=status.HTTP_201_CREATED) @@ -831,6 +831,7 @@ class DefectFaultView(viewsets.ReadOnlyModelViewSet): serializer_class = DefectFaultSerializer queryset = DefectFault.objects.all() + pagination_class = None class CreateDefectFaultView(APIView): @@ -995,6 +996,7 @@ class CleaningView(viewsets.ReadOnlyModelViewSet): serializer_class = CleaningSerializer queryset = Cleaning.objects.all() + pagination_class = None class CreateCleaningView(APIView): @@ -1088,6 +1090,7 @@ class CleaningSuppliesView(viewsets.ReadOnlyModelViewSet): serializer_class = CleaningSuppliesSerializer queryset = CleaningSupplies.objects.all() + pagination_class = None class CreateCleaningSuppliesView(APIView): diff --git a/frontend/src/pages/defectfaultpage.jsx b/frontend/src/pages/defectfaultpage.jsx index ad6f6731..3e2e633d 100644 --- a/frontend/src/pages/defectfaultpage.jsx +++ b/frontend/src/pages/defectfaultpage.jsx @@ -84,7 +84,7 @@ const DefectFault = () => { handleSnackbar(t("defectfixsuccess"), "success"); fetchDefects(); }) - .catch((error) => { + .catch(() => { handleSnackbar(t("defectfixfail"), "error"); }); }; @@ -96,7 +96,7 @@ const DefectFault = () => { handleSnackbar(t("defectmailsuccess"), "success"); fetchDefects(); }) - .catch((error) => { + .catch(() => { handleSnackbar(t("defectmailfail"), "error"); }); }; diff --git a/frontend/src/pages/ownpage.jsx b/frontend/src/pages/ownpage.jsx index b0d76459..da6c8a6b 100644 --- a/frontend/src/pages/ownpage.jsx +++ b/frontend/src/pages/ownpage.jsx @@ -217,7 +217,6 @@ const OwnPage = () => { try { const res = await organizationsAPI.organizationsWithKeys(); const rawData = res.data; - console.log("Fetched organizations:", rawData); const orgData = rawData.map((u) => ({ id: u.id, Organisaatio: u.name, From e096341ff92966199c99e3a2d1a8b19c734c4324 Mon Sep 17 00:00:00 2001 From: iPegii <51372604+iPegii@users.noreply.github.com> Date: Sun, 8 Feb 2026 13:14:50 +0200 Subject: [PATCH 093/149] fix: ownpage to show details of users when permissions high enough --- backend/ilotalo/views.py | 3 +++ 1 file changed, 3 insertions(+) diff --git a/backend/ilotalo/views.py b/backend/ilotalo/views.py index 910d628a..d3876960 100644 --- a/backend/ilotalo/views.py +++ b/backend/ilotalo/views.py @@ -59,6 +59,9 @@ class UserView(viewsets.ReadOnlyModelViewSet): def get_serializer_class(self): # Use minimal serializer for list action (used by YKV etc.) if self.action == 'list': + # Management roles can see all user details in the list + if self.request.user.is_authenticated and self.request.user.role in [LEPPISPJ, LEPPISVARAPJ, MUOKKAUS, JARJESTOPJ]: + return UserNoPasswordSerializer return UserMinimalSerializer return UserNoPasswordSerializer From ca86f87d67181f06b192c3940c346a97429aef69 Mon Sep 17 00:00:00 2001 From: iPegii <51372604+iPegii@users.noreply.github.com> Date: Sun, 8 Feb 2026 13:46:08 +0200 Subject: [PATCH 094/149] updating user information and password to different tabs --- backend/ilotalo/serializers.py | 31 +++++++++- frontend/src/components/UserPage.jsx | 86 ++++++++++++++++++++++------ frontend/src/pages/ownpage.jsx | 78 +++++++++++++++++++++---- frontend/src/translations.json | 12 ++++ 4 files changed, 176 insertions(+), 31 deletions(-) diff --git a/backend/ilotalo/serializers.py b/backend/ilotalo/serializers.py index 8f4b8c0c..77f446c3 100644 --- a/backend/ilotalo/serializers.py +++ b/backend/ilotalo/serializers.py @@ -173,12 +173,13 @@ class UserUpdateSerializer(serializers.ModelSerializer): """ keys = OrganizationSerializer(many=True, read_only=True) + current_password = serializers.CharField(write_only=True, required=False, allow_blank=True) class Meta: model = User fields = '__all__' extra_kwargs = { - 'password': {'write_only': True, 'required': False}, + 'password': {'write_only': True, 'required': False, 'allow_blank': True}, 'username': {'required': False}, 'email': {'required': False}, 'role': {'required': False}, @@ -234,18 +235,42 @@ def validate_telegram(self, tgname): "This telegram name is taken") return tgname + def validate(self, data): + """Validates password when updating a user.""" + current_password = data.get("current_password") + new_password = data.get("password") + + # Skip validation if no fields are actually being changed (might happen in some UI flows) + # but usually, we want to enforce current_password for any update to OwnPage fields. + if not self.instance: + return data + + # Check if the current password is correct + if not current_password or not self.instance.check_password(current_password): + raise exceptions.ValidationError({"current_password": "Invalid current password."}) + + if new_password: # If new password is provided + try: + validate_password(new_password) + except exceptions.ValidationError as e: + serializer_errors = serializers.as_serializer_error(e) + raise exceptions.ValidationError( + {"password": serializer_errors["non_field_errors"]} + ) + return data + def update(self, instance, validated_data): """Update the user instance with validated data.""" + instance.username = validated_data.get('username', instance.username) instance.email = validated_data.get('email', instance.email) instance.telegram = validated_data.get('telegram', instance.telegram) instance.role = validated_data.get('role', instance.role) instance.rights_for_reservation = validated_data.get( 'rights_for_reservation', instance.rights_for_reservation) - # Check if password is provided and update it if so + # Check if password is provided and not empty, and update it if so password = validated_data.get('password') if password: - validate_password(password) # Validate the password instance.set_password(password) instance.save() diff --git a/frontend/src/components/UserPage.jsx b/frontend/src/components/UserPage.jsx index d7045d7a..f54b3575 100644 --- a/frontend/src/components/UserPage.jsx +++ b/frontend/src/components/UserPage.jsx @@ -1,21 +1,74 @@ import React from "react"; -import { Button, TextField } from "@mui/material"; +import { + Button, + TextField, +} from "@mui/material"; import { useTranslation } from "react-i18next"; import { ROLE_DESCRIPTIONS } from "../roles"; const UserPage = ({ + mode = "info", // "info" or "password" username, setUsername, email, setEmail, + password, setPassword, + confirmPassword, setConfirmPassword, + currentPassword, + setCurrentPassword, telegram, setTelegram, handleUserDetails, role, }) => { const { t } = useTranslation(); + + if (mode === "password") { + return ( +
+

{t("changepassword")}

+
+ setCurrentPassword(e.target.value)} + fullWidth + /> + setPassword(e.target.value)} + fullWidth + /> + setConfirmPassword(e.target.value)} + fullWidth + /> + +
+
+ ); + } + return (

{t("owninfo")}

@@ -26,20 +79,6 @@ const UserPage = ({ value={username} onChange={(e) => setUsername(e.target.value)} /> - setPassword(e.target.value)} - /> - setConfirmPassword(e.target.value)} - /> setTelegram(e.target.value)} /> + + setCurrentPassword(e.target.value)} + sx={{ marginTop: "1em" }} + helperText={t("currentpassword_helper")} + /> + - {t("userrole")}: {ROLE_DESCRIPTIONS[role]} +
+ {t("userrole")}: {ROLE_DESCRIPTIONS[role]} +
); diff --git a/frontend/src/pages/ownpage.jsx b/frontend/src/pages/ownpage.jsx index da6c8a6b..bfe51f44 100644 --- a/frontend/src/pages/ownpage.jsx +++ b/frontend/src/pages/ownpage.jsx @@ -7,19 +7,47 @@ import CreateOrganization from "../components/CreateOrganization.jsx"; import AllUsers from "../components/AllUsers.jsx"; import updateaccountcheck from "../utils/updateaccountcheck.js"; import { useTranslation } from "react-i18next"; -import { Snackbar, Alert } from "@mui/material"; +import { Snackbar, Alert, Tabs, Tab, Box } from "@mui/material"; import { ROLE_DESCRIPTIONS } from "../roles.js"; +const TabPanel = (props) => { + const { children, value, index, ...other } = props; + + return ( + + ); +}; + const OwnPage = () => { const { user, setUser } = useStateContext(); const isLoggedIn = !!user; const [username, setUsername] = useState(user?.username || ""); const [password, setPassword] = useState(""); const [confirmPassword, setConfirmPassword] = useState(""); + const [currentPassword, setCurrentPassword] = useState(""); const [email, setEmail] = useState(user?.email || ""); const [telegram, setTelegram] = useState(user?.telegram || ""); const [role, setRole] = useState(user?.role || "5"); + const [tabValue, setTabValue] = useState(0); + + const handleTabChange = (event, newValue) => { + setTabValue(newValue); + }; + + // user_details* variables for viewing and updating someone else's information const [userDetailsUsername, setUserDetailsUsername] = useState(""); const [userDetailsPassword, setUserDetailsPassword] = useState(""); @@ -76,6 +104,7 @@ const OwnPage = () => { username: username, password: password, confirmPassword: confirmPassword, + current_password: currentPassword, email: email, telegram: telegram, }; @@ -87,6 +116,11 @@ const OwnPage = () => { return; } + if (!currentPassword) { + handleSnackbar(t("currentpasswordrequired"), "error"); + return; + } + try { // Validation is now handled by the backend if (password) { @@ -112,14 +146,22 @@ const OwnPage = () => { const updateResponse = await usersAPI.updateUser(user_id, details); setUser(updateResponse.data); - setUser(updateResponse.data); handleSnackbar(t("usereditsuccess"), "success"); await getAllUsers(); + + // Clear passwords after successful update + setPassword(""); + setConfirmPassword(""); + setCurrentPassword(""); } catch (error) { console.error(t("usereditfail"), error); // Handle specific validation errors from backend if (error.response && error.response.data) { const errors = error.response.data; + if (errors.current_password) { + handleSnackbar(t("invalidcurrentpassword"), "error"); + return; + } if (errors.email) { handleSnackbar(t("emailinuse"), "error"); return; @@ -135,9 +177,6 @@ const OwnPage = () => { } handleSnackbar(t("usereditfail"), "error"); } - - setPassword(""); - setConfirmPassword(""); }; const handleSnackbar = (message, severity) => { @@ -520,22 +559,39 @@ const OwnPage = () => {
- { + + + + + + + - } + + + + { Date: Sun, 8 Feb 2026 13:50:31 +0200 Subject: [PATCH 095/149] fix username label overlapping in modify user dialog --- frontend/src/components/AllUsers.jsx | 1 + 1 file changed, 1 insertion(+) diff --git a/frontend/src/components/AllUsers.jsx b/frontend/src/components/AllUsers.jsx index 1ac2b6be..40a3cd73 100644 --- a/frontend/src/components/AllUsers.jsx +++ b/frontend/src/components/AllUsers.jsx @@ -158,6 +158,7 @@ const AllUsers = ({ fullWidth sx={{ marginBottom: "1rem" }} // Add spacing below the field data-testid="username-input" + style={{ marginTop: "0.5em" }} /> Date: Sun, 8 Feb 2026 14:40:53 +0200 Subject: [PATCH 096/149] use role enums instead of integers for clarity --- .../src/components/CleaningSuppliesList.jsx | 3 +- frontend/src/components/DefectList.jsx | 4 +- frontend/src/components/YkvLogoutFunction.jsx | 8 ++-- frontend/src/pages/cleaningschedulepage.jsx | 2 +- frontend/src/pages/ownkeys.jsx | 3 +- frontend/src/pages/ownpage.jsx | 19 +++++---- frontend/src/roles.js | 42 ++++++++++++------- frontend/src/tests/cleaning.test.js | 3 +- .../src/tests/cleaningsuppliespage.test.js | 5 ++- frontend/src/tests/defectfaultpage.test.js | 3 +- frontend/src/tests/frontpage.test.js | 13 +++--- frontend/src/tests/ownkeys.test.js | 35 ++++++++-------- frontend/src/tests/ownpage.test.js | 19 ++++----- frontend/src/tests/reservations.test.js | 7 ++-- frontend/src/utils/createaccount.js | 2 +- frontend/src/utils/keyuserhelpers.js | 7 ++-- 16 files changed, 99 insertions(+), 76 deletions(-) diff --git a/frontend/src/components/CleaningSuppliesList.jsx b/frontend/src/components/CleaningSuppliesList.jsx index 544ff558..aba1384f 100644 --- a/frontend/src/components/CleaningSuppliesList.jsx +++ b/frontend/src/components/CleaningSuppliesList.jsx @@ -4,6 +4,7 @@ import DeleteIcon from '@mui/icons-material/DeleteOutlined'; import { useTranslation } from "react-i18next"; import { DataGrid } from '@mui/x-data-grid'; import { useStateContext } from '@context/ContextProvider'; +import { Role } from '../roles'; const CleaningSuppliesList = ({ allCleaningSupplies, handleDeleteClick }) => { @@ -33,7 +34,7 @@ const CleaningSuppliesList = ({ allCleaningSupplies, handleDeleteClick }) => { { field: "tool", headerName: t("cleaningtool"), width: 400 }, ]; - if (loggedUser && loggedUser.role === 1) { + if (loggedUser && loggedUser.role === Role.LEPPISPJ) { return ( { const { t } = useTranslation(); @@ -52,7 +54,7 @@ const DefectList = ({ allDefects, activeDefects, handleRepairClick, handleEmailC { field: "time", headerName: "Aika", width: 200 }, ]; - if (loggedUser && loggedUser.role === 1) { + if (loggedUser && loggedUser.role === Role.LEPPISPJ) { return ( - {loggedUser.role !== 5 && ( + {loggedUser.role !== Role.TAVALLINEN && (
)} - {loggedUser.role !== 1 && loggedUser.role !== 5 && ( + {loggedUser.role !== Role.LEPPISPJ && loggedUser.role !== Role.TAVALLINEN && (

{t("ownresps")}

)} - {loggedUser.role === 1 && ( + {loggedUser.role === Role.LEPPISPJ && (

{t("allresps")}

diff --git a/frontend/src/pages/cleaningschedulepage.jsx b/frontend/src/pages/cleaningschedulepage.jsx index e442c409..0bc44bca 100644 --- a/frontend/src/pages/cleaningschedulepage.jsx +++ b/frontend/src/pages/cleaningschedulepage.jsx @@ -175,7 +175,7 @@ const CleaningSchedule = () => {

{t("cleaningschedule")}

- {isLoggedIn && loggedUser.role === 1 && ( + {isLoggedIn && loggedUser.role === Role.LEPPISPJ && ( handleFormSubmit(newData)} /> { const { user: loggedUser } = useStateContext(); @@ -97,7 +98,7 @@ const OwnKeys = () => { // function that checks if the user logged in (if there are no responsibilities, the user cant be logged in either) function checkIfLoggedIn() { if (loggedUser) { - if (loggedUser.role !== 5) { + if (loggedUser.role !== Role.TAVALLINEN) { return true; } return false; diff --git a/frontend/src/pages/ownpage.jsx b/frontend/src/pages/ownpage.jsx index bfe51f44..96d2a560 100644 --- a/frontend/src/pages/ownpage.jsx +++ b/frontend/src/pages/ownpage.jsx @@ -8,7 +8,7 @@ import AllUsers from "../components/AllUsers.jsx"; import updateaccountcheck from "../utils/updateaccountcheck.js"; import { useTranslation } from "react-i18next"; import { Snackbar, Alert, Tabs, Tab, Box } from "@mui/material"; -import { ROLE_DESCRIPTIONS } from "../roles.js"; +import { ROLE_DESCRIPTIONS, Role } from "../roles.js"; const TabPanel = (props) => { const { children, value, index, ...other } = props; @@ -39,7 +39,8 @@ const OwnPage = () => { const [currentPassword, setCurrentPassword] = useState(""); const [email, setEmail] = useState(user?.email || ""); const [telegram, setTelegram] = useState(user?.telegram || ""); - const [role, setRole] = useState(user?.role || "5"); + const [role, setRole] = useState(user?.role || Role.TAVALLINEN); + const [tabValue, setTabValue] = useState(0); @@ -418,12 +419,12 @@ const OwnPage = () => { if (confirmUpdate) { usersAPI - .updateUser(selectedUserId, { role: 1 }) + .updateUser(selectedUserId, { role: Role.LEPPISPJ }) .then((response) => { console.log("Role updated successfully:", response.data); }); usersAPI - .updateUser(loggedUserId, { role: 5 }) + .updateUser(loggedUserId, { role: Role.TAVALLINEN }) .then((response) => { localStorage.setItem("loggedUser", JSON.stringify(response.data)); setUser(response.data); @@ -515,18 +516,18 @@ const OwnPage = () => { .getUserInfo() .then((response) => { const currentUser = response.data; - if (currentUser.role === 1) { + if (currentUser.role === Role.LEPPISPJ) { setHasPermission(true); setHasPermissionOrg(true); } else if ( - currentUser.role == 2 || - currentUser.role == 3 || - currentUser.role == 6 + currentUser.role == Role.LEPPISVARAPJ || + currentUser.role == Role.MUOKKAUS || + currentUser.role == Role.JARJESTOPJ ) { setHasPermissionOrg(true); setHasPermission(false); } else if (currentUser[0]) { - if (currentUser[0].role === 1) { + if (currentUser[0].role === Role.LEPPISPJ) { setHasPermission(true); setHasPermissionOrg(true); } diff --git a/frontend/src/roles.js b/frontend/src/roles.js index 4ac81e05..ea4b5eb4 100644 --- a/frontend/src/roles.js +++ b/frontend/src/roles.js @@ -1,19 +1,29 @@ +export const Role = { + LEPPISPJ: 1, + LEPPISVARAPJ: 2, + MUOKKAUS: 3, + AVAIMELLINEN: 4, + TAVALLINEN: 5, + JARJESTOPJ: 6, + JARJESTOVARAPJ: 7 +}; + export const ROLE_DESCRIPTIONS = { - 1: "LeppisPJ", - 2: "LeppisVaraPJ", - 3: "Muokkaus", - 4: "Avaimellinen", - 5: "Tavallinen", - 6: "JärjestöPJ", - 7: "JärjestöVaraPJ" - }; + [Role.LEPPISPJ]: "Leppis PJ", + [Role.LEPPISVARAPJ]: "Leppis Vara PJ", + [Role.MUOKKAUS]: "Muokkaus", + [Role.AVAIMELLINEN]: "Avaimellinen", + [Role.TAVALLINEN]: "Tavallinen", + [Role.JARJESTOPJ]: "Järjestön PJ", + [Role.JARJESTOVARAPJ]: "Järjestön Vara PJ" +}; export const ROLE_OPTIONS = [ - { value: 1, label: 'LeppisPJ' }, - { value: 2, label: 'LeppisVaraPJ' }, - { value: 3, label: 'Muokkaus' }, - { value: 4, label: 'Avaimellinen' }, - { value: 5, label: 'Tavallinen' }, - { value: 6, label: 'JärjestöPJ' }, - { value: 7, label: 'JärjestöVaraPJ' } - ]; \ No newline at end of file + { value: Role.LEPPISPJ, label: 'Leppis PJ' }, + { value: Role.LEPPISVARAPJ, label: 'Leppis Vara PJ' }, + { value: Role.MUOKKAUS, label: 'Muokkaus' }, + { value: Role.AVAIMELLINEN, label: 'Avaimellinen' }, + { value: Role.TAVALLINEN, label: 'Tavallinen' }, + { value: Role.JARJESTOPJ, label: 'Järjestön PJ' }, + { value: Role.JARJESTOVARAPJ, label: 'Järjestön Vara PJ' } +]; \ No newline at end of file diff --git a/frontend/src/tests/cleaning.test.js b/frontend/src/tests/cleaning.test.js index 36f2091a..c1591057 100644 --- a/frontend/src/tests/cleaning.test.js +++ b/frontend/src/tests/cleaning.test.js @@ -5,6 +5,7 @@ import CleanersList from '../components/CleanersList.jsx'; import mockAxios from "../../__mocks__/axios"; import { ContextProvider } from "@context/ContextProvider"; import "@testing-library/jest-dom"; +import { Role } from '../roles'; localStorage.setItem("lang", "fi") @@ -12,7 +13,7 @@ const user = { username: "example_username", email: "example_email@example.com", telegram: "example_telegram", - role: 1, + role: Role.LEPPISPJ, rights_for_reservation: true, id: 1, }; diff --git a/frontend/src/tests/cleaningsuppliespage.test.js b/frontend/src/tests/cleaningsuppliespage.test.js index 5e4a5a96..a971c576 100644 --- a/frontend/src/tests/cleaningsuppliespage.test.js +++ b/frontend/src/tests/cleaningsuppliespage.test.js @@ -9,6 +9,7 @@ import "@testing-library/jest-dom"; import CleaningSupplies from "../../src/pages/cleaningsuppliespage.jsx"; import mockAxios from "../../__mocks__/axios"; import { ContextProvider } from "@context/ContextProvider"; +import { Role } from '../../src/roles'; localStorage.setItem("lang", "fi"); @@ -32,7 +33,7 @@ describe("Cleaningsupplies Component", () => { username: "superman", email: "superman@example.com", telegram: "super_telegram", - role: 1, + role: Role.LEPPISPJ, keys: { "tko-äly": true }, organization: { "tko-äly": true }, rights_for_reservation: true, @@ -95,7 +96,7 @@ describe("Cleaningsupplies Component", () => { username: "superman", email: "superman@example.com", telegram: "super_telegram", - role: 1, + role: Role.LEPPISPJ, keys: { "tko-äly": true }, organization: { "tko-äly": true }, rights_for_reservation: true, diff --git a/frontend/src/tests/defectfaultpage.test.js b/frontend/src/tests/defectfaultpage.test.js index ccf468d2..87e2bb4c 100644 --- a/frontend/src/tests/defectfaultpage.test.js +++ b/frontend/src/tests/defectfaultpage.test.js @@ -9,6 +9,7 @@ import "@testing-library/jest-dom"; import DefectFault from "../../src/pages/defectfaultpage"; import mockAxios from "../../__mocks__/axios"; import { ContextProvider } from "@context/ContextProvider"; +import { Role } from '../../src/roles'; localStorage.setItem("lang", "fi") @@ -32,7 +33,7 @@ describe("DefectFault Component", () => { username: "example_username", email: "example_email@example.com", telegram: "example_telegram", - role: 1, + role: Role.LEPPISPJ, keys: { "tko-äly": true }, organization: { "tko-äly": true }, rights_for_reservation: true, diff --git a/frontend/src/tests/frontpage.test.js b/frontend/src/tests/frontpage.test.js index e73e4249..33d9b769 100644 --- a/frontend/src/tests/frontpage.test.js +++ b/frontend/src/tests/frontpage.test.js @@ -9,6 +9,7 @@ import "@testing-library/jest-dom"; import FrontPage from "../../src/pages/frontpage"; import i18n from "../i18n.js"; import mockAxios from "../../__mocks__/axios"; +import { Role } from '../../src/roles'; localStorage.setItem("lang", "fi"); @@ -52,7 +53,7 @@ test("renders upcoming events", async () => { username: "leppis", email: "leppis@testi.com", telegram: "", - role: 1, + role: Role.LEPPISPJ, rights_for_reservation: false, keys: [1], }, @@ -74,7 +75,7 @@ test("renders upcoming events", async () => { username: "leppis", email: "leppis@testi.com", telegram: "", - role: 1, + role: Role.LEPPISPJ, rights_for_reservation: false, keys: [1], }, @@ -111,7 +112,7 @@ test("renders upcoming events", async () => { username: "leppis", email: "leppis@testi.com", telegram: "", - role: 1, + role: Role.LEPPISPJ, rights_for_reservation: false, keys: [1], }, @@ -133,7 +134,7 @@ test("renders upcoming events", async () => { username: "leppis", email: "leppis@testi.com", telegram: "", - role: 1, + role: Role.LEPPISPJ, rights_for_reservation: false, keys: [1], }, @@ -198,7 +199,7 @@ test("event description dialog works correctly", async () => { username: "leppis", email: "leppis@testi.com", telegram: "", - role: 1, + role: Role.LEPPISPJ, rights_for_reservation: false, keys: [1], }, @@ -220,7 +221,7 @@ test("event description dialog works correctly", async () => { username: "leppis", email: "leppis@testi.com", telegram: "", - role: 1, + role: Role.LEPPISPJ, rights_for_reservation: false, keys: [1], }, diff --git a/frontend/src/tests/ownkeys.test.js b/frontend/src/tests/ownkeys.test.js index 0a36d6da..c52f7be9 100644 --- a/frontend/src/tests/ownkeys.test.js +++ b/frontend/src/tests/ownkeys.test.js @@ -9,6 +9,7 @@ import "@testing-library/jest-dom"; import OwnKeys from "../../src/pages/ownkeys"; import { ContextProvider } from "@context/ContextProvider"; import mockAxios from "../../__mocks__/axios"; +import { Role } from '../../src/roles'; localStorage.setItem("lang", "fi") @@ -32,7 +33,7 @@ describe("OwnKeys Component", () => { username: "example_username", email: "example_email@example.com", telegram: "example_telegram", - role: 1, + role: Role.LEPPISPJ, keys: { "tko-äly": true }, organization: { "tko-äly": true }, rights_for_reservation: true, @@ -76,7 +77,7 @@ describe("OwnKeys Component", () => { username: "example_username", email: "example_email@example.com", telegram: "example_telegram", - role: 1, + role: Role.LEPPISPJ, keys: { "tko-äly": true }, organization: { "tko-äly": true }, rights_for_reservation: true, @@ -145,7 +146,7 @@ describe("OwnKeys Component", () => { username: "example_username", email: "example_email@example.com", telegram: "example_telegram", - role: 1, + role: Role.LEPPISPJ, keys: { "tko-äly": true }, organization: { "tko-äly": true }, rights_for_reservation: true, @@ -175,7 +176,7 @@ describe("OwnKeys Component", () => { username: "example_username", email: "example_email@example.com", telegram: "telegram", - role: 1, + role: Role.LEPPISPJ, keys: [1], }, ], @@ -197,7 +198,7 @@ describe("OwnKeys Component", () => { username: "example_username", email: "example_email@example.com", telegram: "telegram", - role: 1, + role: Role.LEPPISPJ, keys: [1], }, ], @@ -211,7 +212,7 @@ describe("OwnKeys Component", () => { username: "example_username", email: "example_username@example.com", telegram: "telegram", - role: 1, + role: Role.LEPPISPJ, }, responsible_for: "fuksit", login_time: "2024-05-30T09:38:07.170043Z", @@ -232,7 +233,7 @@ describe("OwnKeys Component", () => { username: "example_username", email: "example_email@example.com", telegram: "telegram", - role: 1, + role: Role.LEPPISPJ, keys: [1], }, ], @@ -254,7 +255,7 @@ describe("OwnKeys Component", () => { username: "example_username", email: "example_email@example.com", telegram: "telegram", - role: 1, + role: Role.LEPPISPJ, keys: [1], }, ], @@ -268,7 +269,7 @@ describe("OwnKeys Component", () => { username: "example_username", email: "example_email@example.com", telegram: "telegram", - role: 1, + role: Role.LEPPISPJ, }, responsible_for: "gary", login_time: "2024-05-30T09:59:11.497510Z", @@ -293,7 +294,7 @@ describe("OwnKeys Component", () => { username: "example_username", email: "example_email@example.com", telegram: "telegram", - role: 1, + role: Role.LEPPISPJ, keys: [1], }, ], @@ -307,7 +308,7 @@ describe("OwnKeys Component", () => { username: "example_username", email: "example_email@example.com", telegram: "telegram", - role: 1, + role: Role.LEPPISPJ, }, ], }; @@ -338,12 +339,12 @@ describe("OwnKeys Component", () => { }); }); - // it("time filtering works", async () => { - // const user = { - // username: "example_username", - // email: "example_email@example.com", - // telegram: "example_telegram", - // role: 1, + //it("time filtering works", async () => { + // const user = { + // username: "example_username", + // email: "example_email@example.com", + // telegram: "example_telegram", + // role: Role.LEPPISPJ, // keys: {"tko-äly": true}, // organization: {"tko-äly": true}, // rights_for_reservation: true, diff --git a/frontend/src/tests/ownpage.test.js b/frontend/src/tests/ownpage.test.js index 71e88fff..5c57e22e 100644 --- a/frontend/src/tests/ownpage.test.js +++ b/frontend/src/tests/ownpage.test.js @@ -34,14 +34,13 @@ describe("OwnPage Component", () => { }); }); -it("opens with role 5", async () => { - const user = { - username: "example_username", - email: "example_email@example.com", - telegram: "example_telegram", - role: 5, - }; - localStorage.setItem("loggedUser", JSON.stringify(user)); + it("opens with role 5", async () => { + const user = { + username: "example_username", + email: "example_email@example.com", + telegram: "example_telegram", + role: Role.TAVALLINEN, + }; localStorage.setItem("loggedUser", JSON.stringify(user)); localStorage.setItem("ACCESS_TOKEN", "example_token"); const { getByText, getByLabelText } = render( @@ -148,7 +147,7 @@ it("User updating works", async () => { confirmPassword: "example_password123", email: "example_email@example.com", telegram: "example_telegram", - role: 5, + role: Role.TAVALLINEN, id: 1, }; window.confirm = jest.fn(() => true); @@ -369,7 +368,7 @@ describe("Organizations", () => { username: "leppis", email: "leppis@testi.com", telegram: "", - role: 1, + role: Role.LEPPISPJ, id: 1, }; localStorage.setItem("ACCESS_TOKEN", "example_token"); diff --git a/frontend/src/tests/reservations.test.js b/frontend/src/tests/reservations.test.js index 0b7d7529..669149ce 100644 --- a/frontend/src/tests/reservations.test.js +++ b/frontend/src/tests/reservations.test.js @@ -9,6 +9,7 @@ import { act } from "react"; import Reservations from "../../src/pages/reservations"; import mockAxios from "../../__mocks__/axios.js"; import { ContextProvider } from "@context/ContextProvider"; +import { Role } from '../../src/roles'; localStorage.setItem("lang", "fi") @@ -21,7 +22,7 @@ const user = { username: "example_username", email: "example_email@example.com", telegram: "example_telegram", - role: 1, + role: Role.LEPPISPJ, keys: { "tko-äly": true }, organization: { "tko-äly": true }, rights_for_reservation: true, @@ -70,7 +71,7 @@ describe("Reservations component", () => { username: 'example_username', email: 'example_email@example.com', telegram: 'example_telegram', - role: 1, + role: Role.LEPPISPJ, rights_for_reservation: true }; @@ -126,7 +127,7 @@ describe("Reservations component", () => { // username: 'example_username', // email: 'example_email@example.com', // telegram: 'example_telegram', - // role: 1, + // role: Role.LEPPISPJ, // keys: {}, // organization: {}, // rights_for_reservation: true diff --git a/frontend/src/utils/createaccount.js b/frontend/src/utils/createaccount.js index e083c749..b51744d8 100644 --- a/frontend/src/utils/createaccount.js +++ b/frontend/src/utils/createaccount.js @@ -21,7 +21,7 @@ const createaccount = ({ password, email, telegram, - role: 5, + role: Role.TAVALLINEN, organization: null, keys: null, recaptcha_response: recaptchaResponse diff --git a/frontend/src/utils/keyuserhelpers.js b/frontend/src/utils/keyuserhelpers.js index 677468ad..d8f831c4 100644 --- a/frontend/src/utils/keyuserhelpers.js +++ b/frontend/src/utils/keyuserhelpers.js @@ -1,4 +1,5 @@ import { authAPI, usersAPI } from "../api/api.ts"; +import { Role } from "../roles.js"; export const getPermission = async ({ setHasPermission }) => { /* @@ -10,10 +11,10 @@ export const getPermission = async ({ setHasPermission }) => { .getUserInfo() .then((response) => { const currentUser = response.data; - if (currentUser.role === 1) { + if (currentUser.role === Role.LEPPISPJ) { setHasPermission(true); } else if (currentUser[0]) { - if (currentUser[0].role === 1) { + if (currentUser[0].role === Role.LEPPISPJ) { setHasPermission(true); } } else { @@ -41,7 +42,7 @@ export const fetchAllUsersWithKeys = async ({ // check if a user is valid for making an YKV-login const checkUser = (user, loggedUser) => { - if (user.role === 5) { + if (user.role === Role.TAVALLINEN) { return false; } if (user.id === loggedUser.id) { From b29dc16056d7420d0a07e3bd6545d101d079a20b Mon Sep 17 00:00:00 2001 From: iPegii <51372604+iPegii@users.noreply.github.com> Date: Sun, 8 Feb 2026 14:41:11 +0200 Subject: [PATCH 097/149] Add tooltip to explain the permission levels --- frontend/src/components/AllUsers.jsx | 10 +++++++++- frontend/src/translations.json | 2 ++ 2 files changed, 11 insertions(+), 1 deletion(-) diff --git a/frontend/src/components/AllUsers.jsx b/frontend/src/components/AllUsers.jsx index 40a3cd73..480e719a 100644 --- a/frontend/src/components/AllUsers.jsx +++ b/frontend/src/components/AllUsers.jsx @@ -19,6 +19,9 @@ import { ROLE_OPTIONS } from "../roles.js"; import InputLabel from "@mui/material/InputLabel"; import Select from "@mui/material/Select"; import MenuItem from "@mui/material/MenuItem"; +import Tooltip from "@mui/material/Tooltip"; +import InfoOutlinedIcon from "@mui/icons-material/InfoOutlined"; +import Box from "@mui/material/Box"; const AllUsers = ({ allUsers, @@ -194,7 +197,12 @@ const AllUsers = ({ sx={{ marginBottom: "1rem" }} // Add spacing below the field data-testid="telegram-input" /> - {t("role")} + + {t("role")} + {t("role_info")}} arrow> + + +