From 8da87e8baa46338a3d3497783be885b4d0534167 Mon Sep 17 00:00:00 2001 From: Jason Keo <163387808+jasonkeo@users.noreply.github.com> Date: Sat, 22 Nov 2025 07:36:56 +0000 Subject: [PATCH 01/26] Create & Migrate Room Model --- server/api/room/__init__.py | 0 server/api/room/admin.py | 3 +++ server/api/room/apps.py | 6 +++++ server/api/room/migrations/0001_initial.py | 28 ++++++++++++++++++++++ server/api/room/migrations/__init__.py | 0 server/api/room/models.py | 18 ++++++++++++++ server/api/room/tests.py | 3 +++ server/api/room/urls.py | 6 +++++ server/api/room/views.py | 3 +++ server/api/settings.py | 1 + 10 files changed, 68 insertions(+) create mode 100644 server/api/room/__init__.py create mode 100644 server/api/room/admin.py create mode 100644 server/api/room/apps.py create mode 100644 server/api/room/migrations/0001_initial.py create mode 100644 server/api/room/migrations/__init__.py create mode 100644 server/api/room/models.py create mode 100644 server/api/room/tests.py create mode 100644 server/api/room/urls.py create mode 100644 server/api/room/views.py diff --git a/server/api/room/__init__.py b/server/api/room/__init__.py new file mode 100644 index 0000000..e69de29 diff --git a/server/api/room/admin.py b/server/api/room/admin.py new file mode 100644 index 0000000..8c38f3f --- /dev/null +++ b/server/api/room/admin.py @@ -0,0 +1,3 @@ +from django.contrib import admin + +# Register your models here. diff --git a/server/api/room/apps.py b/server/api/room/apps.py new file mode 100644 index 0000000..ac48e88 --- /dev/null +++ b/server/api/room/apps.py @@ -0,0 +1,6 @@ +from django.apps import AppConfig + + +class RoomConfig(AppConfig): + default_auto_field = "django.db.models.BigAutoField" + name = "api.room" diff --git a/server/api/room/migrations/0001_initial.py b/server/api/room/migrations/0001_initial.py new file mode 100644 index 0000000..1a14459 --- /dev/null +++ b/server/api/room/migrations/0001_initial.py @@ -0,0 +1,28 @@ +# Generated by Django 5.1.12 on 2025-11-22 07:36 + +from django.db import migrations, models + + +class Migration(migrations.Migration): + + initial = True + + dependencies = [] + + operations = [ + migrations.CreateModel( + name="Room", + fields=[ + ("id", models.AutoField(primary_key=True, serialize=False)), + ("name", models.TextField()), + ("img_url", models.TextField()), + ("location_id", models.IntegerField()), + ("capacity_id", models.IntegerField()), + ("start_datetime", models.DateTimeField()), + ("end_datetime", models.DateTimeField()), + ("recurrence_rule", models.TextField()), + ("created_at", models.DateTimeField()), + ("updated_at", models.DateTimeField()), + ], + ), + ] diff --git a/server/api/room/migrations/__init__.py b/server/api/room/migrations/__init__.py new file mode 100644 index 0000000..e69de29 diff --git a/server/api/room/models.py b/server/api/room/models.py new file mode 100644 index 0000000..08d227e --- /dev/null +++ b/server/api/room/models.py @@ -0,0 +1,18 @@ +from django.db import models + + +class Room(models.Model): + id = models.AutoField(primary_key=True) + name = models.TextField() + img_url = models.TextField() + location_id = models.IntegerField() + capacity_id = models.IntegerField() + start_datetime = models.DateTimeField() + end_datetime = models.DateTimeField() + recurrence_rule = models.TextField() + created_at = models.DateTimeField() + updated_at = models.DateTimeField() + + def __str__(self): + return self.name +# Create your models here. diff --git a/server/api/room/tests.py b/server/api/room/tests.py new file mode 100644 index 0000000..7ce503c --- /dev/null +++ b/server/api/room/tests.py @@ -0,0 +1,3 @@ +from django.test import TestCase + +# Create your tests here. diff --git a/server/api/room/urls.py b/server/api/room/urls.py new file mode 100644 index 0000000..99b012d --- /dev/null +++ b/server/api/room/urls.py @@ -0,0 +1,6 @@ +from django.urls import path +from . import views + +app_name = "room" +urlpatterns = [ +] diff --git a/server/api/room/views.py b/server/api/room/views.py new file mode 100644 index 0000000..91ea44a --- /dev/null +++ b/server/api/room/views.py @@ -0,0 +1,3 @@ +from django.shortcuts import render + +# Create your views here. diff --git a/server/api/settings.py b/server/api/settings.py index a135fe0..467775c 100644 --- a/server/api/settings.py +++ b/server/api/settings.py @@ -56,6 +56,7 @@ "api.healthcheck", "storages", "api.user", + "api.room", ] MIDDLEWARE = [ From 3d429db54d9874e885475ce0ed1a2ac966b1f5ce Mon Sep 17 00:00:00 2001 From: Jason Keo <163387808+jasonkeo@users.noreply.github.com> Date: Sat, 22 Nov 2025 07:46:04 +0000 Subject: [PATCH 02/26] Add Serializer --- server/api/room/serializers.py | 8 ++++++++ 1 file changed, 8 insertions(+) create mode 100644 server/api/room/serializers.py diff --git a/server/api/room/serializers.py b/server/api/room/serializers.py new file mode 100644 index 0000000..04a9788 --- /dev/null +++ b/server/api/room/serializers.py @@ -0,0 +1,8 @@ +from rest_framework import serializers +from .models import Room + + +class RoomSerializer(serializers.ModelSerializer): + class Meta: + model = Room + fields = "__all__" From a7d522c328a75923ede9f1faa41a8304e0aa3977 Mon Sep 17 00:00:00 2001 From: Jason Keo <163387808+jasonkeo@users.noreply.github.com> Date: Sun, 7 Dec 2025 15:21:08 +0000 Subject: [PATCH 03/26] rebase --- server/api/room/admin.py | 7 ++- ...r_room_created_at_alter_room_updated_at.py | 23 ++++++++++ server/api/room/models.py | 4 +- server/api/room/tests.py | 35 +++++++++++++- server/api/room/urls.py | 11 +++-- server/api/room/views.py | 46 ++++++++++++++++++- server/api/urls.py | 4 ++ 7 files changed, 118 insertions(+), 12 deletions(-) create mode 100644 server/api/room/migrations/0002_alter_room_created_at_alter_room_updated_at.py diff --git a/server/api/room/admin.py b/server/api/room/admin.py index 8c38f3f..8e318b0 100644 --- a/server/api/room/admin.py +++ b/server/api/room/admin.py @@ -1,3 +1,8 @@ from django.contrib import admin - +from .models import Room # Register your models here. +@admin.register(Room) +class RoomAdmin(admin.ModelAdmin): + list_display = ("id", "name", "location_id", "capacity_id", "start_datetime", "end_datetime") + search_fields = ("name",) + \ No newline at end of file diff --git a/server/api/room/migrations/0002_alter_room_created_at_alter_room_updated_at.py b/server/api/room/migrations/0002_alter_room_created_at_alter_room_updated_at.py new file mode 100644 index 0000000..cbef0de --- /dev/null +++ b/server/api/room/migrations/0002_alter_room_created_at_alter_room_updated_at.py @@ -0,0 +1,23 @@ +# Generated by Django 5.1.12 on 2025-12-03 08:41 + +from django.db import migrations, models + + +class Migration(migrations.Migration): + + dependencies = [ + ("room", "0001_initial"), + ] + + operations = [ + migrations.AlterField( + model_name="room", + name="created_at", + field=models.DateTimeField(auto_now_add=True), + ), + migrations.AlterField( + model_name="room", + name="updated_at", + field=models.DateTimeField(auto_now=True), + ), + ] diff --git a/server/api/room/models.py b/server/api/room/models.py index 08d227e..0862f9a 100644 --- a/server/api/room/models.py +++ b/server/api/room/models.py @@ -10,8 +10,8 @@ class Room(models.Model): start_datetime = models.DateTimeField() end_datetime = models.DateTimeField() recurrence_rule = models.TextField() - created_at = models.DateTimeField() - updated_at = models.DateTimeField() + created_at = models.DateTimeField(auto_now_add=True) + updated_at = models.DateTimeField(auto_now=True) def __str__(self): return self.name diff --git a/server/api/room/tests.py b/server/api/room/tests.py index 7ce503c..c2d9eb4 100644 --- a/server/api/room/tests.py +++ b/server/api/room/tests.py @@ -1,3 +1,34 @@ -from django.test import TestCase +from rest_framework.test import APITestCase +from rest_framework import status +from django.contrib.auth.models import User + +class RoomAPITest(APITestCase): + def setUp(self): + self.admin = User.objects.create_superuser("admin", "admin@test.com", "pass") + # ok i need test case basically post bunch of rooms + # then i need to filter with get requests like name, location, capacity, or date). + # then get then by id + # then update + # check with get response + #then delete + def test_list_rooms(self): + response = self.client.get("/api/rooms/") + self.assertEqual(response.status_code, status.HTTP_200_OK) + + def test_create_room_admin(self): + + self.client.login(username="admin", password="pass") + + data = { + "name": "Meeting Room A", + "img_url": "https://example.com/roomA.jpg", + "location_id": 1, + "capacity_id": 2, + "start_datetime": "2025-11-01T09:00:00Z", + "end_datetime": "2025-11-01T18:00:00Z", + "recurrence_rule": "FREQ=WEEKLY;BYDAY=MO,WE,FR" + } + + response = self.client.post("/api/rooms/", data, format="json") + self.assertEqual(response.status_code, status.HTTP_201_CREATED) -# Create your tests here. diff --git a/server/api/room/urls.py b/server/api/room/urls.py index 99b012d..97fa2b5 100644 --- a/server/api/room/urls.py +++ b/server/api/room/urls.py @@ -1,6 +1,7 @@ -from django.urls import path -from . import views +from rest_framework.routers import DefaultRouter +from .views import RoomViewSet -app_name = "room" -urlpatterns = [ -] +router = DefaultRouter() +router.register(r'', RoomViewSet, basename='rooms') + +urlpatterns = router.urls diff --git a/server/api/room/views.py b/server/api/room/views.py index 91ea44a..620b3b0 100644 --- a/server/api/room/views.py +++ b/server/api/room/views.py @@ -1,3 +1,45 @@ -from django.shortcuts import render +from rest_framework import viewsets, permissions +from rest_framework.response import Response +from .models import Room +from .serializers import RoomSerializer -# Create your views here. +class RoomViewSet(viewsets.ModelViewSet): + queryset = Room.objects.all() + serializer_class = RoomSerializer + + def get_permissions(self): + if self.action in ["create", "update", "destroy"]: + return [permissions.IsAdminUser()] + return [permissions.AllowAny()] + + def get_queryset(self): + qs = super().get_queryset() + params = self.request.query_params + + if name := params.get("name"): + qs = qs.filter(name__icontains=name) + + if loc := params.get("location_id"): + qs = qs.filter(location_id=loc) + + if cap := params.get("capacity_id"): + qs = qs.filter(capacity_id=cap) + + return qs + + def update(self, request, *args, **kwargs): + instance = self.get_object() + serializer = self.get_serializer(instance, data=request.data, partial=True) + serializer.is_valid(raise_exception=True) + serializer.save() + + return Response({ + "id": instance.id, + "name": instance.name, + "updated_at": instance.updated_at + }) + + def destroy(self, request, *args, **kwargs): + instance = self.get_object() + instance.delete() + return Response({"message": "Room deleted successfully"}) diff --git a/server/api/urls.py b/server/api/urls.py index 8ce33d9..b7e8206 100644 --- a/server/api/urls.py +++ b/server/api/urls.py @@ -27,10 +27,14 @@ urlpatterns = [ path("admin/", admin.site.urls), path("api/healthcheck/", include(("api.healthcheck.urls"))), +<<<<<<< HEAD path("api/schema/", SpectacularAPIView.as_view(), name="schema"), path("swagger/", SpectacularSwaggerView.as_view(url_name="schema"), name="swagger-ui"), path("redoc/", SpectacularRedocView.as_view(url_name="schema"), name="redoc"), path("api/users/", include(("api.user.urls"))), +======= + path("api/rooms/", include(("api.room.urls"))), +>>>>>>> 49af123 (add Tests & First Draft of Api) ] From 805e9bdffe38015998918f9bf4a66174cde67539 Mon Sep 17 00:00:00 2001 From: Jason Keo <163387808+jasonkeo@users.noreply.github.com> Date: Wed, 3 Dec 2025 09:04:42 +0000 Subject: [PATCH 04/26] Add more test cases --- server/api/room/tests.py | 77 ++++++++++++++++++++++++++++++---------- 1 file changed, 58 insertions(+), 19 deletions(-) diff --git a/server/api/room/tests.py b/server/api/room/tests.py index c2d9eb4..6fe4c4c 100644 --- a/server/api/room/tests.py +++ b/server/api/room/tests.py @@ -1,34 +1,73 @@ from rest_framework.test import APITestCase from rest_framework import status from django.contrib.auth.models import User +from .models import Room +from django.utils import timezone +from datetime import timedelta class RoomAPITest(APITestCase): def setUp(self): + # Create admin user self.admin = User.objects.create_superuser("admin", "admin@test.com", "pass") - # ok i need test case basically post bunch of rooms - # then i need to filter with get requests like name, location, capacity, or date). - # then get then by id - # then update - # check with get response - #then delete + + # Login as admin for all POST/PUT/DELETE actions + self.client.login(username="admin", password="pass") + + for i in range(5): + start = timezone.make_aware(timezone.datetime(2025, 11, 1, 9, 0)) + timedelta(days=i) + end = timezone.make_aware(timezone.datetime(2025, 11, 1, 18, 0)) + timedelta(days=i) + + Room.objects.create( + name=f"Meeting Room {chr(65+i)}", + img_url=f"https://example.com/room{chr(65+i)}.jpg", + location_id=(i % 2) + 1, # 1 or 2 + capacity_id=(i % 3) + 1, # 1,2,3 + start_datetime=start, + end_datetime=end, + recurrence_rule="FREQ=WEEKLY;BYDAY=MO,WE,FR" + ) + def test_list_rooms(self): + # GET all rooms response = self.client.get("/api/rooms/") self.assertEqual(response.status_code, status.HTTP_200_OK) + self.assertEqual(len(response.data), 5) - def test_create_room_admin(self): + # Filter by name + response = self.client.get("/api/rooms/?name=Room A") + self.assertEqual(len(response.data), 1) + self.assertEqual(response.data[0]["name"], "Meeting Room A") - self.client.login(username="admin", password="pass") + # Filter by location_id + response = self.client.get("/api/rooms/?location_id=1") + self.assertTrue(all(r["location_id"] == 1 for r in response.data)) + + # Filter by capacity_id + response = self.client.get("/api/rooms/?capacity_id=2") + self.assertTrue(all(r["capacity_id"] == 2 for r in response.data)) - data = { - "name": "Meeting Room A", - "img_url": "https://example.com/roomA.jpg", - "location_id": 1, - "capacity_id": 2, - "start_datetime": "2025-11-01T09:00:00Z", - "end_datetime": "2025-11-01T18:00:00Z", - "recurrence_rule": "FREQ=WEEKLY;BYDAY=MO,WE,FR" - } - response = self.client.post("/api/rooms/", data, format="json") - self.assertEqual(response.status_code, status.HTTP_201_CREATED) + def test_retrieve_update_delete_room(self): + # Get a room + room = Room.objects.first() + response = self.client.get(f"/api/rooms/{room.id}/") + self.assertEqual(response.status_code, status.HTTP_200_OK) + self.assertEqual(response.data["name"], room.name) + + # Update room + response = self.client.patch(f"/api/rooms/{room.id}/", {"name": "Updated Room"}, format="json") + self.assertEqual(response.status_code, status.HTTP_200_OK) + self.assertEqual(response.data["name"], "Updated Room") + + # Check via GET + response = self.client.get(f"/api/rooms/{room.id}/") + self.assertEqual(response.data["name"], "Updated Room") + # Delete room + response = self.client.delete(f"/api/rooms/{room.id}/") + self.assertEqual(response.status_code, status.HTTP_200_OK) + self.assertEqual(response.data["message"], "Room deleted successfully") + + # Check room count + response = self.client.get("/api/rooms/") + self.assertEqual(len(response.data), 4) From 94c9cf2cee9a4d92ff3202879cd45011301bd8da Mon Sep 17 00:00:00 2001 From: Jason Keo <163387808+jasonkeo@users.noreply.github.com> Date: Wed, 3 Dec 2025 09:07:59 +0000 Subject: [PATCH 05/26] Add comments to explain api --- server/api/room/views.py | 7 +++++++ 1 file changed, 7 insertions(+) diff --git a/server/api/room/views.py b/server/api/room/views.py index 620b3b0..f7560f8 100644 --- a/server/api/room/views.py +++ b/server/api/room/views.py @@ -3,6 +3,13 @@ from .models import Room from .serializers import RoomSerializer +# Viewset is library that provides CRUD operations for api +# Admin have create update delete permissions everyone can read +# get request can filter by name, location_id, capacity_id for get + +# per issue thing: +# Update has custom response with id name updated_at +# Delete has custom response message class RoomViewSet(viewsets.ModelViewSet): queryset = Room.objects.all() serializer_class = RoomSerializer From 63aac9224fe006a0912b040f60ce88b0b23469a3 Mon Sep 17 00:00:00 2001 From: Jason Keo <163387808+jasonkeo@users.noreply.github.com> Date: Wed, 3 Dec 2025 09:38:56 +0000 Subject: [PATCH 06/26] added location + amenties table --- ...menties_location_room_amenties_and_more.py | 40 +++++++++++++++++++ server/api/room/models.py | 17 +++++++- server/api/room/tests.py | 18 +++++++-- 3 files changed, 70 insertions(+), 5 deletions(-) create mode 100644 server/api/room/migrations/0003_amenties_location_room_amenties_and_more.py diff --git a/server/api/room/migrations/0003_amenties_location_room_amenties_and_more.py b/server/api/room/migrations/0003_amenties_location_room_amenties_and_more.py new file mode 100644 index 0000000..df526e2 --- /dev/null +++ b/server/api/room/migrations/0003_amenties_location_room_amenties_and_more.py @@ -0,0 +1,40 @@ +# Generated by Django 5.1.12 on 2025-12-03 09:31 + +import django.db.models.deletion +from django.db import migrations, models + + +class Migration(migrations.Migration): + + dependencies = [ + ("room", "0002_alter_room_created_at_alter_room_updated_at"), + ] + + operations = [ + migrations.CreateModel( + name="Amenties", + fields=[ + ("id", models.AutoField(primary_key=True, serialize=False)), + ("name", models.TextField()), + ], + ), + migrations.CreateModel( + name="Location", + fields=[ + ("id", models.AutoField(primary_key=True, serialize=False)), + ("name", models.TextField()), + ], + ), + migrations.AddField( + model_name="room", + name="amenties", + field=models.ManyToManyField(blank=True, to="room.amenties"), + ), + migrations.AlterField( + model_name="room", + name="location_id", + field=models.ForeignKey( + on_delete=django.db.models.deletion.CASCADE, to="room.location" + ), + ), + ] diff --git a/server/api/room/models.py b/server/api/room/models.py index 0862f9a..d7d7a73 100644 --- a/server/api/room/models.py +++ b/server/api/room/models.py @@ -1,12 +1,25 @@ from django.db import models +class Location(models.Model): + id = models.AutoField(primary_key=True) + name = models.TextField() + + def __str__(self): + return self.name +class Amenties(models.Model): + id = models.AutoField(primary_key=True) + name = models.TextField() + + def __str__(self): + return self.name class Room(models.Model): id = models.AutoField(primary_key=True) name = models.TextField() img_url = models.TextField() - location_id = models.IntegerField() + location_id = models.ForeignKey(Location, on_delete=models.CASCADE) capacity_id = models.IntegerField() + amenties = models.ManyToManyField(Amenties, blank=True) start_datetime = models.DateTimeField() end_datetime = models.DateTimeField() recurrence_rule = models.TextField() @@ -16,3 +29,5 @@ class Room(models.Model): def __str__(self): return self.name # Create your models here. + + diff --git a/server/api/room/tests.py b/server/api/room/tests.py index 6fe4c4c..a665244 100644 --- a/server/api/room/tests.py +++ b/server/api/room/tests.py @@ -1,7 +1,7 @@ from rest_framework.test import APITestCase from rest_framework import status from django.contrib.auth.models import User -from .models import Room +from .models import Room, Location, Amenties from django.utils import timezone from datetime import timedelta @@ -13,19 +13,29 @@ def setUp(self): # Login as admin for all POST/PUT/DELETE actions self.client.login(username="admin", password="pass") + # Create locations + self.loc1 = Location.objects.create(name="Building A") + self.loc2 = Location.objects.create(name="Building B") + + # Create amenities + self.amenity1 = Amenties.objects.create(name="Projector") + self.amenity2 = Amenties.objects.create(name="Whiteboard") + + # Create rooms for i in range(5): start = timezone.make_aware(timezone.datetime(2025, 11, 1, 9, 0)) + timedelta(days=i) end = timezone.make_aware(timezone.datetime(2025, 11, 1, 18, 0)) + timedelta(days=i) - Room.objects.create( + room = Room.objects.create( name=f"Meeting Room {chr(65+i)}", img_url=f"https://example.com/room{chr(65+i)}.jpg", - location_id=(i % 2) + 1, # 1 or 2 - capacity_id=(i % 3) + 1, # 1,2,3 + location_id=self.loc1, + capacity_id=1, start_datetime=start, end_datetime=end, recurrence_rule="FREQ=WEEKLY;BYDAY=MO,WE,FR" ) + room.amenties.add(self.amenity1, self.amenity2) def test_list_rooms(self): # GET all rooms From 5138babe3870d3607a361ea9eb4bd27c03b42945 Mon Sep 17 00:00:00 2001 From: Jason Keo <163387808+jasonkeo@users.noreply.github.com> Date: Wed, 3 Dec 2025 10:04:12 +0000 Subject: [PATCH 07/26] Fixing chatgpted test cases --- .../0004_capacity_alter_room_capacity_id.py | 28 +++++ .../0005_rename_amenties_room_amenities.py | 18 ++++ server/api/room/models.py | 10 +- server/api/room/tests.py | 101 ++++++++++++------ 4 files changed, 122 insertions(+), 35 deletions(-) create mode 100644 server/api/room/migrations/0004_capacity_alter_room_capacity_id.py create mode 100644 server/api/room/migrations/0005_rename_amenties_room_amenities.py diff --git a/server/api/room/migrations/0004_capacity_alter_room_capacity_id.py b/server/api/room/migrations/0004_capacity_alter_room_capacity_id.py new file mode 100644 index 0000000..f74a25a --- /dev/null +++ b/server/api/room/migrations/0004_capacity_alter_room_capacity_id.py @@ -0,0 +1,28 @@ +# Generated by Django 5.1.12 on 2025-12-03 09:44 + +import django.db.models.deletion +from django.db import migrations, models + + +class Migration(migrations.Migration): + + dependencies = [ + ("room", "0003_amenties_location_room_amenties_and_more"), + ] + + operations = [ + migrations.CreateModel( + name="Capacity", + fields=[ + ("id", models.AutoField(primary_key=True, serialize=False)), + ("name", models.TextField()), + ], + ), + migrations.AlterField( + model_name="room", + name="capacity_id", + field=models.ForeignKey( + on_delete=django.db.models.deletion.CASCADE, to="room.capacity" + ), + ), + ] diff --git a/server/api/room/migrations/0005_rename_amenties_room_amenities.py b/server/api/room/migrations/0005_rename_amenties_room_amenities.py new file mode 100644 index 0000000..f46b464 --- /dev/null +++ b/server/api/room/migrations/0005_rename_amenties_room_amenities.py @@ -0,0 +1,18 @@ +# Generated by Django 5.1.12 on 2025-12-03 09:58 + +from django.db import migrations + + +class Migration(migrations.Migration): + + dependencies = [ + ("room", "0004_capacity_alter_room_capacity_id"), + ] + + operations = [ + migrations.RenameField( + model_name="room", + old_name="amenties", + new_name="amenities", + ), + ] diff --git a/server/api/room/models.py b/server/api/room/models.py index d7d7a73..d54579d 100644 --- a/server/api/room/models.py +++ b/server/api/room/models.py @@ -13,13 +13,19 @@ class Amenties(models.Model): def __str__(self): return self.name +class Capacity(models.Model): + id = models.AutoField(primary_key=True) + name = models.TextField() + + def __str__(self): + return str(self.name) class Room(models.Model): id = models.AutoField(primary_key=True) name = models.TextField() img_url = models.TextField() location_id = models.ForeignKey(Location, on_delete=models.CASCADE) - capacity_id = models.IntegerField() - amenties = models.ManyToManyField(Amenties, blank=True) + capacity_id = models.ForeignKey(Capacity, on_delete=models.CASCADE) + amenities = models.ManyToManyField(Amenties, blank=True) start_datetime = models.DateTimeField() end_datetime = models.DateTimeField() recurrence_rule = models.TextField() diff --git a/server/api/room/tests.py b/server/api/room/tests.py index a665244..99aa880 100644 --- a/server/api/room/tests.py +++ b/server/api/room/tests.py @@ -1,83 +1,118 @@ from rest_framework.test import APITestCase from rest_framework import status from django.contrib.auth.models import User -from .models import Room, Location, Amenties +from .models import Room, Location, Amenties, Capacity from django.utils import timezone from datetime import timedelta +import json class RoomAPITest(APITestCase): def setUp(self): - # Create admin user + # Admin user self.admin = User.objects.create_superuser("admin", "admin@test.com", "pass") - - # Login as admin for all POST/PUT/DELETE actions self.client.login(username="admin", password="pass") - # Create locations + # Locations self.loc1 = Location.objects.create(name="Building A") - self.loc2 = Location.objects.create(name="Building B") + self.loc3 = Location.objects.create(name="Building C") - # Create amenities + # Amenities self.amenity1 = Amenties.objects.create(name="Projector") self.amenity2 = Amenties.objects.create(name="Whiteboard") - - # Create rooms - for i in range(5): - start = timezone.make_aware(timezone.datetime(2025, 11, 1, 9, 0)) + timedelta(days=i) - end = timezone.make_aware(timezone.datetime(2025, 11, 1, 18, 0)) + timedelta(days=i) + self.amenity4 = Amenties.objects.create(name="House") + + # Capacities + self.cap1 = Capacity.objects.create(name="10 people") + self.cap4 = Capacity.objects.create(name="2 people") + + # Rooms + rooms_list = [ + { + "name": "Meeting Room A", + "location": self.loc1, + "capacity": self.cap1, + "amenities": [self.amenity1, self.amenity2] + }, + { + "name": "Meeting Room X", + "location": self.loc3, + "capacity": self.cap4, + "amenities": [self.amenity1, self.amenity4] + } + ] + + for idx, room_data in enumerate(rooms_list): + start = timezone.make_aware(timezone.datetime(2025, 11, 1, 9, 0)) + timedelta(days=idx) + end = timezone.make_aware(timezone.datetime(2025, 11, 1, 18, 0)) + timedelta(days=idx) room = Room.objects.create( - name=f"Meeting Room {chr(65+i)}", - img_url=f"https://example.com/room{chr(65+i)}.jpg", - location_id=self.loc1, - capacity_id=1, + name=room_data["name"], + img_url="https://example.com/room.jpg", + location_id=room_data["location"], + capacity_id=room_data["capacity"], start_datetime=start, end_datetime=end, recurrence_rule="FREQ=WEEKLY;BYDAY=MO,WE,FR" ) - room.amenties.add(self.amenity1, self.amenity2) + room.amenities.set(room_data["amenities"]) + print(f"Created room: {room.name}, ID: {room.id}") def test_list_rooms(self): - # GET all rooms response = self.client.get("/api/rooms/") + print("\nAll Rooms Response:") + print(json.dumps(response.data, indent=4)) self.assertEqual(response.status_code, status.HTTP_200_OK) - self.assertEqual(len(response.data), 5) # Filter by name - response = self.client.get("/api/rooms/?name=Room A") + response = self.client.get("/api/rooms/?name=Meeting Room A") + print("\nFilter by name Response:") + print(json.dumps(response.data, indent=4)) self.assertEqual(len(response.data), 1) self.assertEqual(response.data[0]["name"], "Meeting Room A") # Filter by location_id - response = self.client.get("/api/rooms/?location_id=1") - self.assertTrue(all(r["location_id"] == 1 for r in response.data)) + loc_id = self.loc1.id + response = self.client.get(f"/api/rooms/?location_id={loc_id}") + print("\nFilter by location_id Response:") + print(json.dumps(response.data, indent=4)) + self.assertTrue(all(r["location_id"] == loc_id for r in response.data)) # Filter by capacity_id - response = self.client.get("/api/rooms/?capacity_id=2") - self.assertTrue(all(r["capacity_id"] == 2 for r in response.data)) - + cap_id = self.cap4.id + response = self.client.get(f"/api/rooms/?capacity_id={cap_id}") + print("\nFilter by capacity_id Response:") + print(json.dumps(response.data, indent=4)) + self.assertTrue(all(r["capacity_id"] == cap_id for r in response.data)) def test_retrieve_update_delete_room(self): - # Get a room room = Room.objects.first() + + # Retrieve response = self.client.get(f"/api/rooms/{room.id}/") + print("\nRetrieve Room Response:") + print(json.dumps(response.data, indent=4)) self.assertEqual(response.status_code, status.HTTP_200_OK) - self.assertEqual(response.data["name"], room.name) - # Update room + # Update response = self.client.patch(f"/api/rooms/{room.id}/", {"name": "Updated Room"}, format="json") + print("\nUpdate Room Response:") + print(response.content.decode()) self.assertEqual(response.status_code, status.HTTP_200_OK) self.assertEqual(response.data["name"], "Updated Room") - # Check via GET + # GET after update response = self.client.get(f"/api/rooms/{room.id}/") + print("\nRetrieve After Update Response:") + print(response.content.decode()) self.assertEqual(response.data["name"], "Updated Room") - # Delete room + # Delete response = self.client.delete(f"/api/rooms/{room.id}/") + print("\nDelete Room Response:") + print(response.content.decode()) self.assertEqual(response.status_code, status.HTTP_200_OK) - self.assertEqual(response.data["message"], "Room deleted successfully") - # Check room count + # GET all rooms after delete response = self.client.get("/api/rooms/") - self.assertEqual(len(response.data), 4) + print("\nAll Rooms After Delete Response:") + print(response.content.decode()) From 40e818f332ecc123caf4b12f9690475fabdb7745 Mon Sep 17 00:00:00 2001 From: Jason Keo <163387808+jasonkeo@users.noreply.github.com> Date: Wed, 3 Dec 2025 10:38:47 +0000 Subject: [PATCH 08/26] Fixing Api responses to match requirement description --- server/api/room/serializers.py | 38 +++++++++++++++++++++++++++++++++- server/api/room/tests.py | 2 -- server/api/room/views.py | 10 ++++++++- 3 files changed, 46 insertions(+), 4 deletions(-) diff --git a/server/api/room/serializers.py b/server/api/room/serializers.py index 04a9788..646da7b 100644 --- a/server/api/room/serializers.py +++ b/server/api/room/serializers.py @@ -1,8 +1,44 @@ from rest_framework import serializers -from .models import Room +from .models import Room, Location, Capacity, Amenties +class LocationSerializer(serializers.ModelSerializer): + class Meta: + model = Location + fields = ["id", "name"] + +class CapacitySerializer(serializers.ModelSerializer): + class Meta: + model = Capacity + fields = ["id", "name"] +class AmenitySerializer(serializers.ModelSerializer): + class Meta: + model = Amenties + fields = ["id", "name"] + +# 2 different serialiser because api requirements want more or less details depending on request type class RoomSerializer(serializers.ModelSerializer): + location = LocationSerializer(read_only=True) + capacity = CapacitySerializer(read_only=True) + amenities = AmenitySerializer(many=True, read_only=True) + + class Meta: + model = Room + fields = [ + "id", + "name", + "img_url", + "location", + "capacity", + "amenities", + ] + + +class RoomListSerializer(serializers.ModelSerializer): + location = LocationSerializer(read_only=True) + capacity = CapacitySerializer(read_only=True) + amenities = AmenitySerializer(many=True, read_only=True) + class Meta: model = Room fields = "__all__" diff --git a/server/api/room/tests.py b/server/api/room/tests.py index 99aa880..b329f49 100644 --- a/server/api/room/tests.py +++ b/server/api/room/tests.py @@ -75,14 +75,12 @@ def test_list_rooms(self): response = self.client.get(f"/api/rooms/?location_id={loc_id}") print("\nFilter by location_id Response:") print(json.dumps(response.data, indent=4)) - self.assertTrue(all(r["location_id"] == loc_id for r in response.data)) # Filter by capacity_id cap_id = self.cap4.id response = self.client.get(f"/api/rooms/?capacity_id={cap_id}") print("\nFilter by capacity_id Response:") print(json.dumps(response.data, indent=4)) - self.assertTrue(all(r["capacity_id"] == cap_id for r in response.data)) def test_retrieve_update_delete_room(self): room = Room.objects.first() diff --git a/server/api/room/views.py b/server/api/room/views.py index f7560f8..288c417 100644 --- a/server/api/room/views.py +++ b/server/api/room/views.py @@ -1,7 +1,8 @@ from rest_framework import viewsets, permissions from rest_framework.response import Response from .models import Room -from .serializers import RoomSerializer +from .serializers import RoomSerializer, RoomListSerializer + # Viewset is library that provides CRUD operations for api # Admin have create update delete permissions everyone can read @@ -19,6 +20,12 @@ def get_permissions(self): return [permissions.IsAdminUser()] return [permissions.AllowAny()] + # 2 different serialiser because api requirements want more or less details depending on request type + def get_serializer_class(self): + if self.action == "retrieve": + return RoomListSerializer + return RoomSerializer + def get_queryset(self): qs = super().get_queryset() params = self.request.query_params @@ -43,6 +50,7 @@ def update(self, request, *args, **kwargs): return Response({ "id": instance.id, "name": instance.name, + "amenities": serializer.data.get("amenities", []), "updated_at": instance.updated_at }) From 541e107c52aceff0646e9dc76909afce91643de7 Mon Sep 17 00:00:00 2001 From: Jason Keo <163387808+jasonkeo@users.noreply.github.com> Date: Sat, 6 Dec 2025 03:26:54 +0000 Subject: [PATCH 09/26] fix merge errors --- server/api/room/admin.py | 7 ++++++- server/api/room/models.py | 17 +++++++++++++++++ server/api/room/tests.py | 6 ++++++ server/api/room/urls.py | 9 +++++++++ server/api/room/views.py | 6 ++++++ 5 files changed, 44 insertions(+), 1 deletion(-) diff --git a/server/api/room/admin.py b/server/api/room/admin.py index 8e318b0..60cfa64 100644 --- a/server/api/room/admin.py +++ b/server/api/room/admin.py @@ -1,8 +1,13 @@ from django.contrib import admin +<<<<<<< Updated upstream from .models import Room # Register your models here. @admin.register(Room) class RoomAdmin(admin.ModelAdmin): list_display = ("id", "name", "location_id", "capacity_id", "start_datetime", "end_datetime") search_fields = ("name",) - \ No newline at end of file + +======= + +# Register your models here. +>>>>>>> Stashed changes diff --git a/server/api/room/models.py b/server/api/room/models.py index d54579d..4f1daea 100644 --- a/server/api/room/models.py +++ b/server/api/room/models.py @@ -1,5 +1,6 @@ from django.db import models +<<<<<<< Updated upstream class Location(models.Model): id = models.AutoField(primary_key=True) name = models.TextField() @@ -19,10 +20,14 @@ class Capacity(models.Model): def __str__(self): return str(self.name) +======= + +>>>>>>> Stashed changes class Room(models.Model): id = models.AutoField(primary_key=True) name = models.TextField() img_url = models.TextField() +<<<<<<< Updated upstream location_id = models.ForeignKey(Location, on_delete=models.CASCADE) capacity_id = models.ForeignKey(Capacity, on_delete=models.CASCADE) amenities = models.ManyToManyField(Amenties, blank=True) @@ -31,9 +36,21 @@ class Room(models.Model): recurrence_rule = models.TextField() created_at = models.DateTimeField(auto_now_add=True) updated_at = models.DateTimeField(auto_now=True) +======= + location_id = models.IntegerField() + capacity_id = models.IntegerField() + start_datetime = models.DateTimeField() + end_datetime = models.DateTimeField() + recurrence_rule = models.TextField() + created_at = models.DateTimeField() + updated_at = models.DateTimeField() +>>>>>>> Stashed changes def __str__(self): return self.name # Create your models here. +<<<<<<< Updated upstream +======= +>>>>>>> Stashed changes diff --git a/server/api/room/tests.py b/server/api/room/tests.py index b329f49..3e56b0c 100644 --- a/server/api/room/tests.py +++ b/server/api/room/tests.py @@ -1,3 +1,4 @@ +<<<<<<< Updated upstream from rest_framework.test import APITestCase from rest_framework import status from django.contrib.auth.models import User @@ -114,3 +115,8 @@ def test_retrieve_update_delete_room(self): response = self.client.get("/api/rooms/") print("\nAll Rooms After Delete Response:") print(response.content.decode()) +======= +from django.test import TestCase + +# Create your tests here. +>>>>>>> Stashed changes diff --git a/server/api/room/urls.py b/server/api/room/urls.py index 97fa2b5..0a13cb8 100644 --- a/server/api/room/urls.py +++ b/server/api/room/urls.py @@ -1,3 +1,4 @@ +<<<<<<< Updated upstream from rest_framework.routers import DefaultRouter from .views import RoomViewSet @@ -5,3 +6,11 @@ router.register(r'', RoomViewSet, basename='rooms') urlpatterns = router.urls +======= +from django.urls import path +from . import views + +app_name = "room" +urlpatterns = [ +] +>>>>>>> Stashed changes diff --git a/server/api/room/views.py b/server/api/room/views.py index 288c417..73dd4cd 100644 --- a/server/api/room/views.py +++ b/server/api/room/views.py @@ -1,3 +1,4 @@ +<<<<<<< Updated upstream from rest_framework import viewsets, permissions from rest_framework.response import Response from .models import Room @@ -58,3 +59,8 @@ def destroy(self, request, *args, **kwargs): instance = self.get_object() instance.delete() return Response({"message": "Room deleted successfully"}) +======= +from django.shortcuts import render + +# Create your views here. +>>>>>>> Stashed changes From adc116b2948f0fa9818bb16f78b789c9638ed3da Mon Sep 17 00:00:00 2001 From: Jason Keo <163387808+jasonkeo@users.noreply.github.com> Date: Sat, 6 Dec 2025 03:28:21 +0000 Subject: [PATCH 10/26] more fixes --- server/api/room/admin.py | 6 ------ server/api/room/models.py | 17 ----------------- server/api/room/tests.py | 6 ------ server/api/room/urls.py | 9 --------- server/api/room/views.py | 6 ------ 5 files changed, 44 deletions(-) diff --git a/server/api/room/admin.py b/server/api/room/admin.py index 60cfa64..a00871d 100644 --- a/server/api/room/admin.py +++ b/server/api/room/admin.py @@ -1,13 +1,7 @@ from django.contrib import admin -<<<<<<< Updated upstream from .models import Room # Register your models here. @admin.register(Room) class RoomAdmin(admin.ModelAdmin): list_display = ("id", "name", "location_id", "capacity_id", "start_datetime", "end_datetime") search_fields = ("name",) - -======= - -# Register your models here. ->>>>>>> Stashed changes diff --git a/server/api/room/models.py b/server/api/room/models.py index 4f1daea..d54579d 100644 --- a/server/api/room/models.py +++ b/server/api/room/models.py @@ -1,6 +1,5 @@ from django.db import models -<<<<<<< Updated upstream class Location(models.Model): id = models.AutoField(primary_key=True) name = models.TextField() @@ -20,14 +19,10 @@ class Capacity(models.Model): def __str__(self): return str(self.name) -======= - ->>>>>>> Stashed changes class Room(models.Model): id = models.AutoField(primary_key=True) name = models.TextField() img_url = models.TextField() -<<<<<<< Updated upstream location_id = models.ForeignKey(Location, on_delete=models.CASCADE) capacity_id = models.ForeignKey(Capacity, on_delete=models.CASCADE) amenities = models.ManyToManyField(Amenties, blank=True) @@ -36,21 +31,9 @@ class Room(models.Model): recurrence_rule = models.TextField() created_at = models.DateTimeField(auto_now_add=True) updated_at = models.DateTimeField(auto_now=True) -======= - location_id = models.IntegerField() - capacity_id = models.IntegerField() - start_datetime = models.DateTimeField() - end_datetime = models.DateTimeField() - recurrence_rule = models.TextField() - created_at = models.DateTimeField() - updated_at = models.DateTimeField() ->>>>>>> Stashed changes def __str__(self): return self.name # Create your models here. -<<<<<<< Updated upstream -======= ->>>>>>> Stashed changes diff --git a/server/api/room/tests.py b/server/api/room/tests.py index 3e56b0c..b329f49 100644 --- a/server/api/room/tests.py +++ b/server/api/room/tests.py @@ -1,4 +1,3 @@ -<<<<<<< Updated upstream from rest_framework.test import APITestCase from rest_framework import status from django.contrib.auth.models import User @@ -115,8 +114,3 @@ def test_retrieve_update_delete_room(self): response = self.client.get("/api/rooms/") print("\nAll Rooms After Delete Response:") print(response.content.decode()) -======= -from django.test import TestCase - -# Create your tests here. ->>>>>>> Stashed changes diff --git a/server/api/room/urls.py b/server/api/room/urls.py index 0a13cb8..97fa2b5 100644 --- a/server/api/room/urls.py +++ b/server/api/room/urls.py @@ -1,4 +1,3 @@ -<<<<<<< Updated upstream from rest_framework.routers import DefaultRouter from .views import RoomViewSet @@ -6,11 +5,3 @@ router.register(r'', RoomViewSet, basename='rooms') urlpatterns = router.urls -======= -from django.urls import path -from . import views - -app_name = "room" -urlpatterns = [ -] ->>>>>>> Stashed changes diff --git a/server/api/room/views.py b/server/api/room/views.py index 73dd4cd..288c417 100644 --- a/server/api/room/views.py +++ b/server/api/room/views.py @@ -1,4 +1,3 @@ -<<<<<<< Updated upstream from rest_framework import viewsets, permissions from rest_framework.response import Response from .models import Room @@ -59,8 +58,3 @@ def destroy(self, request, *args, **kwargs): instance = self.get_object() instance.delete() return Response({"message": "Room deleted successfully"}) -======= -from django.shortcuts import render - -# Create your views here. ->>>>>>> Stashed changes From 4ac62fdb2d8219e0ae54b3feb18883dc3d3d5fdd Mon Sep 17 00:00:00 2001 From: Jason Keo <163387808+jasonkeo@users.noreply.github.com> Date: Sat, 6 Dec 2025 03:33:30 +0000 Subject: [PATCH 11/26] added location & capicity to django admin page --- server/api/room/admin.py | 8 ++++++++ 1 file changed, 8 insertions(+) diff --git a/server/api/room/admin.py b/server/api/room/admin.py index a00871d..c3f4303 100644 --- a/server/api/room/admin.py +++ b/server/api/room/admin.py @@ -5,3 +5,11 @@ class RoomAdmin(admin.ModelAdmin): list_display = ("id", "name", "location_id", "capacity_id", "start_datetime", "end_datetime") search_fields = ("name",) + +class LocationAdmin(admin.ModelAdmin): + list_display = ("id", "name") + search_fields = ("name",) + +class CapacityAdmin(admin.ModelAdmin): + list_display = ("id", "name") + search_fields = ("name",) \ No newline at end of file From 4e29146f4ef41f2be844d96a17183390b35f5c32 Mon Sep 17 00:00:00 2001 From: Jason Keo <163387808+jasonkeo@users.noreply.github.com> Date: Sat, 6 Dec 2025 03:35:01 +0000 Subject: [PATCH 12/26] fix add model to admin --- server/api/room/admin.py | 4 +++- 1 file changed, 3 insertions(+), 1 deletion(-) diff --git a/server/api/room/admin.py b/server/api/room/admin.py index c3f4303..fa0e2b8 100644 --- a/server/api/room/admin.py +++ b/server/api/room/admin.py @@ -1,15 +1,17 @@ from django.contrib import admin -from .models import Room +from .models import Room, Location, Capacity # Register your models here. @admin.register(Room) class RoomAdmin(admin.ModelAdmin): list_display = ("id", "name", "location_id", "capacity_id", "start_datetime", "end_datetime") search_fields = ("name",) +@admin.register(Location) class LocationAdmin(admin.ModelAdmin): list_display = ("id", "name") search_fields = ("name",) +@admin.register(Capacity) class CapacityAdmin(admin.ModelAdmin): list_display = ("id", "name") search_fields = ("name",) \ No newline at end of file From 3609faad8342792bb179695f6cedd0bdad10820a Mon Sep 17 00:00:00 2001 From: Jason Keo <163387808+jasonkeo@users.noreply.github.com> Date: Sat, 6 Dec 2025 03:38:59 +0000 Subject: [PATCH 13/26] add amenties to admin --- server/api/room/admin.py | 7 ++++++- 1 file changed, 6 insertions(+), 1 deletion(-) diff --git a/server/api/room/admin.py b/server/api/room/admin.py index fa0e2b8..1879d2d 100644 --- a/server/api/room/admin.py +++ b/server/api/room/admin.py @@ -1,5 +1,5 @@ from django.contrib import admin -from .models import Room, Location, Capacity +from .models import Room, Location, Capacity, Amenties # Register your models here. @admin.register(Room) class RoomAdmin(admin.ModelAdmin): @@ -13,5 +13,10 @@ class LocationAdmin(admin.ModelAdmin): @admin.register(Capacity) class CapacityAdmin(admin.ModelAdmin): + list_display = ("id", "name") + search_fields = ("name",) + +@admin.register(Amenties) +class AmentiesAdmin(admin.ModelAdmin): list_display = ("id", "name") search_fields = ("name",) \ No newline at end of file From 17a7c6ef64752bbb9edfc738f137caef7fcf1b49 Mon Sep 17 00:00:00 2001 From: Jason Keo <163387808+jasonkeo@users.noreply.github.com> Date: Sat, 6 Dec 2025 04:07:53 +0000 Subject: [PATCH 14/26] add image upload to local file --- server/api/room/models.py | 20 ++++++++++---------- server/api/settings.py | 2 ++ 2 files changed, 12 insertions(+), 10 deletions(-) diff --git a/server/api/room/models.py b/server/api/room/models.py index d54579d..ad63775 100644 --- a/server/api/room/models.py +++ b/server/api/room/models.py @@ -2,33 +2,33 @@ class Location(models.Model): id = models.AutoField(primary_key=True) - name = models.TextField() + name = models.TextField(blank=False) def __str__(self): return self.name class Amenties(models.Model): id = models.AutoField(primary_key=True) - name = models.TextField() + name = models.TextField(blank=False) def __str__(self): return self.name class Capacity(models.Model): id = models.AutoField(primary_key=True) - name = models.TextField() + name = models.TextField(blank=False) def __str__(self): return str(self.name) class Room(models.Model): id = models.AutoField(primary_key=True) - name = models.TextField() - img_url = models.TextField() - location_id = models.ForeignKey(Location, on_delete=models.CASCADE) - capacity_id = models.ForeignKey(Capacity, on_delete=models.CASCADE) + name = models.TextField(blank=False) + img_url = models.ImageField(upload_to='') # change to upload s3 later for deployment? + location_id = models.ForeignKey(Location, on_delete=models.CASCADE, blank=False) + capacity_id = models.ForeignKey(Capacity, on_delete=models.CASCADE, blank=False) amenities = models.ManyToManyField(Amenties, blank=True) - start_datetime = models.DateTimeField() - end_datetime = models.DateTimeField() - recurrence_rule = models.TextField() + start_datetime = models.DateTimeField(blank=False) + end_datetime = models.DateTimeField(blank=False) + recurrence_rule = models.TextField(blank=False) created_at = models.DateTimeField(auto_now_add=True) updated_at = models.DateTimeField(auto_now=True) diff --git a/server/api/settings.py b/server/api/settings.py index 467775c..1edfd22 100644 --- a/server/api/settings.py +++ b/server/api/settings.py @@ -167,6 +167,8 @@ # STATIC_ROOT is where the static files get copied to when "collectstatic" is run. STATIC_ROOT = "static_files" +MEDIA_ROOT = BASE_DIR / "room_images" + # This is where to _find_ static files when 'collectstatic' is run. # These files are then copied to the STATIC_ROOT location. STATICFILES_DIRS = ("static",) From aca4760a447d3811bf5700ce1acfc2c4737d43e4 Mon Sep 17 00:00:00 2001 From: Jason Keo <163387808+jasonkeo@users.noreply.github.com> Date: Sat, 6 Dec 2025 04:46:52 +0000 Subject: [PATCH 15/26] fix api response --- server/api/room/serializers.py | 32 ++++++++++++++++++-------------- 1 file changed, 18 insertions(+), 14 deletions(-) diff --git a/server/api/room/serializers.py b/server/api/room/serializers.py index 646da7b..81b6402 100644 --- a/server/api/room/serializers.py +++ b/server/api/room/serializers.py @@ -1,15 +1,6 @@ from rest_framework import serializers from .models import Room, Location, Capacity, Amenties -class LocationSerializer(serializers.ModelSerializer): - class Meta: - model = Location - fields = ["id", "name"] - -class CapacitySerializer(serializers.ModelSerializer): - class Meta: - model = Capacity - fields = ["id", "name"] class AmenitySerializer(serializers.ModelSerializer): class Meta: @@ -18,8 +9,8 @@ class Meta: # 2 different serialiser because api requirements want more or less details depending on request type class RoomSerializer(serializers.ModelSerializer): - location = LocationSerializer(read_only=True) - capacity = CapacitySerializer(read_only=True) + location = serializers.StringRelatedField(read_only=True) + capacity = serializers.StringRelatedField(read_only=True) amenities = AmenitySerializer(many=True, read_only=True) class Meta: @@ -35,10 +26,23 @@ class Meta: class RoomListSerializer(serializers.ModelSerializer): - location = LocationSerializer(read_only=True) - capacity = CapacitySerializer(read_only=True) + location_id = serializers.StringRelatedField(read_only=True) + capacity_id = serializers.StringRelatedField(read_only=True) amenities = AmenitySerializer(many=True, read_only=True) class Meta: model = Room - fields = "__all__" + fields = [ + "id", + "name", + "img_url", + "location_id", + "capacity_id", + "amenities", + "start_datetime", + "end_datetime", + "recurrence_rule", + "created_at", + "updated_at", + ] + From 08e9e088ef17170c9b62a54530205118805e1a34 Mon Sep 17 00:00:00 2001 From: Jason Keo <163387808+jasonkeo@users.noreply.github.com> Date: Sat, 6 Dec 2025 04:54:13 +0000 Subject: [PATCH 16/26] fix naming of fields from location_id to location? might cause errors? --- server/api/room/admin.py | 2 +- ...name_capacity_id_room_capacity_and_more.py | 33 +++++++++++++++++++ server/api/room/models.py | 6 ++-- server/api/room/serializers.py | 10 +++--- server/api/room/tests.py | 8 ++--- 5 files changed, 46 insertions(+), 13 deletions(-) create mode 100644 server/api/room/migrations/0006_rename_capacity_id_room_capacity_and_more.py diff --git a/server/api/room/admin.py b/server/api/room/admin.py index 1879d2d..2176a21 100644 --- a/server/api/room/admin.py +++ b/server/api/room/admin.py @@ -3,7 +3,7 @@ # Register your models here. @admin.register(Room) class RoomAdmin(admin.ModelAdmin): - list_display = ("id", "name", "location_id", "capacity_id", "start_datetime", "end_datetime") + list_display = ("id", "name", "location", "capacity", "start_datetime", "end_datetime") search_fields = ("name",) @admin.register(Location) diff --git a/server/api/room/migrations/0006_rename_capacity_id_room_capacity_and_more.py b/server/api/room/migrations/0006_rename_capacity_id_room_capacity_and_more.py new file mode 100644 index 0000000..a8d680e --- /dev/null +++ b/server/api/room/migrations/0006_rename_capacity_id_room_capacity_and_more.py @@ -0,0 +1,33 @@ +# Generated by Django 5.1.12 on 2025-12-06 04:52 + +from django.db import migrations, models + + +class Migration(migrations.Migration): + + dependencies = [ + ("room", "0005_rename_amenties_room_amenities"), + ] + + operations = [ + migrations.RenameField( + model_name="room", + old_name="capacity_id", + new_name="capacity", + ), + migrations.RenameField( + model_name="room", + old_name="location_id", + new_name="location", + ), + migrations.RemoveField( + model_name="room", + name="img_url", + ), + migrations.AddField( + model_name="room", + name="img", + field=models.ImageField(default=2, upload_to=""), + preserve_default=False, + ), + ] diff --git a/server/api/room/models.py b/server/api/room/models.py index ad63775..6e658d2 100644 --- a/server/api/room/models.py +++ b/server/api/room/models.py @@ -22,9 +22,9 @@ def __str__(self): class Room(models.Model): id = models.AutoField(primary_key=True) name = models.TextField(blank=False) - img_url = models.ImageField(upload_to='') # change to upload s3 later for deployment? - location_id = models.ForeignKey(Location, on_delete=models.CASCADE, blank=False) - capacity_id = models.ForeignKey(Capacity, on_delete=models.CASCADE, blank=False) + img = models.ImageField(upload_to='') # change to upload s3 later for deployment? + location = models.ForeignKey(Location, on_delete=models.CASCADE, blank=False) + capacity = models.ForeignKey(Capacity, on_delete=models.CASCADE, blank=False) amenities = models.ManyToManyField(Amenties, blank=True) start_datetime = models.DateTimeField(blank=False) end_datetime = models.DateTimeField(blank=False) diff --git a/server/api/room/serializers.py b/server/api/room/serializers.py index 81b6402..6be0a92 100644 --- a/server/api/room/serializers.py +++ b/server/api/room/serializers.py @@ -1,5 +1,5 @@ from rest_framework import serializers -from .models import Room, Location, Capacity, Amenties +from .models import Room, Amenties class AmenitySerializer(serializers.ModelSerializer): @@ -26,8 +26,8 @@ class Meta: class RoomListSerializer(serializers.ModelSerializer): - location_id = serializers.StringRelatedField(read_only=True) - capacity_id = serializers.StringRelatedField(read_only=True) + location = serializers.StringRelatedField(read_only=True) + capacity = serializers.StringRelatedField(read_only=True) amenities = AmenitySerializer(many=True, read_only=True) class Meta: @@ -36,8 +36,8 @@ class Meta: "id", "name", "img_url", - "location_id", - "capacity_id", + "location", + "capacity", "amenities", "start_datetime", "end_datetime", diff --git a/server/api/room/tests.py b/server/api/room/tests.py index b329f49..207619a 100644 --- a/server/api/room/tests.py +++ b/server/api/room/tests.py @@ -48,8 +48,8 @@ def setUp(self): room = Room.objects.create( name=room_data["name"], img_url="https://example.com/room.jpg", - location_id=room_data["location"], - capacity_id=room_data["capacity"], + location=room_data["location"], + capacity=room_data["capacity"], start_datetime=start, end_datetime=end, recurrence_rule="FREQ=WEEKLY;BYDAY=MO,WE,FR" @@ -73,13 +73,13 @@ def test_list_rooms(self): # Filter by location_id loc_id = self.loc1.id response = self.client.get(f"/api/rooms/?location_id={loc_id}") - print("\nFilter by location_id Response:") + print("\nFilter by location Response:") print(json.dumps(response.data, indent=4)) # Filter by capacity_id cap_id = self.cap4.id response = self.client.get(f"/api/rooms/?capacity_id={cap_id}") - print("\nFilter by capacity_id Response:") + print("\nFilter by capacity Response:") print(json.dumps(response.data, indent=4)) def test_retrieve_update_delete_room(self): From 01769cdd798d426d55bf2fd32900c5322e5f29d5 Mon Sep 17 00:00:00 2001 From: Jason Keo <163387808+jasonkeo@users.noreply.github.com> Date: Sat, 6 Dec 2025 07:07:15 +0000 Subject: [PATCH 17/26] add status & change capicity to int --- server/api/room/admin.py | 7 +---- ...om_capacity_room_status_delete_capacity.py | 26 +++++++++++++++++++ server/api/room/models.py | 9 ++----- server/api/room/serializers.py | 6 ++--- server/api/room/tests.py | 2 +- 5 files changed, 32 insertions(+), 18 deletions(-) create mode 100644 server/api/room/migrations/0007_alter_room_capacity_room_status_delete_capacity.py diff --git a/server/api/room/admin.py b/server/api/room/admin.py index 2176a21..21da2fd 100644 --- a/server/api/room/admin.py +++ b/server/api/room/admin.py @@ -1,5 +1,5 @@ from django.contrib import admin -from .models import Room, Location, Capacity, Amenties +from .models import Room, Location, Amenties # Register your models here. @admin.register(Room) class RoomAdmin(admin.ModelAdmin): @@ -11,11 +11,6 @@ class LocationAdmin(admin.ModelAdmin): list_display = ("id", "name") search_fields = ("name",) -@admin.register(Capacity) -class CapacityAdmin(admin.ModelAdmin): - list_display = ("id", "name") - search_fields = ("name",) - @admin.register(Amenties) class AmentiesAdmin(admin.ModelAdmin): list_display = ("id", "name") diff --git a/server/api/room/migrations/0007_alter_room_capacity_room_status_delete_capacity.py b/server/api/room/migrations/0007_alter_room_capacity_room_status_delete_capacity.py new file mode 100644 index 0000000..4a2110f --- /dev/null +++ b/server/api/room/migrations/0007_alter_room_capacity_room_status_delete_capacity.py @@ -0,0 +1,26 @@ +# Generated by Django 5.1.12 on 2025-12-06 07:05 + +from django.db import migrations, models + + +class Migration(migrations.Migration): + + dependencies = [ + ("room", "0006_rename_capacity_id_room_capacity_and_more"), + ] + + operations = [ + migrations.AlterField( + model_name="room", + name="capacity", + field=models.IntegerField(), + ), + migrations.AddField( + model_name="room", + name="status", + field=models.BooleanField(default=True), + ), + migrations.DeleteModel( + name="Capacity", + ), + ] diff --git a/server/api/room/models.py b/server/api/room/models.py index 6e658d2..0d5e52b 100644 --- a/server/api/room/models.py +++ b/server/api/room/models.py @@ -13,19 +13,15 @@ class Amenties(models.Model): def __str__(self): return self.name -class Capacity(models.Model): - id = models.AutoField(primary_key=True) - name = models.TextField(blank=False) - def __str__(self): - return str(self.name) class Room(models.Model): id = models.AutoField(primary_key=True) name = models.TextField(blank=False) img = models.ImageField(upload_to='') # change to upload s3 later for deployment? location = models.ForeignKey(Location, on_delete=models.CASCADE, blank=False) - capacity = models.ForeignKey(Capacity, on_delete=models.CASCADE, blank=False) + capacity = models.IntegerField(blank=False) amenities = models.ManyToManyField(Amenties, blank=True) + status = models.BooleanField(default=True) start_datetime = models.DateTimeField(blank=False) end_datetime = models.DateTimeField(blank=False) recurrence_rule = models.TextField(blank=False) @@ -36,4 +32,3 @@ def __str__(self): return self.name # Create your models here. - diff --git a/server/api/room/serializers.py b/server/api/room/serializers.py index 6be0a92..4d517b5 100644 --- a/server/api/room/serializers.py +++ b/server/api/room/serializers.py @@ -10,7 +10,6 @@ class Meta: # 2 different serialiser because api requirements want more or less details depending on request type class RoomSerializer(serializers.ModelSerializer): location = serializers.StringRelatedField(read_only=True) - capacity = serializers.StringRelatedField(read_only=True) amenities = AmenitySerializer(many=True, read_only=True) class Meta: @@ -18,7 +17,7 @@ class Meta: fields = [ "id", "name", - "img_url", + "img", "location", "capacity", "amenities", @@ -27,7 +26,6 @@ class Meta: class RoomListSerializer(serializers.ModelSerializer): location = serializers.StringRelatedField(read_only=True) - capacity = serializers.StringRelatedField(read_only=True) amenities = AmenitySerializer(many=True, read_only=True) class Meta: @@ -35,7 +33,7 @@ class Meta: fields = [ "id", "name", - "img_url", + "img", "location", "capacity", "amenities", diff --git a/server/api/room/tests.py b/server/api/room/tests.py index 207619a..4124ea4 100644 --- a/server/api/room/tests.py +++ b/server/api/room/tests.py @@ -47,7 +47,7 @@ def setUp(self): room = Room.objects.create( name=room_data["name"], - img_url="https://example.com/room.jpg", + img="https://example.com/room.jpg", location=room_data["location"], capacity=room_data["capacity"], start_datetime=start, From 62315a0fdb55daf2833733850fd3a8caf923f3f3 Mon Sep 17 00:00:00 2001 From: Jason Keo <163387808+jasonkeo@users.noreply.github.com> Date: Sat, 6 Dec 2025 07:21:59 +0000 Subject: [PATCH 18/26] Make test better --- server/api/room/tests.py | 109 +++++++++++++++++---------------------- 1 file changed, 48 insertions(+), 61 deletions(-) diff --git a/server/api/room/tests.py b/server/api/room/tests.py index 4124ea4..cf673fb 100644 --- a/server/api/room/tests.py +++ b/server/api/room/tests.py @@ -1,9 +1,8 @@ from rest_framework.test import APITestCase from rest_framework import status from django.contrib.auth.models import User -from .models import Room, Location, Amenties, Capacity +from .models import Room, Location, Amenties from django.utils import timezone -from datetime import timedelta import json class RoomAPITest(APITestCase): @@ -21,96 +20,84 @@ def setUp(self): self.amenity2 = Amenties.objects.create(name="Whiteboard") self.amenity4 = Amenties.objects.create(name="House") - # Capacities - self.cap1 = Capacity.objects.create(name="10 people") - self.cap4 = Capacity.objects.create(name="2 people") - # Rooms - rooms_list = [ - { - "name": "Meeting Room A", - "location": self.loc1, - "capacity": self.cap1, - "amenities": [self.amenity1, self.amenity2] - }, - { - "name": "Meeting Room X", - "location": self.loc3, - "capacity": self.cap4, - "amenities": [self.amenity1, self.amenity4] - } - ] - - for idx, room_data in enumerate(rooms_list): - start = timezone.make_aware(timezone.datetime(2025, 11, 1, 9, 0)) + timedelta(days=idx) - end = timezone.make_aware(timezone.datetime(2025, 11, 1, 18, 0)) + timedelta(days=idx) - - room = Room.objects.create( - name=room_data["name"], - img="https://example.com/room.jpg", - location=room_data["location"], - capacity=room_data["capacity"], - start_datetime=start, - end_datetime=end, - recurrence_rule="FREQ=WEEKLY;BYDAY=MO,WE,FR" - ) - room.amenities.set(room_data["amenities"]) - print(f"Created room: {room.name}, ID: {room.id}") - - def test_list_rooms(self): + self.room1 = Room.objects.create( + name="Conference Room 1", + location=self.loc1, + capacity=10, + start_datetime=timezone.make_aware(timezone.datetime(2025, 10, 1, 9, 0)), + end_datetime=timezone.make_aware(timezone.datetime(2025, 10, 1, 18, 0)), + recurrence_rule="FREQ=DAILY;BYDAY=MO,TU,WE," + ) + self.room1.amenities.set([self.amenity1, self.amenity2]) + + self.room2 = Room.objects.create( + name="Meeting Room A", + location=self.loc3, + capacity=5, + start_datetime=timezone.make_aware(timezone.datetime(2025, 11, 1, 10, 0)), + end_datetime=timezone.make_aware(timezone.datetime(2025, 11, 1, 17, 0)), + recurrence_rule="FREQ=WEEKLY;BYDAY=MO,WE,FR" + ) + self.room2.amenities.set([self.amenity4]) + + # -------- LIST & FILTER TESTS -------- + def test_list_all_rooms(self): response = self.client.get("/api/rooms/") print("\nAll Rooms Response:") print(json.dumps(response.data, indent=4)) self.assertEqual(response.status_code, status.HTTP_200_OK) + self.assertEqual(len(response.data), 2) - # Filter by name + def test_filter_rooms_by_name(self): response = self.client.get("/api/rooms/?name=Meeting Room A") - print("\nFilter by name Response:") + print("\nFilter by Name Response:") print(json.dumps(response.data, indent=4)) self.assertEqual(len(response.data), 1) self.assertEqual(response.data[0]["name"], "Meeting Room A") - # Filter by location_id + def test_filter_rooms_by_location(self): loc_id = self.loc1.id response = self.client.get(f"/api/rooms/?location_id={loc_id}") - print("\nFilter by location Response:") + print("\nFilter by Location Response:") print(json.dumps(response.data, indent=4)) + self.assertTrue(all(r["location_id"] == loc_id for r in response.data)) - # Filter by capacity_id - cap_id = self.cap4.id - response = self.client.get(f"/api/rooms/?capacity_id={cap_id}") - print("\nFilter by capacity Response:") - print(json.dumps(response.data, indent=4)) - - def test_retrieve_update_delete_room(self): + # -------- RETRIEVE TEST -------- + def test_retrieve_room(self): room = Room.objects.first() - - # Retrieve response = self.client.get(f"/api/rooms/{room.id}/") print("\nRetrieve Room Response:") print(json.dumps(response.data, indent=4)) self.assertEqual(response.status_code, status.HTTP_200_OK) + self.assertEqual(response.data["id"], room.id) - # Update - response = self.client.patch(f"/api/rooms/{room.id}/", {"name": "Updated Room"}, format="json") + # -------- UPDATE TEST -------- + def test_update_room(self): + room = Room.objects.first() + response = self.client.patch( + f"/api/rooms/{room.id}/", + {"name": "Updated Room"}, + format="json" + ) print("\nUpdate Room Response:") - print(response.content.decode()) + print(response.content.decode()) self.assertEqual(response.status_code, status.HTTP_200_OK) self.assertEqual(response.data["name"], "Updated Room") - # GET after update + # Confirm update response = self.client.get(f"/api/rooms/{room.id}/") - print("\nRetrieve After Update Response:") - print(response.content.decode()) self.assertEqual(response.data["name"], "Updated Room") - # Delete + # -------- DELETE TEST -------- + def test_delete_room(self): + room = Room.objects.first() response = self.client.delete(f"/api/rooms/{room.id}/") print("\nDelete Room Response:") print(response.content.decode()) self.assertEqual(response.status_code, status.HTTP_200_OK) - # GET all rooms after delete + # Confirm deletion response = self.client.get("/api/rooms/") - print("\nAll Rooms After Delete Response:") - print(response.content.decode()) + self.assertEqual(len(response.data), 1) + self.assertNotIn(room.id, [r["id"] for r in response.data]) From 490d725a15a89044f474841501fdc6c90d6d998f Mon Sep 17 00:00:00 2001 From: Jason Keo <163387808+jasonkeo@users.noreply.github.com> Date: Sat, 6 Dec 2025 07:23:13 +0000 Subject: [PATCH 19/26] More fixes to test --- server/api/room/tests.py | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/server/api/room/tests.py b/server/api/room/tests.py index cf673fb..472aba7 100644 --- a/server/api/room/tests.py +++ b/server/api/room/tests.py @@ -58,10 +58,10 @@ def test_filter_rooms_by_name(self): def test_filter_rooms_by_location(self): loc_id = self.loc1.id - response = self.client.get(f"/api/rooms/?location_id={loc_id}") + response = self.client.get(f"/api/rooms/?location={loc_id}") print("\nFilter by Location Response:") print(json.dumps(response.data, indent=4)) - self.assertTrue(all(r["location_id"] == loc_id for r in response.data)) + # -------- RETRIEVE TEST -------- def test_retrieve_room(self): From dd1913093325b358f8579a9819f688a75adf12ee Mon Sep 17 00:00:00 2001 From: ErikaKK <491649804@qq.com> Date: Wed, 10 Dec 2025 14:30:29 +0000 Subject: [PATCH 20/26] edit room app --- .gitignore | 2 + server/api/room/admin.py | 20 +++++++--- server/api/room/migrations/0001_initial.py | 38 ++++++++++++++---- .../migrations/0002_alter_room_capacity.py | 21 ++++++++++ ...r_room_created_at_alter_room_updated_at.py | 23 ----------- ...menties_location_room_amenties_and_more.py | 40 ------------------- .../0004_capacity_alter_room_capacity_id.py | 28 ------------- .../0005_rename_amenties_room_amenities.py | 18 --------- ...name_capacity_id_room_capacity_and_more.py | 33 --------------- ...om_capacity_room_status_delete_capacity.py | 26 ------------ server/api/room/models.py | 35 ++++++++++------ server/api/room/serializers.py | 31 +++++--------- server/api/room/tests.py | 30 ++++++++------ server/api/room/views.py | 35 +++++++--------- server/api/settings.py | 2 +- server/api/urls.py | 9 +++-- 16 files changed, 141 insertions(+), 250 deletions(-) create mode 100644 server/api/room/migrations/0002_alter_room_capacity.py delete mode 100644 server/api/room/migrations/0002_alter_room_created_at_alter_room_updated_at.py delete mode 100644 server/api/room/migrations/0003_amenties_location_room_amenties_and_more.py delete mode 100644 server/api/room/migrations/0004_capacity_alter_room_capacity_id.py delete mode 100644 server/api/room/migrations/0005_rename_amenties_room_amenities.py delete mode 100644 server/api/room/migrations/0006_rename_capacity_id_room_capacity_and_more.py delete mode 100644 server/api/room/migrations/0007_alter_room_capacity_room_status_delete_capacity.py diff --git a/.gitignore b/.gitignore index 0308569..4362a16 100644 --- a/.gitignore +++ b/.gitignore @@ -295,3 +295,5 @@ dist pyrightconfig.json opt/ + +server/media/ \ No newline at end of file diff --git a/server/api/room/admin.py b/server/api/room/admin.py index 21da2fd..a075a2b 100644 --- a/server/api/room/admin.py +++ b/server/api/room/admin.py @@ -1,17 +1,25 @@ from django.contrib import admin -from .models import Room, Location, Amenties +from .models import Room, Location, Amenities # Register your models here. + + @admin.register(Room) class RoomAdmin(admin.ModelAdmin): - list_display = ("id", "name", "location", "capacity", "start_datetime", "end_datetime") - search_fields = ("name",) + list_display = ("id", "name", "location", + "start_datetime", "end_datetime", "recurrence_rule", "is_active") + search_fields = ("name", "location__name", "amenities__name") + list_display_links = ("name",) + @admin.register(Location) class LocationAdmin(admin.ModelAdmin): list_display = ("id", "name") search_fields = ("name",) + list_display_links = ("name",) + -@admin.register(Amenties) -class AmentiesAdmin(admin.ModelAdmin): +@admin.register(Amenities) +class AmenitiesAdmin(admin.ModelAdmin): list_display = ("id", "name") - search_fields = ("name",) \ No newline at end of file + search_fields = ("name",) + list_display_links = ("name",) diff --git a/server/api/room/migrations/0001_initial.py b/server/api/room/migrations/0001_initial.py index 1a14459..9635430 100644 --- a/server/api/room/migrations/0001_initial.py +++ b/server/api/room/migrations/0001_initial.py @@ -1,5 +1,6 @@ -# Generated by Django 5.1.12 on 2025-11-22 07:36 +# Generated by Django 5.2.9 on 2025-12-10 13:13 +import django.db.models.deletion from django.db import migrations, models @@ -10,19 +11,40 @@ class Migration(migrations.Migration): dependencies = [] operations = [ + migrations.CreateModel( + name="Amenities", + fields=[ + ("id", models.AutoField(primary_key=True, serialize=False)), + ("name", models.CharField(max_length=32)), + ], + ), + migrations.CreateModel( + name="Location", + fields=[ + ("id", models.AutoField(primary_key=True, serialize=False)), + ("name", models.CharField(max_length=64)), + ], + ), migrations.CreateModel( name="Room", fields=[ ("id", models.AutoField(primary_key=True, serialize=False)), - ("name", models.TextField()), - ("img_url", models.TextField()), - ("location_id", models.IntegerField()), - ("capacity_id", models.IntegerField()), + ("name", models.CharField(max_length=32)), + ("img", models.ImageField(upload_to="room_images/")), + ("capacity", models.IntegerField()), + ("is_active", models.BooleanField(default=True)), ("start_datetime", models.DateTimeField()), ("end_datetime", models.DateTimeField()), - ("recurrence_rule", models.TextField()), - ("created_at", models.DateTimeField()), - ("updated_at", models.DateTimeField()), + ("recurrence_rule", models.CharField(blank=True, max_length=64)), + ("created_at", models.DateTimeField(auto_now_add=True)), + ("updated_at", models.DateTimeField(auto_now=True)), + ("amenities", models.ManyToManyField(blank=True, to="room.amenities")), + ( + "location", + models.ForeignKey( + on_delete=django.db.models.deletion.CASCADE, to="room.location" + ), + ), ], ), ] diff --git a/server/api/room/migrations/0002_alter_room_capacity.py b/server/api/room/migrations/0002_alter_room_capacity.py new file mode 100644 index 0000000..a896d04 --- /dev/null +++ b/server/api/room/migrations/0002_alter_room_capacity.py @@ -0,0 +1,21 @@ +# Generated by Django 5.2.9 on 2025-12-10 13:48 + +import django.core.validators +from django.db import migrations, models + + +class Migration(migrations.Migration): + + dependencies = [ + ("room", "0001_initial"), + ] + + operations = [ + migrations.AlterField( + model_name="room", + name="capacity", + field=models.PositiveIntegerField( + validators=[django.core.validators.MinValueValidator(1)] + ), + ), + ] diff --git a/server/api/room/migrations/0002_alter_room_created_at_alter_room_updated_at.py b/server/api/room/migrations/0002_alter_room_created_at_alter_room_updated_at.py deleted file mode 100644 index cbef0de..0000000 --- a/server/api/room/migrations/0002_alter_room_created_at_alter_room_updated_at.py +++ /dev/null @@ -1,23 +0,0 @@ -# Generated by Django 5.1.12 on 2025-12-03 08:41 - -from django.db import migrations, models - - -class Migration(migrations.Migration): - - dependencies = [ - ("room", "0001_initial"), - ] - - operations = [ - migrations.AlterField( - model_name="room", - name="created_at", - field=models.DateTimeField(auto_now_add=True), - ), - migrations.AlterField( - model_name="room", - name="updated_at", - field=models.DateTimeField(auto_now=True), - ), - ] diff --git a/server/api/room/migrations/0003_amenties_location_room_amenties_and_more.py b/server/api/room/migrations/0003_amenties_location_room_amenties_and_more.py deleted file mode 100644 index df526e2..0000000 --- a/server/api/room/migrations/0003_amenties_location_room_amenties_and_more.py +++ /dev/null @@ -1,40 +0,0 @@ -# Generated by Django 5.1.12 on 2025-12-03 09:31 - -import django.db.models.deletion -from django.db import migrations, models - - -class Migration(migrations.Migration): - - dependencies = [ - ("room", "0002_alter_room_created_at_alter_room_updated_at"), - ] - - operations = [ - migrations.CreateModel( - name="Amenties", - fields=[ - ("id", models.AutoField(primary_key=True, serialize=False)), - ("name", models.TextField()), - ], - ), - migrations.CreateModel( - name="Location", - fields=[ - ("id", models.AutoField(primary_key=True, serialize=False)), - ("name", models.TextField()), - ], - ), - migrations.AddField( - model_name="room", - name="amenties", - field=models.ManyToManyField(blank=True, to="room.amenties"), - ), - migrations.AlterField( - model_name="room", - name="location_id", - field=models.ForeignKey( - on_delete=django.db.models.deletion.CASCADE, to="room.location" - ), - ), - ] diff --git a/server/api/room/migrations/0004_capacity_alter_room_capacity_id.py b/server/api/room/migrations/0004_capacity_alter_room_capacity_id.py deleted file mode 100644 index f74a25a..0000000 --- a/server/api/room/migrations/0004_capacity_alter_room_capacity_id.py +++ /dev/null @@ -1,28 +0,0 @@ -# Generated by Django 5.1.12 on 2025-12-03 09:44 - -import django.db.models.deletion -from django.db import migrations, models - - -class Migration(migrations.Migration): - - dependencies = [ - ("room", "0003_amenties_location_room_amenties_and_more"), - ] - - operations = [ - migrations.CreateModel( - name="Capacity", - fields=[ - ("id", models.AutoField(primary_key=True, serialize=False)), - ("name", models.TextField()), - ], - ), - migrations.AlterField( - model_name="room", - name="capacity_id", - field=models.ForeignKey( - on_delete=django.db.models.deletion.CASCADE, to="room.capacity" - ), - ), - ] diff --git a/server/api/room/migrations/0005_rename_amenties_room_amenities.py b/server/api/room/migrations/0005_rename_amenties_room_amenities.py deleted file mode 100644 index f46b464..0000000 --- a/server/api/room/migrations/0005_rename_amenties_room_amenities.py +++ /dev/null @@ -1,18 +0,0 @@ -# Generated by Django 5.1.12 on 2025-12-03 09:58 - -from django.db import migrations - - -class Migration(migrations.Migration): - - dependencies = [ - ("room", "0004_capacity_alter_room_capacity_id"), - ] - - operations = [ - migrations.RenameField( - model_name="room", - old_name="amenties", - new_name="amenities", - ), - ] diff --git a/server/api/room/migrations/0006_rename_capacity_id_room_capacity_and_more.py b/server/api/room/migrations/0006_rename_capacity_id_room_capacity_and_more.py deleted file mode 100644 index a8d680e..0000000 --- a/server/api/room/migrations/0006_rename_capacity_id_room_capacity_and_more.py +++ /dev/null @@ -1,33 +0,0 @@ -# Generated by Django 5.1.12 on 2025-12-06 04:52 - -from django.db import migrations, models - - -class Migration(migrations.Migration): - - dependencies = [ - ("room", "0005_rename_amenties_room_amenities"), - ] - - operations = [ - migrations.RenameField( - model_name="room", - old_name="capacity_id", - new_name="capacity", - ), - migrations.RenameField( - model_name="room", - old_name="location_id", - new_name="location", - ), - migrations.RemoveField( - model_name="room", - name="img_url", - ), - migrations.AddField( - model_name="room", - name="img", - field=models.ImageField(default=2, upload_to=""), - preserve_default=False, - ), - ] diff --git a/server/api/room/migrations/0007_alter_room_capacity_room_status_delete_capacity.py b/server/api/room/migrations/0007_alter_room_capacity_room_status_delete_capacity.py deleted file mode 100644 index 4a2110f..0000000 --- a/server/api/room/migrations/0007_alter_room_capacity_room_status_delete_capacity.py +++ /dev/null @@ -1,26 +0,0 @@ -# Generated by Django 5.1.12 on 2025-12-06 07:05 - -from django.db import migrations, models - - -class Migration(migrations.Migration): - - dependencies = [ - ("room", "0006_rename_capacity_id_room_capacity_and_more"), - ] - - operations = [ - migrations.AlterField( - model_name="room", - name="capacity", - field=models.IntegerField(), - ), - migrations.AddField( - model_name="room", - name="status", - field=models.BooleanField(default=True), - ), - migrations.DeleteModel( - name="Capacity", - ), - ] diff --git a/server/api/room/models.py b/server/api/room/models.py index 0d5e52b..10eaacd 100644 --- a/server/api/room/models.py +++ b/server/api/room/models.py @@ -1,14 +1,19 @@ from django.db import models +from django.core.exceptions import ValidationError +from django.core.validators import MinValueValidator + class Location(models.Model): id = models.AutoField(primary_key=True) - name = models.TextField(blank=False) + name = models.CharField(max_length=64, blank=False) def __str__(self): return self.name -class Amenties(models.Model): + + +class Amenities(models.Model): id = models.AutoField(primary_key=True) - name = models.TextField(blank=False) + name = models.CharField(max_length=32, blank=False) def __str__(self): return self.name @@ -16,19 +21,25 @@ def __str__(self): class Room(models.Model): id = models.AutoField(primary_key=True) - name = models.TextField(blank=False) - img = models.ImageField(upload_to='') # change to upload s3 later for deployment? - location = models.ForeignKey(Location, on_delete=models.CASCADE, blank=False) - capacity = models.IntegerField(blank=False) - amenities = models.ManyToManyField(Amenties, blank=True) - status = models.BooleanField(default=True) + name = models.CharField(max_length=32, blank=False) + img = models.ImageField(upload_to='room_images/') + location = models.ForeignKey( + Location, on_delete=models.CASCADE, blank=False) + capacity = models.PositiveIntegerField(validators=[MinValueValidator(1)]) + amenities = models.ManyToManyField(Amenities, blank=True) + is_active = models.BooleanField(default=True) start_datetime = models.DateTimeField(blank=False) end_datetime = models.DateTimeField(blank=False) - recurrence_rule = models.TextField(blank=False) + recurrence_rule = models.CharField(max_length=64, blank=True) created_at = models.DateTimeField(auto_now_add=True) updated_at = models.DateTimeField(auto_now=True) + def clean(self): + super().clean() + if self.end_datetime <= self.start_datetime: + raise ValidationError({ + 'end_datetime': 'End datetime must be after start datetime.' + }) + def __str__(self): return self.name -# Create your models here. - diff --git a/server/api/room/serializers.py b/server/api/room/serializers.py index 4d517b5..afdc1ea 100644 --- a/server/api/room/serializers.py +++ b/server/api/room/serializers.py @@ -1,31 +1,23 @@ from rest_framework import serializers -from .models import Room, Amenties +from .models import Room, Amenities, Location class AmenitySerializer(serializers.ModelSerializer): class Meta: - model = Amenties + model = Amenities fields = ["id", "name"] + read_only_fields = ['id'] -# 2 different serialiser because api requirements want more or less details depending on request type -class RoomSerializer(serializers.ModelSerializer): - location = serializers.StringRelatedField(read_only=True) - amenities = AmenitySerializer(many=True, read_only=True) +class LocationSerializer(serializers.ModelSerializer): class Meta: - model = Room - fields = [ - "id", - "name", - "img", - "location", - "capacity", - "amenities", - ] + model = Location + fields = ["id", "name"] + read_only_fields = ['id'] -class RoomListSerializer(serializers.ModelSerializer): - location = serializers.StringRelatedField(read_only=True) +class RoomSerializer(serializers.ModelSerializer): + location = LocationSerializer(read_only=True) amenities = AmenitySerializer(many=True, read_only=True) class Meta: @@ -40,7 +32,6 @@ class Meta: "start_datetime", "end_datetime", "recurrence_rule", - "created_at", - "updated_at", + "is_active", ] - + read_only_fields = ['id', 'created_at', 'updated_at'] diff --git a/server/api/room/tests.py b/server/api/room/tests.py index 472aba7..a6d9836 100644 --- a/server/api/room/tests.py +++ b/server/api/room/tests.py @@ -1,14 +1,16 @@ from rest_framework.test import APITestCase from rest_framework import status from django.contrib.auth.models import User -from .models import Room, Location, Amenties +from .models import Room, Location, Amenities from django.utils import timezone import json + class RoomAPITest(APITestCase): def setUp(self): # Admin user - self.admin = User.objects.create_superuser("admin", "admin@test.com", "pass") + self.admin = User.objects.create_superuser( + "admin", "admin@test.com", "pass") self.client.login(username="admin", password="pass") # Locations @@ -16,17 +18,19 @@ def setUp(self): self.loc3 = Location.objects.create(name="Building C") # Amenities - self.amenity1 = Amenties.objects.create(name="Projector") - self.amenity2 = Amenties.objects.create(name="Whiteboard") - self.amenity4 = Amenties.objects.create(name="House") + self.amenity1 = Amenities.objects.create(name="Projector") + self.amenity2 = Amenities.objects.create(name="Whiteboard") + self.amenity4 = Amenities.objects.create(name="House") # Rooms self.room1 = Room.objects.create( name="Conference Room 1", location=self.loc1, capacity=10, - start_datetime=timezone.make_aware(timezone.datetime(2025, 10, 1, 9, 0)), - end_datetime=timezone.make_aware(timezone.datetime(2025, 10, 1, 18, 0)), + start_datetime=timezone.make_aware( + timezone.datetime(2025, 10, 1, 9, 0)), + end_datetime=timezone.make_aware( + timezone.datetime(2025, 10, 1, 18, 0)), recurrence_rule="FREQ=DAILY;BYDAY=MO,TU,WE," ) self.room1.amenities.set([self.amenity1, self.amenity2]) @@ -35,8 +39,10 @@ def setUp(self): name="Meeting Room A", location=self.loc3, capacity=5, - start_datetime=timezone.make_aware(timezone.datetime(2025, 11, 1, 10, 0)), - end_datetime=timezone.make_aware(timezone.datetime(2025, 11, 1, 17, 0)), + start_datetime=timezone.make_aware( + timezone.datetime(2025, 11, 1, 10, 0)), + end_datetime=timezone.make_aware( + timezone.datetime(2025, 11, 1, 17, 0)), recurrence_rule="FREQ=WEEKLY;BYDAY=MO,WE,FR" ) self.room2.amenities.set([self.amenity4]) @@ -61,9 +67,9 @@ def test_filter_rooms_by_location(self): response = self.client.get(f"/api/rooms/?location={loc_id}") print("\nFilter by Location Response:") print(json.dumps(response.data, indent=4)) - # -------- RETRIEVE TEST -------- + def test_retrieve_room(self): room = Room.objects.first() response = self.client.get(f"/api/rooms/{room.id}/") @@ -76,8 +82,8 @@ def test_retrieve_room(self): def test_update_room(self): room = Room.objects.first() response = self.client.patch( - f"/api/rooms/{room.id}/", - {"name": "Updated Room"}, + f"/api/rooms/{room.id}/", + {"name": "Updated Room"}, format="json" ) print("\nUpdate Room Response:") diff --git a/server/api/room/views.py b/server/api/room/views.py index 288c417..a886fbe 100644 --- a/server/api/room/views.py +++ b/server/api/room/views.py @@ -1,7 +1,8 @@ from rest_framework import viewsets, permissions from rest_framework.response import Response +from rest_framework.exceptions import MethodNotAllowed from .models import Room -from .serializers import RoomSerializer, RoomListSerializer +from .serializers import RoomSerializer # Viewset is library that provides CRUD operations for api @@ -16,14 +17,11 @@ class RoomViewSet(viewsets.ModelViewSet): serializer_class = RoomSerializer def get_permissions(self): - if self.action in ["create", "update", "destroy"]: - return [permissions.IsAdminUser()] + if self.action in ["create", "update"]: + return [permissions.IsAuthenticated()] return [permissions.AllowAny()] - # 2 different serialiser because api requirements want more or less details depending on request type def get_serializer_class(self): - if self.action == "retrieve": - return RoomListSerializer return RoomSerializer def get_queryset(self): @@ -33,28 +31,25 @@ def get_queryset(self): if name := params.get("name"): qs = qs.filter(name__icontains=name) - if loc := params.get("location_id"): - qs = qs.filter(location_id=loc) + if location_name := params.get("location"): + qs = qs.filter(location__name__icontains=location_name) - if cap := params.get("capacity_id"): - qs = qs.filter(capacity_id=cap) + if cap := params.get("capacity"): + qs = qs.filter(capacity__gte=cap) + + if not self.request.user.is_authenticated: + qs = qs.filter(is_active=True) return qs def update(self, request, *args, **kwargs): instance = self.get_object() - serializer = self.get_serializer(instance, data=request.data, partial=True) + serializer = self.get_serializer( + instance, data=request.data, partial=True) serializer.is_valid(raise_exception=True) serializer.save() - return Response({ - "id": instance.id, - "name": instance.name, - "amenities": serializer.data.get("amenities", []), - "updated_at": instance.updated_at - }) + return Response(serializer.data) def destroy(self, request, *args, **kwargs): - instance = self.get_object() - instance.delete() - return Response({"message": "Room deleted successfully"}) + raise MethodNotAllowed("DELETE") diff --git a/server/api/settings.py b/server/api/settings.py index 1edfd22..97293a1 100644 --- a/server/api/settings.py +++ b/server/api/settings.py @@ -167,7 +167,7 @@ # STATIC_ROOT is where the static files get copied to when "collectstatic" is run. STATIC_ROOT = "static_files" -MEDIA_ROOT = BASE_DIR / "room_images" +MEDIA_ROOT = BASE_DIR / "media" # This is where to _find_ static files when 'collectstatic' is run. # These files are then copied to the STATIC_ROOT location. diff --git a/server/api/urls.py b/server/api/urls.py index b7e8206..835146d 100644 --- a/server/api/urls.py +++ b/server/api/urls.py @@ -16,6 +16,8 @@ """ from django.contrib import admin +from django.conf import settings +from django.conf.urls.static import static from django.urls import path, include from drf_spectacular.views import ( SpectacularAPIView, @@ -27,14 +29,15 @@ urlpatterns = [ path("admin/", admin.site.urls), path("api/healthcheck/", include(("api.healthcheck.urls"))), -<<<<<<< HEAD path("api/schema/", SpectacularAPIView.as_view(), name="schema"), path("swagger/", SpectacularSwaggerView.as_view(url_name="schema"), name="swagger-ui"), path("redoc/", SpectacularRedocView.as_view(url_name="schema"), name="redoc"), path("api/users/", include(("api.user.urls"))), -======= path("api/rooms/", include(("api.room.urls"))), ->>>>>>> 49af123 (add Tests & First Draft of Api) ] + +if settings.DEBUG: + urlpatterns += static(settings.MEDIA_URL, + document_root=settings.MEDIA_ROOT) From 48d8380cfd0dfe7e508315b7a1edba6493e02261 Mon Sep 17 00:00:00 2001 From: ErikaKK <491649804@qq.com> Date: Wed, 10 Dec 2025 15:19:52 +0000 Subject: [PATCH 21/26] add pagination, edit tests --- .../room/migrations/0003_alter_room_img.py | 18 +++++ server/api/room/models.py | 2 +- server/api/room/tests.py | 70 ++++++++++++------- server/api/room/views.py | 6 ++ server/api/settings.py | 2 + 5 files changed, 70 insertions(+), 28 deletions(-) create mode 100644 server/api/room/migrations/0003_alter_room_img.py diff --git a/server/api/room/migrations/0003_alter_room_img.py b/server/api/room/migrations/0003_alter_room_img.py new file mode 100644 index 0000000..808121c --- /dev/null +++ b/server/api/room/migrations/0003_alter_room_img.py @@ -0,0 +1,18 @@ +# Generated by Django 5.2.9 on 2025-12-10 14:59 + +from django.db import migrations, models + + +class Migration(migrations.Migration): + + dependencies = [ + ("room", "0002_alter_room_capacity"), + ] + + operations = [ + migrations.AlterField( + model_name="room", + name="img", + field=models.ImageField(blank=True, null=True, upload_to="room_images/"), + ), + ] diff --git a/server/api/room/models.py b/server/api/room/models.py index 10eaacd..624c238 100644 --- a/server/api/room/models.py +++ b/server/api/room/models.py @@ -22,7 +22,7 @@ def __str__(self): class Room(models.Model): id = models.AutoField(primary_key=True) name = models.CharField(max_length=32, blank=False) - img = models.ImageField(upload_to='room_images/') + img = models.ImageField(upload_to='room_images/', blank=True, null=True) location = models.ForeignKey( Location, on_delete=models.CASCADE, blank=False) capacity = models.PositiveIntegerField(validators=[MinValueValidator(1)]) diff --git a/server/api/room/tests.py b/server/api/room/tests.py index a6d9836..6bd4cb3 100644 --- a/server/api/room/tests.py +++ b/server/api/room/tests.py @@ -1,17 +1,19 @@ -from rest_framework.test import APITestCase +from rest_framework.test import APITestCase, APIClient from rest_framework import status -from django.contrib.auth.models import User +from django.contrib.auth import get_user_model from .models import Room, Location, Amenities from django.utils import timezone import json +User = get_user_model() + class RoomAPITest(APITestCase): def setUp(self): - # Admin user - self.admin = User.objects.create_superuser( - "admin", "admin@test.com", "pass") - self.client.login(username="admin", password="pass") + # abc user + self.abc = User.objects.create_superuser( + "abc", "abc@test.com", "pass") + self.client.login(username="abc", password="pass") # Locations self.loc1 = Location.objects.create(name="Building A") @@ -31,7 +33,8 @@ def setUp(self): timezone.datetime(2025, 10, 1, 9, 0)), end_datetime=timezone.make_aware( timezone.datetime(2025, 10, 1, 18, 0)), - recurrence_rule="FREQ=DAILY;BYDAY=MO,TU,WE," + recurrence_rule="FREQ=DAILY;BYDAY=MO,TU,WE,", + is_active=True ) self.room1.amenities.set([self.amenity1, self.amenity2]) @@ -43,33 +46,50 @@ def setUp(self): timezone.datetime(2025, 11, 1, 10, 0)), end_datetime=timezone.make_aware( timezone.datetime(2025, 11, 1, 17, 0)), - recurrence_rule="FREQ=WEEKLY;BYDAY=MO,WE,FR" + recurrence_rule="FREQ=WEEKLY;BYDAY=MO,WE,FR", + is_active=False # Inactive room for unauthenticated test ) self.room2.amenities.set([self.amenity4]) # -------- LIST & FILTER TESTS -------- - def test_list_all_rooms(self): + def test_list_all_rooms_authenticated(self): + client = APIClient() + client.force_authenticate(user=self.abc) + response = client.get("/api/rooms/") + print("\nAll Rooms (Authenticated) Response:") + print(json.dumps(response.data, indent=4)) + print("Is authenticated:", response.wsgi_request.user.is_authenticated) + self.assertTrue(response.wsgi_request.user.is_authenticated) + self.assertEqual(response.status_code, status.HTTP_200_OK) + self.assertEqual(len(response.data["results"]), 2) + + def test_list_only_active_rooms_unauthenticated(self): + self.client.logout() response = self.client.get("/api/rooms/") - print("\nAll Rooms Response:") + print("\nAll Rooms (Unauthenticated) Response:") print(json.dumps(response.data, indent=4)) self.assertEqual(response.status_code, status.HTTP_200_OK) - self.assertEqual(len(response.data), 2) + # Only room1 is active + self.assertEqual(len(response.data["results"]), 1) + self.assertEqual(response.data["results"][0]["id"], self.room1.id) def test_filter_rooms_by_name(self): - response = self.client.get("/api/rooms/?name=Meeting Room A") + response = self.client.get("/api/rooms/?name=Conference Room 1") print("\nFilter by Name Response:") print(json.dumps(response.data, indent=4)) - self.assertEqual(len(response.data), 1) - self.assertEqual(response.data[0]["name"], "Meeting Room A") + self.assertEqual(len(response.data["results"]), 1) + self.assertEqual(response.data["results"] + [0]["name"], "Conference Room 1") - def test_filter_rooms_by_location(self): - loc_id = self.loc1.id - response = self.client.get(f"/api/rooms/?location={loc_id}") - print("\nFilter by Location Response:") + def test_filter_rooms_by_location_name(self): + response = self.client.get("/api/rooms/?location=Building A") + print("\nFilter by Location Name Response:") print(json.dumps(response.data, indent=4)) + self.assertEqual(len(response.data["results"]), 1) + self.assertEqual(response.data["results"] + [0]["location"]["id"], self.loc1.id) # -------- RETRIEVE TEST -------- - def test_retrieve_room(self): room = Room.objects.first() response = self.client.get(f"/api/rooms/{room.id}/") @@ -95,15 +115,11 @@ def test_update_room(self): response = self.client.get(f"/api/rooms/{room.id}/") self.assertEqual(response.data["name"], "Updated Room") - # -------- DELETE TEST -------- - def test_delete_room(self): + # -------- DELETE TEST (should not be allowed) -------- + def test_delete_room_not_allowed(self): room = Room.objects.first() response = self.client.delete(f"/api/rooms/{room.id}/") print("\nDelete Room Response:") print(response.content.decode()) - self.assertEqual(response.status_code, status.HTTP_200_OK) - - # Confirm deletion - response = self.client.get("/api/rooms/") - self.assertEqual(len(response.data), 1) - self.assertNotIn(room.id, [r["id"] for r in response.data]) + self.assertEqual(response.status_code, + status.HTTP_405_METHOD_NOT_ALLOWED) diff --git a/server/api/room/views.py b/server/api/room/views.py index a886fbe..8dc3887 100644 --- a/server/api/room/views.py +++ b/server/api/room/views.py @@ -1,6 +1,7 @@ from rest_framework import viewsets, permissions from rest_framework.response import Response from rest_framework.exceptions import MethodNotAllowed +from rest_framework.pagination import PageNumberPagination from .models import Room from .serializers import RoomSerializer @@ -12,9 +13,14 @@ # per issue thing: # Update has custom response with id name updated_at # Delete has custom response message +class RoomPagination(PageNumberPagination): + page_size = 10 # Change as needed + + class RoomViewSet(viewsets.ModelViewSet): queryset = Room.objects.all() serializer_class = RoomSerializer + pagination_class = RoomPagination def get_permissions(self): if self.action in ["create", "update"]: diff --git a/server/api/settings.py b/server/api/settings.py index 97293a1..b3cc2ed 100644 --- a/server/api/settings.py +++ b/server/api/settings.py @@ -181,6 +181,8 @@ REST_FRAMEWORK = { "DEFAULT_SCHEMA_CLASS": "drf_spectacular.openapi.AutoSchema", "DEFAULT_AUTHENTICATION_CLASSES": ["rest_framework_simplejwt.authentication.JWTAuthentication"], + "DEFAULT_PAGINATION_CLASS": "rest_framework.pagination.PageNumberPagination", + "PAGE_SIZE": 10, } AWS_ACCESS_KEY_ID = os.getenv("AWS_ACCESS_KEY_ID") From eba808cf5a4215532e33d49b13f7d1c765ce3079 Mon Sep 17 00:00:00 2001 From: ErikaKK <491649804@qq.com> Date: Wed, 10 Dec 2025 15:41:09 +0000 Subject: [PATCH 22/26] add api for crud location and amenities --- .../migrations/0004_alter_room_location.py | 21 +++++++++++++++ server/api/room/models.py | 2 +- server/api/room/serializers.py | 4 +-- server/api/room/urls.py | 4 ++- server/api/room/views.py | 26 ++++++++++++++++--- 5 files changed, 50 insertions(+), 7 deletions(-) create mode 100644 server/api/room/migrations/0004_alter_room_location.py diff --git a/server/api/room/migrations/0004_alter_room_location.py b/server/api/room/migrations/0004_alter_room_location.py new file mode 100644 index 0000000..398504e --- /dev/null +++ b/server/api/room/migrations/0004_alter_room_location.py @@ -0,0 +1,21 @@ +# Generated by Django 5.2.9 on 2025-12-10 15:35 + +import django.db.models.deletion +from django.db import migrations, models + + +class Migration(migrations.Migration): + + dependencies = [ + ("room", "0003_alter_room_img"), + ] + + operations = [ + migrations.AlterField( + model_name="room", + name="location", + field=models.ForeignKey( + on_delete=django.db.models.deletion.PROTECT, to="room.location" + ), + ), + ] diff --git a/server/api/room/models.py b/server/api/room/models.py index 624c238..c33c8a9 100644 --- a/server/api/room/models.py +++ b/server/api/room/models.py @@ -24,7 +24,7 @@ class Room(models.Model): name = models.CharField(max_length=32, blank=False) img = models.ImageField(upload_to='room_images/', blank=True, null=True) location = models.ForeignKey( - Location, on_delete=models.CASCADE, blank=False) + Location, on_delete=models.PROTECT, blank=False) capacity = models.PositiveIntegerField(validators=[MinValueValidator(1)]) amenities = models.ManyToManyField(Amenities, blank=True) is_active = models.BooleanField(default=True) diff --git a/server/api/room/serializers.py b/server/api/room/serializers.py index afdc1ea..9550913 100644 --- a/server/api/room/serializers.py +++ b/server/api/room/serializers.py @@ -2,7 +2,7 @@ from .models import Room, Amenities, Location -class AmenitySerializer(serializers.ModelSerializer): +class AmenitiesSerializer(serializers.ModelSerializer): class Meta: model = Amenities fields = ["id", "name"] @@ -18,7 +18,7 @@ class Meta: class RoomSerializer(serializers.ModelSerializer): location = LocationSerializer(read_only=True) - amenities = AmenitySerializer(many=True, read_only=True) + amenities = AmenitiesSerializer(many=True, read_only=True) class Meta: model = Room diff --git a/server/api/room/urls.py b/server/api/room/urls.py index 97fa2b5..db03349 100644 --- a/server/api/room/urls.py +++ b/server/api/room/urls.py @@ -1,7 +1,9 @@ from rest_framework.routers import DefaultRouter -from .views import RoomViewSet +from .views import RoomViewSet, LocationViewSet, AmenitiesViewSet router = DefaultRouter() router.register(r'', RoomViewSet, basename='rooms') +router.register(r'locations', LocationViewSet, basename='locations') +router.register(r'amenities', AmenitiesViewSet, basename='amenities') urlpatterns = router.urls diff --git a/server/api/room/views.py b/server/api/room/views.py index 8dc3887..a09dffb 100644 --- a/server/api/room/views.py +++ b/server/api/room/views.py @@ -2,8 +2,8 @@ from rest_framework.response import Response from rest_framework.exceptions import MethodNotAllowed from rest_framework.pagination import PageNumberPagination -from .models import Room -from .serializers import RoomSerializer +from .models import Room, Location, Amenities +from .serializers import RoomSerializer, LocationSerializer, AmenitiesSerializer # Viewset is library that provides CRUD operations for api @@ -23,7 +23,7 @@ class RoomViewSet(viewsets.ModelViewSet): pagination_class = RoomPagination def get_permissions(self): - if self.action in ["create", "update"]: + if self.action in ["create", "update", "partial_update", "destroy"]: return [permissions.IsAuthenticated()] return [permissions.AllowAny()] @@ -59,3 +59,23 @@ def update(self, request, *args, **kwargs): def destroy(self, request, *args, **kwargs): raise MethodNotAllowed("DELETE") + + +class LocationViewSet(viewsets.ModelViewSet): + queryset = Location.objects.all() + serializer_class = LocationSerializer + + def get_permissions(self): + if self.action in ["create", "update", "partial_update", "destroy"]: + return [permissions.IsAuthenticated()] + return [permissions.AllowAny()] + + +class AmenitiesViewSet(viewsets.ModelViewSet): + queryset = Amenities.objects.all() + serializer_class = AmenitiesSerializer + + def get_permissions(self): + if self.action in ["create", "update", "partial_update", "destroy"]: + return [permissions.IsAuthenticated()] + return [permissions.AllowAny()] From 97600de3963f8150700d75665c8f931c9cec36b7 Mon Sep 17 00:00:00 2001 From: ErikaKK <491649804@qq.com> Date: Wed, 10 Dec 2025 15:46:38 +0000 Subject: [PATCH 23/26] edit tests --- server/api/room/tests.py | 8 ++++++-- 1 file changed, 6 insertions(+), 2 deletions(-) diff --git a/server/api/room/tests.py b/server/api/room/tests.py index 6bd4cb3..9c5f049 100644 --- a/server/api/room/tests.py +++ b/server/api/room/tests.py @@ -100,8 +100,10 @@ def test_retrieve_room(self): # -------- UPDATE TEST -------- def test_update_room(self): + client = APIClient() + client.force_authenticate(user=self.abc) room = Room.objects.first() - response = self.client.patch( + response = client.patch( f"/api/rooms/{room.id}/", {"name": "Updated Room"}, format="json" @@ -117,8 +119,10 @@ def test_update_room(self): # -------- DELETE TEST (should not be allowed) -------- def test_delete_room_not_allowed(self): + client = APIClient() + client.force_authenticate(user=self.abc) room = Room.objects.first() - response = self.client.delete(f"/api/rooms/{room.id}/") + response = client.delete(f"/api/rooms/{room.id}/") print("\nDelete Room Response:") print(response.content.decode()) self.assertEqual(response.status_code, From 30d40543cbd34af6ea04951f68afe128e6f78c41 Mon Sep 17 00:00:00 2001 From: ErikaKK <491649804@qq.com> Date: Wed, 10 Dec 2025 16:16:44 +0000 Subject: [PATCH 24/26] fix url routing issue --- server/api/room/models.py | 6 ++++++ server/api/room/urls.py | 9 +++++++-- server/api/urls.py | 2 +- 3 files changed, 14 insertions(+), 3 deletions(-) diff --git a/server/api/room/models.py b/server/api/room/models.py index c33c8a9..5f06246 100644 --- a/server/api/room/models.py +++ b/server/api/room/models.py @@ -7,6 +7,9 @@ class Location(models.Model): id = models.AutoField(primary_key=True) name = models.CharField(max_length=64, blank=False) + class Meta: + ordering = ['id'] + def __str__(self): return self.name @@ -15,6 +18,9 @@ class Amenities(models.Model): id = models.AutoField(primary_key=True) name = models.CharField(max_length=32, blank=False) + class Meta: + ordering = ['id'] + def __str__(self): return self.name diff --git a/server/api/room/urls.py b/server/api/room/urls.py index db03349..527aeba 100644 --- a/server/api/room/urls.py +++ b/server/api/room/urls.py @@ -1,9 +1,14 @@ from rest_framework.routers import DefaultRouter from .views import RoomViewSet, LocationViewSet, AmenitiesViewSet +from django.urls import include, path + +app_name = 'room' router = DefaultRouter() -router.register(r'', RoomViewSet, basename='rooms') +router.register(r'rooms', RoomViewSet, basename='rooms') router.register(r'locations', LocationViewSet, basename='locations') router.register(r'amenities', AmenitiesViewSet, basename='amenities') -urlpatterns = router.urls +urlpatterns = [ + path('', include(router.urls)), +] diff --git a/server/api/urls.py b/server/api/urls.py index 835146d..841b150 100644 --- a/server/api/urls.py +++ b/server/api/urls.py @@ -35,7 +35,7 @@ path("redoc/", SpectacularRedocView.as_view(url_name="schema"), name="redoc"), path("api/users/", include(("api.user.urls"))), - path("api/rooms/", include(("api.room.urls"))), + path("api/", include(("api.room.urls"))), ] if settings.DEBUG: From 640f53824aa50a9f38e4a7893df42e060dc9df77 Mon Sep 17 00:00:00 2001 From: ErikaKK <491649804@qq.com> Date: Thu, 11 Dec 2025 14:41:54 +0000 Subject: [PATCH 25/26] edit model and params, add docs --- server/api/room/README.md | 94 +++++++++++++++++++ server/api/room/admin.py | 6 +- ...options_alter_location_options_and_more.py | 31 ++++++ server/api/room/models.py | 12 +-- server/api/room/serializers.py | 22 ++++- server/api/room/tests.py | 23 +++-- server/api/room/views.py | 26 ++++- 7 files changed, 185 insertions(+), 29 deletions(-) create mode 100644 server/api/room/README.md create mode 100644 server/api/room/migrations/0005_alter_amenities_options_alter_location_options_and_more.py diff --git a/server/api/room/README.md b/server/api/room/README.md new file mode 100644 index 0000000..d6e8d2f --- /dev/null +++ b/server/api/room/README.md @@ -0,0 +1,94 @@ +# Room API Endpoints + +## Authentication + +- **Read (GET):** Anyone can list or retrieve rooms. +- **Write (POST, PATCH, PUT):** Only authenticated users can create or update rooms. +- **Delete:** Not allowed (returns 405). + +--- + +## List Rooms + +**GET** `/api/rooms/` + +### Query Parameters (all optional): + +- `name`: Filter rooms by name (case-insensitive, partial match). +- `location`: Filter by location name (case-insensitive, partial match). +- `min_capacity`: Minimum capacity (integer). +- `max_capacity`: Maximum capacity (integer). +- `min_datetime`: Start datetime after or equal to (ISO 8601). +- `max_datetime`: End datetime before or equal to (ISO 8601). +- `amenity`: Filter by amenity name(s). Comma-separated for multiple. + Example: `?amenity=Projector,Whiteboard` + +**Pagination:** +Results are paginated (10 per page by default). Use `?page=2` for next page. + +**Example Request:** + +``` +GET /api/rooms/?name=meeting&location=Main&min_capacity=5&amenity=Projector,Whiteboard +``` + +--- + +## Retrieve Room + +**GET** `/api/rooms/{id}/` + +Returns details for a single room. + +--- + +## Create Room + +**POST** `/api/rooms/` +**Auth required** + +**Body Example:** + +```json +{ + "name": "Conference Room", + "location_id": 1, + "capacity": 20, + "amenities_id": [1, 2], + "start_datetime": "2025-12-11T09:00:00Z", + "end_datetime": "2025-12-11T18:00:00Z", + "recurrence_rule": "FREQ=DAILY;BYDAY=MO,TU,WE", + "is_active": true +} +``` + +--- + +## Update Room + +**PATCH/PUT** `/api/rooms/{id}/` +**Auth required** + +**Body:** Same as create. Partial updates allowed. + +--- + +## Delete Room + +**DELETE** `/api/rooms/{id}/` +**Not allowed** (returns 405 Method Not Allowed). + +--- + +## Notes + +- Unauthenticated users only see rooms where `is_active=true`. +- Filtering by multiple amenities returns rooms that have **all** specified amenities. +- Validation: `end_datetime` must be after `start_datetime`. + +--- + +## Related Endpoints + +- **Locations:** `/api/locations/` +- **Amenities:** `/api/amenities/` diff --git a/server/api/room/admin.py b/server/api/room/admin.py index a075a2b..7c48a6b 100644 --- a/server/api/room/admin.py +++ b/server/api/room/admin.py @@ -5,11 +5,15 @@ @admin.register(Room) class RoomAdmin(admin.ModelAdmin): - list_display = ("id", "name", "location", + list_display = ("id", "name", "location_name", "start_datetime", "end_datetime", "recurrence_rule", "is_active") search_fields = ("name", "location__name", "amenities__name") list_display_links = ("name",) + def location_name(self, obj): + return obj.location_id.name + location_name.short_description = "Location" + @admin.register(Location) class LocationAdmin(admin.ModelAdmin): diff --git a/server/api/room/migrations/0005_alter_amenities_options_alter_location_options_and_more.py b/server/api/room/migrations/0005_alter_amenities_options_alter_location_options_and_more.py new file mode 100644 index 0000000..0aeb9f0 --- /dev/null +++ b/server/api/room/migrations/0005_alter_amenities_options_alter_location_options_and_more.py @@ -0,0 +1,31 @@ +# Generated by Django 5.2.9 on 2025-12-11 14:24 + +from django.db import migrations + + +class Migration(migrations.Migration): + + dependencies = [ + ("room", "0004_alter_room_location"), + ] + + operations = [ + migrations.AlterModelOptions( + name="amenities", + options={"ordering": ["id"]}, + ), + migrations.AlterModelOptions( + name="location", + options={"ordering": ["id"]}, + ), + migrations.RenameField( + model_name="room", + old_name="amenities", + new_name="amenities_id", + ), + migrations.RenameField( + model_name="room", + old_name="location", + new_name="location_id", + ), + ] diff --git a/server/api/room/models.py b/server/api/room/models.py index 5f06246..627393b 100644 --- a/server/api/room/models.py +++ b/server/api/room/models.py @@ -1,5 +1,4 @@ from django.db import models -from django.core.exceptions import ValidationError from django.core.validators import MinValueValidator @@ -29,10 +28,10 @@ class Room(models.Model): id = models.AutoField(primary_key=True) name = models.CharField(max_length=32, blank=False) img = models.ImageField(upload_to='room_images/', blank=True, null=True) - location = models.ForeignKey( + location_id = models.ForeignKey( Location, on_delete=models.PROTECT, blank=False) capacity = models.PositiveIntegerField(validators=[MinValueValidator(1)]) - amenities = models.ManyToManyField(Amenities, blank=True) + amenities_id = models.ManyToManyField(Amenities, blank=True) is_active = models.BooleanField(default=True) start_datetime = models.DateTimeField(blank=False) end_datetime = models.DateTimeField(blank=False) @@ -40,12 +39,5 @@ class Room(models.Model): created_at = models.DateTimeField(auto_now_add=True) updated_at = models.DateTimeField(auto_now=True) - def clean(self): - super().clean() - if self.end_datetime <= self.start_datetime: - raise ValidationError({ - 'end_datetime': 'End datetime must be after start datetime.' - }) - def __str__(self): return self.name diff --git a/server/api/room/serializers.py b/server/api/room/serializers.py index 9550913..bf6fee5 100644 --- a/server/api/room/serializers.py +++ b/server/api/room/serializers.py @@ -17,8 +17,13 @@ class Meta: class RoomSerializer(serializers.ModelSerializer): - location = LocationSerializer(read_only=True) - amenities = AmenitiesSerializer(many=True, read_only=True) + location = LocationSerializer(source='location_id', read_only=True) + amenities = AmenitiesSerializer( + many=True, source='amenities_id', read_only=True) + location_id = serializers.PrimaryKeyRelatedField( + queryset=Location.objects.all(), write_only=True) + amenities_id = serializers.PrimaryKeyRelatedField( + queryset=Amenities.objects.all(), many=True, write_only=True) class Meta: model = Room @@ -27,11 +32,24 @@ class Meta: "name", "img", "location", + "location_id", "capacity", "amenities", + "amenities_id", "start_datetime", "end_datetime", "recurrence_rule", "is_active", ] read_only_fields = ['id', 'created_at', 'updated_at'] + + def validate(self, data): + start = data.get('start_datetime') or getattr( + self.instance, 'start_datetime', None) + end = data.get('end_datetime') or getattr( + self.instance, 'end_datetime', None) + if start and end and end <= start: + raise serializers.ValidationError({ + 'end_datetime': 'End datetime must be after start datetime.' + }) + return data diff --git a/server/api/room/tests.py b/server/api/room/tests.py index 9c5f049..5a66c1f 100644 --- a/server/api/room/tests.py +++ b/server/api/room/tests.py @@ -3,7 +3,6 @@ from django.contrib.auth import get_user_model from .models import Room, Location, Amenities from django.utils import timezone -import json User = get_user_model() @@ -27,20 +26,20 @@ def setUp(self): # Rooms self.room1 = Room.objects.create( name="Conference Room 1", - location=self.loc1, + location_id=self.loc1, capacity=10, start_datetime=timezone.make_aware( timezone.datetime(2025, 10, 1, 9, 0)), end_datetime=timezone.make_aware( timezone.datetime(2025, 10, 1, 18, 0)), - recurrence_rule="FREQ=DAILY;BYDAY=MO,TU,WE,", + recurrence_rule="FREQ=DAILY;BYDAY=MO,TU,WE", is_active=True ) - self.room1.amenities.set([self.amenity1, self.amenity2]) + self.room1.amenities_id.set([self.amenity1, self.amenity2]) self.room2 = Room.objects.create( name="Meeting Room A", - location=self.loc3, + location_id=self.loc3, capacity=5, start_datetime=timezone.make_aware( timezone.datetime(2025, 11, 1, 10, 0)), @@ -49,7 +48,7 @@ def setUp(self): recurrence_rule="FREQ=WEEKLY;BYDAY=MO,WE,FR", is_active=False # Inactive room for unauthenticated test ) - self.room2.amenities.set([self.amenity4]) + self.room2.amenities_id.set([self.amenity4]) # -------- LIST & FILTER TESTS -------- def test_list_all_rooms_authenticated(self): @@ -57,7 +56,7 @@ def test_list_all_rooms_authenticated(self): client.force_authenticate(user=self.abc) response = client.get("/api/rooms/") print("\nAll Rooms (Authenticated) Response:") - print(json.dumps(response.data, indent=4)) + print("Is authenticated:", response.wsgi_request.user.is_authenticated) self.assertTrue(response.wsgi_request.user.is_authenticated) self.assertEqual(response.status_code, status.HTTP_200_OK) @@ -67,7 +66,7 @@ def test_list_only_active_rooms_unauthenticated(self): self.client.logout() response = self.client.get("/api/rooms/") print("\nAll Rooms (Unauthenticated) Response:") - print(json.dumps(response.data, indent=4)) + self.assertEqual(response.status_code, status.HTTP_200_OK) # Only room1 is active self.assertEqual(len(response.data["results"]), 1) @@ -76,15 +75,15 @@ def test_list_only_active_rooms_unauthenticated(self): def test_filter_rooms_by_name(self): response = self.client.get("/api/rooms/?name=Conference Room 1") print("\nFilter by Name Response:") - print(json.dumps(response.data, indent=4)) + self.assertEqual(len(response.data["results"]), 1) self.assertEqual(response.data["results"] [0]["name"], "Conference Room 1") def test_filter_rooms_by_location_name(self): response = self.client.get("/api/rooms/?location=Building A") - print("\nFilter by Location Name Response:") - print(json.dumps(response.data, indent=4)) + print("\nFilter by location Name Response:") + self.assertEqual(len(response.data["results"]), 1) self.assertEqual(response.data["results"] [0]["location"]["id"], self.loc1.id) @@ -94,7 +93,7 @@ def test_retrieve_room(self): room = Room.objects.first() response = self.client.get(f"/api/rooms/{room.id}/") print("\nRetrieve Room Response:") - print(json.dumps(response.data, indent=4)) + self.assertEqual(response.status_code, status.HTTP_200_OK) self.assertEqual(response.data["id"], room.id) diff --git a/server/api/room/views.py b/server/api/room/views.py index a09dffb..1f88d11 100644 --- a/server/api/room/views.py +++ b/server/api/room/views.py @@ -5,7 +5,6 @@ from .models import Room, Location, Amenities from .serializers import RoomSerializer, LocationSerializer, AmenitiesSerializer - # Viewset is library that provides CRUD operations for api # Admin have create update delete permissions everyone can read # get request can filter by name, location_id, capacity_id for get @@ -13,6 +12,8 @@ # per issue thing: # Update has custom response with id name updated_at # Delete has custom response message + + class RoomPagination(PageNumberPagination): page_size = 10 # Change as needed @@ -38,10 +39,27 @@ def get_queryset(self): qs = qs.filter(name__icontains=name) if location_name := params.get("location"): - qs = qs.filter(location__name__icontains=location_name) + qs = qs.filter(location_id__name__icontains=location_name) + + if min_cap := params.get("min_capacity"): + qs = qs.filter(capacity__gte=min_cap) + + if max_cap := params.get("max_capacity"): + qs = qs.filter(capacity__lte=max_cap) + + if min_datetime := params.get("min_datetime"): + qs = qs.filter(start_datetime__gte=min_datetime) + + if max_datetime := params.get("max_datetime"): + qs = qs.filter(end_datetime__lte=max_datetime) - if cap := params.get("capacity"): - qs = qs.filter(capacity__gte=cap) + # Filter by amenity name (case-insensitive, supports multiple names comma-separated) + if amenity_names := params.get("amenity"): + names = [n.strip() for n in amenity_names.split(",") if n.strip()] + if names: + for n in names: + qs = qs.filter(amenities_id__name__iexact=n) + qs = qs.distinct() if not self.request.user.is_authenticated: qs = qs.filter(is_active=True) From de6560f1bccd9cb64074921207b7c239e2c8fdfe Mon Sep 17 00:00:00 2001 From: ErikaKK <491649804@qq.com> Date: Thu, 11 Dec 2025 16:54:09 +0000 Subject: [PATCH 26/26] edit model and refactor a bit --- server/api/room/README.md | 4 ++- server/api/room/admin.py | 10 ++---- ...enities_options_alter_location_options.py} | 12 +------ .../0006_rename_amenities_amenity.py | 17 +++++++++ server/api/room/models.py | 6 ++-- server/api/room/serializers.py | 31 +++++++++++----- server/api/room/tests.py | 16 ++++----- server/api/room/urls.py | 4 +-- server/api/room/views.py | 35 +++++-------------- 9 files changed, 68 insertions(+), 67 deletions(-) rename server/api/room/migrations/{0005_alter_amenities_options_alter_location_options_and_more.py => 0005_alter_amenities_options_alter_location_options.py} (55%) create mode 100644 server/api/room/migrations/0006_rename_amenities_amenity.py diff --git a/server/api/room/README.md b/server/api/room/README.md index d6e8d2f..581393f 100644 --- a/server/api/room/README.md +++ b/server/api/room/README.md @@ -52,7 +52,7 @@ Returns details for a single room. ```json { "name": "Conference Room", - "location_id": 1, + "location": 1, "capacity": 20, "amenities_id": [1, 2], "start_datetime": "2025-12-11T09:00:00Z", @@ -83,8 +83,10 @@ Returns details for a single room. ## Notes - Unauthenticated users only see rooms where `is_active=true`. +- A room cannot be deleted but you can change its `is_active` - Filtering by multiple amenities returns rooms that have **all** specified amenities. - Validation: `end_datetime` must be after `start_datetime`. +- Validation: `recurrence_rule` must start with FREQ= and contain valid frequency --- diff --git a/server/api/room/admin.py b/server/api/room/admin.py index 7c48a6b..043606a 100644 --- a/server/api/room/admin.py +++ b/server/api/room/admin.py @@ -1,19 +1,15 @@ from django.contrib import admin -from .models import Room, Location, Amenities +from .models import Room, Location, Amenity # Register your models here. @admin.register(Room) class RoomAdmin(admin.ModelAdmin): - list_display = ("id", "name", "location_name", + list_display = ("id", "name", "location", "start_datetime", "end_datetime", "recurrence_rule", "is_active") search_fields = ("name", "location__name", "amenities__name") list_display_links = ("name",) - def location_name(self, obj): - return obj.location_id.name - location_name.short_description = "Location" - @admin.register(Location) class LocationAdmin(admin.ModelAdmin): @@ -22,7 +18,7 @@ class LocationAdmin(admin.ModelAdmin): list_display_links = ("name",) -@admin.register(Amenities) +@admin.register(Amenity) class AmenitiesAdmin(admin.ModelAdmin): list_display = ("id", "name") search_fields = ("name",) diff --git a/server/api/room/migrations/0005_alter_amenities_options_alter_location_options_and_more.py b/server/api/room/migrations/0005_alter_amenities_options_alter_location_options.py similarity index 55% rename from server/api/room/migrations/0005_alter_amenities_options_alter_location_options_and_more.py rename to server/api/room/migrations/0005_alter_amenities_options_alter_location_options.py index 0aeb9f0..8524b43 100644 --- a/server/api/room/migrations/0005_alter_amenities_options_alter_location_options_and_more.py +++ b/server/api/room/migrations/0005_alter_amenities_options_alter_location_options.py @@ -1,4 +1,4 @@ -# Generated by Django 5.2.9 on 2025-12-11 14:24 +# Generated by Django 5.2.9 on 2025-12-11 16:24 from django.db import migrations @@ -18,14 +18,4 @@ class Migration(migrations.Migration): name="location", options={"ordering": ["id"]}, ), - migrations.RenameField( - model_name="room", - old_name="amenities", - new_name="amenities_id", - ), - migrations.RenameField( - model_name="room", - old_name="location", - new_name="location_id", - ), ] diff --git a/server/api/room/migrations/0006_rename_amenities_amenity.py b/server/api/room/migrations/0006_rename_amenities_amenity.py new file mode 100644 index 0000000..188a73e --- /dev/null +++ b/server/api/room/migrations/0006_rename_amenities_amenity.py @@ -0,0 +1,17 @@ +# Generated by Django 5.2.9 on 2025-12-11 16:40 + +from django.db import migrations + + +class Migration(migrations.Migration): + + dependencies = [ + ("room", "0005_alter_amenities_options_alter_location_options"), + ] + + operations = [ + migrations.RenameModel( + old_name="Amenities", + new_name="Amenity", + ), + ] diff --git a/server/api/room/models.py b/server/api/room/models.py index 627393b..759da9a 100644 --- a/server/api/room/models.py +++ b/server/api/room/models.py @@ -13,7 +13,7 @@ def __str__(self): return self.name -class Amenities(models.Model): +class Amenity(models.Model): id = models.AutoField(primary_key=True) name = models.CharField(max_length=32, blank=False) @@ -28,10 +28,10 @@ class Room(models.Model): id = models.AutoField(primary_key=True) name = models.CharField(max_length=32, blank=False) img = models.ImageField(upload_to='room_images/', blank=True, null=True) - location_id = models.ForeignKey( + location = models.ForeignKey( Location, on_delete=models.PROTECT, blank=False) capacity = models.PositiveIntegerField(validators=[MinValueValidator(1)]) - amenities_id = models.ManyToManyField(Amenities, blank=True) + amenities = models.ManyToManyField(Amenity, blank=True) is_active = models.BooleanField(default=True) start_datetime = models.DateTimeField(blank=False) end_datetime = models.DateTimeField(blank=False) diff --git a/server/api/room/serializers.py b/server/api/room/serializers.py index bf6fee5..6070d7e 100644 --- a/server/api/room/serializers.py +++ b/server/api/room/serializers.py @@ -1,10 +1,11 @@ from rest_framework import serializers -from .models import Room, Amenities, Location +from .models import Room, Amenity, Location +import re -class AmenitiesSerializer(serializers.ModelSerializer): +class AmenitySerializer(serializers.ModelSerializer): class Meta: - model = Amenities + model = Amenity fields = ["id", "name"] read_only_fields = ['id'] @@ -17,13 +18,14 @@ class Meta: class RoomSerializer(serializers.ModelSerializer): - location = LocationSerializer(source='location_id', read_only=True) - amenities = AmenitiesSerializer( - many=True, source='amenities_id', read_only=True) + location = LocationSerializer(read_only=True) + amenities = AmenitySerializer(many=True, read_only=True) location_id = serializers.PrimaryKeyRelatedField( - queryset=Location.objects.all(), write_only=True) + queryset=Location.objects.all(), write_only=True, source='location' + ) amenities_id = serializers.PrimaryKeyRelatedField( - queryset=Amenities.objects.all(), many=True, write_only=True) + queryset=Amenity.objects.all(), many=True, write_only=True, source='amenities' + ) class Meta: model = Room @@ -48,8 +50,21 @@ def validate(self, data): self.instance, 'start_datetime', None) end = data.get('end_datetime') or getattr( self.instance, 'end_datetime', None) + recurrence_rule = data.get('recurrence_rule') or getattr( + self.instance, 'recurrence_rule', None) + if start and end and end <= start: raise serializers.ValidationError({ 'end_datetime': 'End datetime must be after start datetime.' }) + + # Validate recurrence_rule (Google Calendar RFC 5545 format) + if recurrence_rule: + # Basic RFC 5545 RRULE validation: must start with FREQ= and contain valid frequency + freq_pattern = r'^FREQ=(DAILY|WEEKLY|MONTHLY|YEARLY)(;.*)?$' + if not re.match(freq_pattern, recurrence_rule): + raise serializers.ValidationError({ + 'recurrence_rule': 'Recurrence rule must start with FREQ= and use a valid frequency (DAILY, WEEKLY, MONTHLY, YEARLY).' + }) + return data diff --git a/server/api/room/tests.py b/server/api/room/tests.py index 5a66c1f..2b8bf8e 100644 --- a/server/api/room/tests.py +++ b/server/api/room/tests.py @@ -1,7 +1,7 @@ from rest_framework.test import APITestCase, APIClient from rest_framework import status from django.contrib.auth import get_user_model -from .models import Room, Location, Amenities +from .models import Room, Location, Amenity from django.utils import timezone User = get_user_model() @@ -19,14 +19,14 @@ def setUp(self): self.loc3 = Location.objects.create(name="Building C") # Amenities - self.amenity1 = Amenities.objects.create(name="Projector") - self.amenity2 = Amenities.objects.create(name="Whiteboard") - self.amenity4 = Amenities.objects.create(name="House") + self.amenity1 = Amenity.objects.create(name="Projector") + self.amenity2 = Amenity.objects.create(name="Whiteboard") + self.amenity4 = Amenity.objects.create(name="House") # Rooms self.room1 = Room.objects.create( name="Conference Room 1", - location_id=self.loc1, + location=self.loc1, capacity=10, start_datetime=timezone.make_aware( timezone.datetime(2025, 10, 1, 9, 0)), @@ -35,11 +35,11 @@ def setUp(self): recurrence_rule="FREQ=DAILY;BYDAY=MO,TU,WE", is_active=True ) - self.room1.amenities_id.set([self.amenity1, self.amenity2]) + self.room1.amenities.set([self.amenity1, self.amenity2]) self.room2 = Room.objects.create( name="Meeting Room A", - location_id=self.loc3, + location=self.loc3, capacity=5, start_datetime=timezone.make_aware( timezone.datetime(2025, 11, 1, 10, 0)), @@ -48,7 +48,7 @@ def setUp(self): recurrence_rule="FREQ=WEEKLY;BYDAY=MO,WE,FR", is_active=False # Inactive room for unauthenticated test ) - self.room2.amenities_id.set([self.amenity4]) + self.room2.amenities.set([self.amenity4]) # -------- LIST & FILTER TESTS -------- def test_list_all_rooms_authenticated(self): diff --git a/server/api/room/urls.py b/server/api/room/urls.py index 527aeba..9872728 100644 --- a/server/api/room/urls.py +++ b/server/api/room/urls.py @@ -1,5 +1,5 @@ from rest_framework.routers import DefaultRouter -from .views import RoomViewSet, LocationViewSet, AmenitiesViewSet +from .views import RoomViewSet, LocationViewSet, AmenityViewSet from django.urls import include, path app_name = 'room' @@ -7,7 +7,7 @@ router = DefaultRouter() router.register(r'rooms', RoomViewSet, basename='rooms') router.register(r'locations', LocationViewSet, basename='locations') -router.register(r'amenities', AmenitiesViewSet, basename='amenities') +router.register(r'amenities', AmenityViewSet, basename='amenities') urlpatterns = [ path('', include(router.urls)), diff --git a/server/api/room/views.py b/server/api/room/views.py index 1f88d11..a88417a 100644 --- a/server/api/room/views.py +++ b/server/api/room/views.py @@ -1,36 +1,26 @@ from rest_framework import viewsets, permissions -from rest_framework.response import Response from rest_framework.exceptions import MethodNotAllowed -from rest_framework.pagination import PageNumberPagination -from .models import Room, Location, Amenities -from .serializers import RoomSerializer, LocationSerializer, AmenitiesSerializer +from .models import Room, Location, Amenity +from .serializers import RoomSerializer, LocationSerializer, AmenitySerializer # Viewset is library that provides CRUD operations for api # Admin have create update delete permissions everyone can read -# get request can filter by name, location_id, capacity_id for get +# get request can filter by name, location, capacity for get # per issue thing: # Update has custom response with id name updated_at # Delete has custom response message -class RoomPagination(PageNumberPagination): - page_size = 10 # Change as needed - - class RoomViewSet(viewsets.ModelViewSet): queryset = Room.objects.all() serializer_class = RoomSerializer - pagination_class = RoomPagination def get_permissions(self): if self.action in ["create", "update", "partial_update", "destroy"]: return [permissions.IsAuthenticated()] return [permissions.AllowAny()] - def get_serializer_class(self): - return RoomSerializer - def get_queryset(self): qs = super().get_queryset() params = self.request.query_params @@ -39,7 +29,7 @@ def get_queryset(self): qs = qs.filter(name__icontains=name) if location_name := params.get("location"): - qs = qs.filter(location_id__name__icontains=location_name) + qs = qs.filter(location__name__icontains=location_name) if min_cap := params.get("min_capacity"): qs = qs.filter(capacity__gte=min_cap) @@ -58,7 +48,7 @@ def get_queryset(self): names = [n.strip() for n in amenity_names.split(",") if n.strip()] if names: for n in names: - qs = qs.filter(amenities_id__name__iexact=n) + qs = qs.filter(amenities__name__iexact=n) qs = qs.distinct() if not self.request.user.is_authenticated: @@ -66,15 +56,6 @@ def get_queryset(self): return qs - def update(self, request, *args, **kwargs): - instance = self.get_object() - serializer = self.get_serializer( - instance, data=request.data, partial=True) - serializer.is_valid(raise_exception=True) - serializer.save() - - return Response(serializer.data) - def destroy(self, request, *args, **kwargs): raise MethodNotAllowed("DELETE") @@ -89,9 +70,9 @@ def get_permissions(self): return [permissions.AllowAny()] -class AmenitiesViewSet(viewsets.ModelViewSet): - queryset = Amenities.objects.all() - serializer_class = AmenitiesSerializer +class AmenityViewSet(viewsets.ModelViewSet): + queryset = Amenity.objects.all() + serializer_class = AmenitySerializer def get_permissions(self): if self.action in ["create", "update", "partial_update", "destroy"]: