diff --git a/backend/pennmobile/settings/base.py b/backend/pennmobile/settings/base.py index cf285a89..c1bcd22a 100644 --- a/backend/pennmobile/settings/base.py +++ b/backend/pennmobile/settings/base.py @@ -46,6 +46,7 @@ "accounts.apps.AccountsConfig", "identity.apps.IdentityConfig", "analytics.apps.AnalyticsConfig", + "wrapped.apps.WrappedConfig", "django_filters", "debug_toolbar", "gsr_booking", diff --git a/backend/pennmobile/urls.py b/backend/pennmobile/urls.py index 5e94960e..f517a8af 100644 --- a/backend/pennmobile/urls.py +++ b/backend/pennmobile/urls.py @@ -27,6 +27,7 @@ path("dining/", include("dining.urls")), path("penndata/", include("penndata.urls")), path("sublet/", include("sublet.urls")), + path("wrapped/", include("wrapped.urls")), ] urlpatterns = [ diff --git a/backend/wrapped/__init__.py b/backend/wrapped/__init__.py new file mode 100644 index 00000000..e69de29b diff --git a/backend/wrapped/admin.py b/backend/wrapped/admin.py new file mode 100644 index 00000000..24a043ba --- /dev/null +++ b/backend/wrapped/admin.py @@ -0,0 +1,45 @@ +from django.contrib import admin + +from wrapped.models import ( + GlobalStat, + GlobalStatKey, + GlobalStatPageField, + IndividualStat, + IndividualStatKey, + IndividualStatPageField, + Page, + Semester, +) + + +class WrappedIndividualAdmin(admin.ModelAdmin): + search_fields = ["user__username__icontains", "key__key__icontains", "semester__icontains"] + list_display = ["user", "key", "value", "semester"] + + +class WrappedGlobalAdmin(admin.ModelAdmin): + + list_display = ["key", "value", "semester"] + search_fields = ["key__icontains"] + + +class IndividualStatPageFieldAdmin(admin.TabularInline): + model = IndividualStatPageField + extra = 1 + + +class GlobalStatPageFieldAdmin(admin.TabularInline): + model = GlobalStatPageField + extra = 1 + + +class PageAdmin(admin.ModelAdmin): + inlines = [IndividualStatPageFieldAdmin, GlobalStatPageFieldAdmin] + + +admin.site.register(IndividualStat, WrappedIndividualAdmin) +admin.site.register(GlobalStat, WrappedGlobalAdmin) +admin.site.register(IndividualStatKey) +admin.site.register(GlobalStatKey) +admin.site.register(Page, PageAdmin) +admin.site.register(Semester) diff --git a/backend/wrapped/apps.py b/backend/wrapped/apps.py new file mode 100644 index 00000000..7b3d895a --- /dev/null +++ b/backend/wrapped/apps.py @@ -0,0 +1,6 @@ +from django.apps import AppConfig + + +class WrappedConfig(AppConfig): + default_auto_field = "django.db.models.BigAutoField" + name = "wrapped" diff --git a/backend/wrapped/migrations/0001_initial.py b/backend/wrapped/migrations/0001_initial.py new file mode 100644 index 00000000..a69873cb --- /dev/null +++ b/backend/wrapped/migrations/0001_initial.py @@ -0,0 +1,172 @@ +# Generated by Django 5.0.2 on 2024-11-10 16:21 + +import datetime + +import django.db.models.deletion +from django.conf import settings +from django.db import migrations, models + + +class Migration(migrations.Migration): + + initial = True + + dependencies = [ + migrations.swappable_dependency(settings.AUTH_USER_MODEL), + ] + + operations = [ + migrations.CreateModel( + name="GlobalStatKey", + fields=[ + ("key", models.CharField(max_length=50, primary_key=True, serialize=False)), + ], + ), + migrations.CreateModel( + name="IndividualStatKey", + fields=[ + ("key", models.CharField(max_length=50, primary_key=True, serialize=False)), + ], + ), + migrations.CreateModel( + name="GlobalStatPageField", + fields=[ + ( + "id", + models.BigAutoField( + auto_created=True, primary_key=True, serialize=False, verbose_name="ID" + ), + ), + ("text_field_name", models.CharField(max_length=50)), + ( + "global_stat_key", + models.ForeignKey( + default=None, + on_delete=django.db.models.deletion.CASCADE, + to="wrapped.globalstatkey", + ), + ), + ], + ), + migrations.CreateModel( + name="IndividualStatPageField", + fields=[ + ( + "id", + models.BigAutoField( + auto_created=True, primary_key=True, serialize=False, verbose_name="ID" + ), + ), + ("text_field_name", models.CharField(max_length=50)), + ( + "individual_stat_key", + models.ForeignKey( + default=None, + on_delete=django.db.models.deletion.CASCADE, + to="wrapped.individualstatkey", + ), + ), + ], + ), + migrations.CreateModel( + name="Page", + fields=[ + ("name", models.CharField(max_length=50, primary_key=True, serialize=False)), + ("template_path", models.CharField(max_length=50)), + ("duration", models.DurationField(blank=True, default=datetime.timedelta(0))), + ( + "global_stats", + models.ManyToManyField( + blank=True, + through="wrapped.GlobalStatPageField", + to="wrapped.globalstatkey", + ), + ), + ( + "individual_stats", + models.ManyToManyField( + blank=True, + through="wrapped.IndividualStatPageField", + to="wrapped.individualstatkey", + ), + ), + ], + ), + migrations.AddField( + model_name="individualstatpagefield", + name="Page", + field=models.ForeignKey(on_delete=django.db.models.deletion.CASCADE, to="wrapped.page"), + ), + migrations.AddField( + model_name="globalstatpagefield", + name="Page", + field=models.ForeignKey(on_delete=django.db.models.deletion.CASCADE, to="wrapped.page"), + ), + migrations.CreateModel( + name="Semester", + fields=[ + ("semester", models.CharField(max_length=5, primary_key=True, serialize=False)), + ("pages", models.ManyToManyField(blank=True, to="wrapped.page")), + ], + ), + migrations.CreateModel( + name="IndividualStat", + fields=[ + ( + "id", + models.BigAutoField( + auto_created=True, primary_key=True, serialize=False, verbose_name="ID" + ), + ), + ("value", models.CharField(max_length=50)), + ( + "user", + models.ForeignKey( + on_delete=django.db.models.deletion.CASCADE, to=settings.AUTH_USER_MODEL + ), + ), + ( + "key", + models.ForeignKey( + on_delete=django.db.models.deletion.CASCADE, to="wrapped.individualstatkey" + ), + ), + ( + "semester", + models.ForeignKey( + on_delete=django.db.models.deletion.CASCADE, to="wrapped.semester" + ), + ), + ], + options={ + "unique_together": {("key", "semester", "user")}, + }, + ), + migrations.CreateModel( + name="GlobalStat", + fields=[ + ( + "id", + models.BigAutoField( + auto_created=True, primary_key=True, serialize=False, verbose_name="ID" + ), + ), + ("value", models.CharField(max_length=50)), + ( + "key", + models.ForeignKey( + on_delete=django.db.models.deletion.CASCADE, to="wrapped.globalstatkey" + ), + ), + ( + "semester", + models.ForeignKey( + on_delete=django.db.models.deletion.CASCADE, to="wrapped.semester" + ), + ), + ], + options={ + "unique_together": {("key", "semester")}, + }, + ), + ] diff --git a/backend/wrapped/migrations/0002_rename_page_globalstatpagefield_page_and_more.py b/backend/wrapped/migrations/0002_rename_page_globalstatpagefield_page_and_more.py new file mode 100644 index 00000000..dcfe7098 --- /dev/null +++ b/backend/wrapped/migrations/0002_rename_page_globalstatpagefield_page_and_more.py @@ -0,0 +1,30 @@ +# Generated by Django 5.0.2 on 2024-11-10 17:40 + +import datetime + +from django.db import migrations, models + + +class Migration(migrations.Migration): + + dependencies = [ + ("wrapped", "0001_initial"), + ] + + operations = [ + migrations.RenameField( + model_name="globalstatpagefield", + old_name="Page", + new_name="page", + ), + migrations.RenameField( + model_name="individualstatpagefield", + old_name="Page", + new_name="page", + ), + migrations.AlterField( + model_name="page", + name="duration", + field=models.DurationField(blank=True, default=datetime.timedelta(0), null=True), + ), + ] diff --git a/backend/wrapped/migrations/__init__.py b/backend/wrapped/migrations/__init__.py new file mode 100644 index 00000000..e69de29b diff --git a/backend/wrapped/models.py b/backend/wrapped/models.py new file mode 100644 index 00000000..04445427 --- /dev/null +++ b/backend/wrapped/models.py @@ -0,0 +1,107 @@ +from datetime import timedelta + +from django.contrib.auth import get_user_model +from django.db import models + + +User = get_user_model() + + +class StatKey(models.Model): + key = models.CharField(max_length=50, primary_key=True, null=False, blank=False) + + def __str__(self) -> str: + return self.key + + class Meta: + abstract = True + + +class IndividualStatKey(StatKey): + pass + + +class GlobalStatKey(StatKey): + pass + + +class Semester(models.Model): + semester = models.CharField(max_length=5, primary_key=True, null=False, blank=False) + pages = models.ManyToManyField("Page", blank=True) + + +class GlobalStat(models.Model): + + key = models.ForeignKey(GlobalStatKey, on_delete=models.CASCADE) + value = models.CharField(max_length=50, null=False, blank=False) + semester = models.ForeignKey(Semester, on_delete=models.CASCADE) + + class Meta: + unique_together = ("key", "semester") + + def __str__(self): + return f"Global -- {self.key}-{str(self.semester)} : {self.value}" + + +class IndividualStat(models.Model): + user = models.ForeignKey(User, on_delete=models.CASCADE) + key = models.ForeignKey(IndividualStatKey, on_delete=models.CASCADE) + + value = models.CharField(max_length=50, null=False, blank=False) + semester = models.ForeignKey(Semester, on_delete=models.CASCADE) + + class Meta: + unique_together = ("key", "semester", "user") + + def __str__(self) -> str: + return f"User: {self.user} -- {self.key}-{str(self.semester)} : {self.value}" + + +class Page(models.Model): + + name = models.CharField(max_length=50, primary_key=True, null=False, blank=False) + template_path = models.CharField(max_length=50, null=False, blank=False) + individual_stats = models.ManyToManyField( + IndividualStatKey, through="IndividualStatPageField", blank=True + ) + global_stats = models.ManyToManyField(GlobalStatKey, through="GlobalStatPageField", blank=True) + duration = models.DurationField(blank=True, null=True, default=timedelta(minutes=0)) + + def __str__(self): + return f"{self.name}" + + +class IndividualStatPageField(models.Model): + individual_stat_key = models.ForeignKey( + IndividualStatKey, null=False, blank=False, default=None, on_delete=models.CASCADE + ) + page = models.ForeignKey(Page, null=False, blank=False, on_delete=models.CASCADE) + text_field_name = models.CharField(max_length=50, null=False, blank=False) + + def get_value(self, user, semester): + return ( + IndividualStat.objects.filter( + user=user, key=self.individual_stat_key, semester=semester + ) + .first() + .value + ) + + def __str__(self): + return f"{self.page} -> {self.text_field_name} : {self.individual_stat_key}" + + +class GlobalStatPageField(models.Model): + global_stat_key = models.ForeignKey( + GlobalStatKey, null=False, blank=False, default=None, on_delete=models.CASCADE + ) + page = models.ForeignKey(Page, null=False, blank=False, on_delete=models.CASCADE) + text_field_name = models.CharField(max_length=50, null=False, blank=False) + + def get_value(self, _user, semester): + return ( + GlobalStat.objects.filter(key=self.global_stat_key.key, semester=semester).first().value + ) + + def __str__(self): + return f"{self.page} -> {self.text_field_name} : {self.global_stat_key}" diff --git a/backend/wrapped/serializers.py b/backend/wrapped/serializers.py new file mode 100644 index 00000000..23856e8d --- /dev/null +++ b/backend/wrapped/serializers.py @@ -0,0 +1,69 @@ +from rest_framework import serializers + +from wrapped.models import GlobalStatPageField, IndividualStatPageField, Page, Semester + + +class PageFieldSerializer(serializers.ModelSerializer): + stat_value = serializers.SerializerMethodField() + + class Meta: + abstract = True + fields = ["text_field_name", "stat_value"] + + def get_stat_value(self, obj): + user = self.context.get("user") + semester = self.context.get("semester") + return obj.get_value(user, semester) + + +class IndividualStatPageFieldSerializer(PageFieldSerializer): + class Meta(PageFieldSerializer.Meta): + model = IndividualStatPageField + + +class GlobalStatPageFieldSerializer(PageFieldSerializer): + class Meta(PageFieldSerializer.Meta): + model = GlobalStatPageField + + +class PageSerializer(serializers.ModelSerializer): + + combined_stats = serializers.SerializerMethodField() + + class Meta: + model = Page + fields = ["name", "template_path", "combined_stats", "duration"] + + def get_combined_stats(self, obj): + if not (semester := self.context.get("semester", obj)): + return {} + user = self.context.get("user") + individual_stat_fields = IndividualStatPageFieldSerializer( + obj.individualstatpagefield_set.all(), + context={"semester": semester, "user": user}, + many=True, + ).data + + global_stat_fields = GlobalStatPageFieldSerializer( + obj.globalstatpagefield_set.all(), + context={"semester": semester, "user": user}, + many=True, + ).data + + field_list = individual_stat_fields + global_stat_fields + + return {field["text_field_name"]: field["stat_value"] for field in field_list} + + +class SemesterSerializer(serializers.ModelSerializer): + pages = serializers.SerializerMethodField() + + class Meta: + model = Semester + fields = ["semester", "pages"] + + def get_pages(self, obj): + user = self.context.get("user") + return PageSerializer( + obj.pages.all(), many=True, context={"semester": obj, "user": user} + ).data diff --git a/backend/wrapped/tests.py b/backend/wrapped/tests.py new file mode 100644 index 00000000..e69de29b diff --git a/backend/wrapped/urls.py b/backend/wrapped/urls.py new file mode 100644 index 00000000..4f1e5850 --- /dev/null +++ b/backend/wrapped/urls.py @@ -0,0 +1,8 @@ +from django.urls import path + +from wrapped.views import SemesterView + + +urlpatterns = [ + path("semester//", SemesterView.as_view(), name="semester-detail"), +] diff --git a/backend/wrapped/views.py b/backend/wrapped/views.py new file mode 100644 index 00000000..defb0769 --- /dev/null +++ b/backend/wrapped/views.py @@ -0,0 +1,15 @@ +from rest_framework.permissions import IsAuthenticated +from rest_framework.response import Response +from rest_framework.views import APIView + +from wrapped.models import Semester +from wrapped.serializers import SemesterSerializer + + +class SemesterView(APIView): + permission_classes = [IsAuthenticated] + + def get(self, request, semester_id): + semester = Semester.objects.get(semester=semester_id) + serializer = SemesterSerializer(semester, context={"user": request.user}) + return Response(serializer.data)