diff --git a/backend/tests/wrapped/__init__.py b/backend/tests/wrapped/__init__.py new file mode 100644 index 00000000..e69de29b diff --git a/backend/tests/wrapped/test_models.py b/backend/tests/wrapped/test_models.py new file mode 100644 index 00000000..33360d2a --- /dev/null +++ b/backend/tests/wrapped/test_models.py @@ -0,0 +1,123 @@ +from datetime import timedelta + +from django.contrib.auth import get_user_model +from django.db import IntegrityError +from django.test import TestCase + +from wrapped.models import ( + GlobalStat, + GlobalStatKey, + GlobalStatPageField, + IndividualStat, + IndividualStatKey, + IndividualStatPageField, + Page, + Semester, +) + + +User = get_user_model() + + +# Will add more failing tests + + +class WrappedModelsTestCase(TestCase): + def setUp(self): + self.user = User.objects.create_user(username="test", password="test") + self.semester = Semester.objects.create(semester="2025fa") + self.semester2 = Semester.objects.create(semester="2025sp") + + self.ind_key = IndividualStatKey.objects.create(key="gsr_hours") + self.glob_key = GlobalStatKey.objects.create(key="total_gsr_hours") + + self.ind_stat = IndividualStat.objects.create( + user=self.user, + key=self.ind_key, + value="5", + semester=self.semester, + ) + self.glob_stat = GlobalStat.objects.create( + key=self.glob_key, + value="1000", + semester=self.semester, + ) + + self.page = Page.objects.create( + name="GSR_Page", + id=1, + template_path="wrapped/gsr_page.html", + duration=timedelta(minutes=1), + ) + self.page2 = Page.objects.create( + name="GSR_Page2", + id=2, + template_path="wrapped/gsr_page2.html", + duration=timedelta(minutes=1), + ) + self.semester.pages.add(self.page) + self.semester.pages.add(self.page2) + + # Through models for page fields + self.ind_field = IndividualStatPageField.objects.create( + individual_stat_key=self.ind_key, + page=self.page, + text_field_name="top", + ) + self.glob_field = GlobalStatPageField.objects.create( + global_stat_key=self.glob_key, + page=self.page, + text_field_name="bottom", + ) + + def test_str_methods(self): + self.assertEqual("GSR_Page", str(self.page)) + self.assertEqual("User: test -- gsr_hours-2025fa : 5", str(self.ind_stat)) + self.assertEqual("Global -- total_gsr_hours-2025fa : 1000", str(self.glob_stat)) + self.assertEqual("2025fa", str(self.semester)) + self.assertEqual("GSR_Page -> top : gsr_hours", str(self.ind_field)) + self.assertEqual("GSR_Page -> bottom : total_gsr_hours", str(self.glob_field)) + + def test_semester_pages(self): + self.assertEqual([self.page, self.page2], list(self.semester.pages.all())) + + def test_unique_together_individualstat(self): + with self.assertRaises(IntegrityError): + IndividualStat.objects.create( + user=self.user, + key=self.ind_key, + value="6", + semester=self.semester, + ) + + def test_unique_together_globalstat(self): + with self.assertRaises(IntegrityError): + GlobalStat.objects.create( + key=self.glob_key, + value="200", + semester=self.semester, + ) + + def test_page_fields(self): + self.assertEqual([self.ind_key], list(self.page.individual_stats.all())) + self.assertEqual([self.glob_key], list(self.page.global_stats.all())) + + def test_stat_page_fields(self): + self.assertEqual(self.ind_key, self.ind_field.individual_stat_key) + self.assertEqual(self.glob_key, self.glob_field.global_stat_key) + self.assertEqual(self.page, self.ind_field.page) + self.assertEqual(self.page, self.glob_field.page) + self.assertEqual("top", self.ind_field.text_field_name) + self.assertEqual("bottom", self.glob_field.text_field_name) + + def test_stat_page_get_value(self): + self.assertEqual("5", self.ind_field.get_value(self.user, self.semester)) + self.assertEqual("1000", self.glob_field.get_value(self.user, self.semester)) + + def updating_semester_non_duplicate(self): + self.semester.semester = "2025T" + self.semester.save() + self.assertEqual(Semester.objects.get(semester="2025T"), self.semester) + self.assertEqual(Semester.objects.get(semester="2025fa"), None) + self.assertEqual(len(Semester.objects.all()), 2) + print(Semester.objects.all()) diff --git a/backend/tests/wrapped/test_routes.py b/backend/tests/wrapped/test_routes.py new file mode 100644 index 00000000..c9788f3c --- /dev/null +++ b/backend/tests/wrapped/test_routes.py @@ -0,0 +1,160 @@ +from datetime import timedelta + +from django.contrib.auth import get_user_model +from django.test import TestCase +from rest_framework.test import APIClient + +from wrapped.models import ( + GlobalStat, + GlobalStatKey, + GlobalStatPageField, + IndividualStat, + IndividualStatKey, + IndividualStatPageField, + Page, + Semester, +) + + +User = get_user_model() + + +# Will add more failing tests + + +class WrappedRoutesTestCase(TestCase): + def setUp(self): + self.client = APIClient() + self.user = User.objects.create_user(username="test", password="test") + self.client.force_authenticate(user=self.user) + + self.semester = Semester.objects.create(semester="2025fa", current=True) + + self.ind_key = IndividualStatKey.objects.create(key="gsr_hours") + self.glob_key = GlobalStatKey.objects.create(key="total_gsr_hours") + self.glob_key2 = GlobalStatKey.objects.create(key="total_gym_hours") + self.ind_key2 = IndividualStatKey.objects.create(key="gym_hours") + + self.ind_stat = IndividualStat.objects.create( + user=self.user, + key=self.ind_key, + value="5", + semester=self.semester, + ) + + self.ind_stat2 = IndividualStat.objects.create( + user=self.user, + key=self.ind_key2, + value="10", + semester=self.semester, + ) + + self.glob_stat = GlobalStat.objects.create( + key=self.glob_key, + value="1000", + semester=self.semester, + ) + self.glob_stat2 = GlobalStat.objects.create( + key=self.glob_key2, + value="2000", + semester=self.semester, + ) + + self.page = Page.objects.create( + name="GSR_Page", + id=1, + template_path="wrapped/gsr_page.html", + duration=timedelta(minutes=1), + ) + self.page2 = Page.objects.create( + name="GSR_Page2", + id=2, + template_path="wrapped/gsr_page2.html", + duration=timedelta(minutes=1), + ) + self.semester.pages.add(self.page) + self.semester.pages.add(self.page2) + + # Through models for page fields + self.ind_field = IndividualStatPageField.objects.create( + individual_stat_key=self.ind_key, + page=self.page, + text_field_name="top", + ) + self.ind_field2 = IndividualStatPageField.objects.create( + individual_stat_key=self.ind_key2, + page=self.page, + text_field_name="middle", + ) + self.glob_field = GlobalStatPageField.objects.create( + global_stat_key=self.glob_key, + page=self.page, + text_field_name="bottom", + ) + self.glob_field2 = GlobalStatPageField.objects.create( + global_stat_key=self.glob_key2, + page=self.page, + text_field_name="middle_left", + ) + + def test_get_current_semester(self): + response = self.client.get("/wrapped/semester/current/") + self.assertEqual(response.status_code, 200) + self.assertEqual( + response.json(), + { + "semester": "2025fa", + "pages": [ + { + "id": 1, + "name": "GSR_Page", + "template_path": "wrapped/gsr_page.html", + "combined_stats": { + "top": "5", + "middle": "10", + "bottom": "1000", + "middle_left": "2000", + }, + "duration": "00:01:00", + }, + { + "id": 2, + "name": "GSR_Page2", + "template_path": "wrapped/gsr_page2.html", + "combined_stats": {}, + "duration": "00:01:00", + }, + ], + }, + ) + + def test_get_semester(self): + response = self.client.get("/wrapped/semester/2025fa/") + self.assertEqual(response.status_code, 200) + self.assertEqual( + response.json(), + { + "semester": "2025fa", + "pages": [ + { + "id": 1, + "name": "GSR_Page", + "template_path": "wrapped/gsr_page.html", + "combined_stats": { + "top": "5", + "middle": "10", + "bottom": "1000", + "middle_left": "2000", + }, + "duration": "00:01:00", + }, + { + "id": 2, + "name": "GSR_Page2", + "template_path": "wrapped/gsr_page2.html", + "combined_stats": {}, + "duration": "00:01:00", + }, + ], + }, + ) diff --git a/backend/tests/wrapped/test_serializers.py b/backend/tests/wrapped/test_serializers.py new file mode 100644 index 00000000..562aa53f --- /dev/null +++ b/backend/tests/wrapped/test_serializers.py @@ -0,0 +1,133 @@ +from datetime import timedelta + +from django.contrib.auth import get_user_model +from django.test import TestCase + +from wrapped.models import ( + GlobalStat, + GlobalStatKey, + GlobalStatPageField, + IndividualStat, + IndividualStatKey, + IndividualStatPageField, + Page, + Semester, +) +from wrapped.serializers import PageSerializer, SemesterSerializer + + +User = get_user_model() + + +# Will add more failing tests + + +class WrappedSerializersTestCase(TestCase): + def setUp(self): + + self.user = User.objects.create_user(username="test", password="test") + self.semester = Semester.objects.create(semester="2025fa") + + self.ind_key = IndividualStatKey.objects.create(key="gsr_hours") + self.glob_key = GlobalStatKey.objects.create(key="total_gsr_hours") + self.glob_key2 = GlobalStatKey.objects.create(key="total_gym_hours") + self.ind_key2 = IndividualStatKey.objects.create(key="gym_hours") + + self.ind_stat = IndividualStat.objects.create( + user=self.user, + key=self.ind_key, + value="5", + semester=self.semester, + ) + + self.ind_stat2 = IndividualStat.objects.create( + user=self.user, + key=self.ind_key2, + value="10", + semester=self.semester, + ) + + self.glob_stat = GlobalStat.objects.create( + key=self.glob_key, + value="1000", + semester=self.semester, + ) + self.glob_stat2 = GlobalStat.objects.create( + key=self.glob_key2, + value="2000", + semester=self.semester, + ) + + self.page = Page.objects.create( + name="GSR_Page", + id=1, + template_path="wrapped/gsr_page.html", + duration=timedelta(minutes=1), + ) + self.page2 = Page.objects.create( + name="GSR_Page2", + id=2, + template_path="wrapped/gsr_page2.html", + duration=timedelta(minutes=1), + ) + self.semester.pages.add(self.page) + self.semester.pages.add(self.page2) + + # Through models for page fields + self.ind_field = IndividualStatPageField.objects.create( + individual_stat_key=self.ind_key, + page=self.page, + text_field_name="top", + ) + self.ind_field2 = IndividualStatPageField.objects.create( + individual_stat_key=self.ind_key2, + page=self.page, + text_field_name="middle", + ) + self.glob_field = GlobalStatPageField.objects.create( + global_stat_key=self.glob_key, + page=self.page, + text_field_name="bottom", + ) + self.glob_field2 = GlobalStatPageField.objects.create( + global_stat_key=self.glob_key2, + page=self.page, + text_field_name="middle_left", + ) + + def test_page_serializer(self): + serializer = PageSerializer( + self.page, context={"semester": self.semester, "user": self.user} + ) + data = serializer.data + self.assertEqual(data["name"], "GSR_Page") + self.assertEqual(data["id"], 1) + self.assertEqual(data["template_path"], "wrapped/gsr_page.html") + self.assertEqual(data["duration"], "00:01:00") + self.assertEqual( + data["combined_stats"], + { + "top": "5", + "bottom": "1000", + "middle": "10", + "middle_left": "2000", + }, + ) + + def test_semester_serializer(self): + serializer = SemesterSerializer(self.semester, context={"user": self.user}) + data = serializer.data + self.assertEqual(data["semester"], "2025fa") + self.assertEqual( + data["pages"], + [ + PageSerializer( + self.page, + context={"semester": self.semester, "user": self.user}, + ).data, + PageSerializer( + self.page2, + context={"semester": self.semester, "user": self.user}, + ).data, + ], + ) diff --git a/backend/wrapped/migrations/0006_semester_current_semester_semester_unique_and_more.py b/backend/wrapped/migrations/0006_semester_current_semester_semester_unique_and_more.py new file mode 100644 index 00000000..22cd4d8c --- /dev/null +++ b/backend/wrapped/migrations/0006_semester_current_semester_semester_unique_and_more.py @@ -0,0 +1,30 @@ +# Generated by Django 5.0.2 on 2025-11-16 19:44 + +from django.db import migrations, models + + +class Migration(migrations.Migration): + + dependencies = [ + ("wrapped", "0005_alter_semester_semester"), + ] + + operations = [ + migrations.AddField( + model_name="semester", + name="current", + field=models.BooleanField(default=False), + ), + migrations.AddConstraint( + model_name="semester", + constraint=models.UniqueConstraint(fields=("semester",), name="semester_unique"), + ), + migrations.AddConstraint( + model_name="semester", + constraint=models.UniqueConstraint( + condition=models.Q(("current", True)), + fields=("current",), + name="single_current_semester", + ), + ), + ] diff --git a/backend/wrapped/models.py b/backend/wrapped/models.py index a4d71e0a..2364dcac 100644 --- a/backend/wrapped/models.py +++ b/backend/wrapped/models.py @@ -2,6 +2,7 @@ from django.contrib.auth import get_user_model from django.db import models +from django.db.models import Q User = get_user_model() @@ -28,6 +29,20 @@ class GlobalStatKey(StatKey): class Semester(models.Model): semester = models.CharField(max_length=16, primary_key=True, null=False, blank=False) pages = models.ManyToManyField("Page", blank=True) + current = models.BooleanField(default=False) + + class Meta: + constraints = [ + models.UniqueConstraint(fields=["semester"], name="semester_unique"), + models.UniqueConstraint( + fields=["current"], + condition=Q(current=True), + name="single_current_semester", + ), + ] + + def __str__(self): + return self.semester class GlobalStat(models.Model): diff --git a/backend/wrapped/urls.py b/backend/wrapped/urls.py index 4f1e5850..500c41c6 100644 --- a/backend/wrapped/urls.py +++ b/backend/wrapped/urls.py @@ -1,8 +1,9 @@ from django.urls import path -from wrapped.views import SemesterView +from wrapped.views import SemesterCurrentView, SemesterView urlpatterns = [ + path("semester/current/", SemesterCurrentView.as_view(), name="semester-current-detail"), path("semester//", SemesterView.as_view(), name="semester-detail"), ] diff --git a/backend/wrapped/views.py b/backend/wrapped/views.py index defb0769..8537350a 100644 --- a/backend/wrapped/views.py +++ b/backend/wrapped/views.py @@ -13,3 +13,12 @@ def get(self, request, semester_id): semester = Semester.objects.get(semester=semester_id) serializer = SemesterSerializer(semester, context={"user": request.user}) return Response(serializer.data) + + +class SemesterCurrentView(APIView): + permission_classes = [IsAuthenticated] + + def get(self, request): + current_semester = Semester.objects.filter(current=True).first() + serializer = SemesterSerializer(current_semester, context={"user": request.user}) + return Response(serializer.data)