diff --git a/backend/pennmobile/urls.py b/backend/pennmobile/urls.py index 1f45902e..f517a8af 100644 --- a/backend/pennmobile/urls.py +++ b/backend/pennmobile/urls.py @@ -27,7 +27,7 @@ path("dining/", include("dining.urls")), path("penndata/", include("penndata.urls")), path("sublet/", include("sublet.urls")), - path("wrapped/", include("wrapped.urls")) + path("wrapped/", include("wrapped.urls")), ] urlpatterns = [ diff --git a/backend/wrapped/admin.py b/backend/wrapped/admin.py index 8647d64c..24a043ba 100644 --- a/backend/wrapped/admin.py +++ b/backend/wrapped/admin.py @@ -1,7 +1,15 @@ from django.contrib import admin -from django.core.exceptions import ValidationError -from wrapped.models import GlobalStatKey, GlobalStat,IndividualStat, IndividualStatKey, Page, IndividualStatPageField, GlobalStatPageField, Semester +from wrapped.models import ( + GlobalStat, + GlobalStatKey, + GlobalStatPageField, + IndividualStat, + IndividualStatKey, + IndividualStatPageField, + Page, + Semester, +) class WrappedIndividualAdmin(admin.ModelAdmin): @@ -10,15 +18,17 @@ class WrappedIndividualAdmin(admin.ModelAdmin): class WrappedGlobalAdmin(admin.ModelAdmin): - + list_display = ["key", "value", "semester"] search_fields = ["key__icontains"] -class IndividualStatPageFieldAdmin(admin.TabularInline): + +class IndividualStatPageFieldAdmin(admin.TabularInline): model = IndividualStatPageField extra = 1 -class GlobalStatPageFieldAdmin(admin.TabularInline): + +class GlobalStatPageFieldAdmin(admin.TabularInline): model = GlobalStatPageField extra = 1 @@ -27,18 +37,9 @@ class PageAdmin(admin.ModelAdmin): inlines = [IndividualStatPageFieldAdmin, GlobalStatPageFieldAdmin] - - -# admin.site.register(WrappedIndividualAdmin, WrappedGlobalAdmin) 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) -# admin.site.register(Page) - - - - diff --git a/backend/wrapped/apps.py b/backend/wrapped/apps.py index e7886443..7b3d895a 100644 --- a/backend/wrapped/apps.py +++ b/backend/wrapped/apps.py @@ -2,7 +2,5 @@ class WrappedConfig(AppConfig): - default_auto_field = 'django.db.models.BigAutoField' - name = 'wrapped' - def ready(self): - import wrapped.signals # Import the file where your signal is defined \ No newline at end of file + 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 index 52c7bc68..a69873cb 100644 --- a/backend/wrapped/migrations/0001_initial.py +++ b/backend/wrapped/migrations/0001_initial.py @@ -1,6 +1,7 @@ # 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 @@ -16,83 +17,156 @@ class Migration(migrations.Migration): operations = [ migrations.CreateModel( - name='GlobalStatKey', + name="GlobalStatKey", fields=[ - ('key', models.CharField(max_length=50, primary_key=True, serialize=False)), + ("key", models.CharField(max_length=50, primary_key=True, serialize=False)), ], ), migrations.CreateModel( - name='IndividualStatKey', + name="IndividualStatKey", fields=[ - ('key', models.CharField(max_length=50, primary_key=True, serialize=False)), + ("key", models.CharField(max_length=50, primary_key=True, serialize=False)), ], ), migrations.CreateModel( - name='GlobalStatPageField', + 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')), + ( + "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', + 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')), + ( + "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', + 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')), + ("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'), + 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'), + model_name="globalstatpagefield", + name="Page", + field=models.ForeignKey(on_delete=django.db.models.deletion.CASCADE, to="wrapped.page"), ), migrations.CreateModel( - name='Semester', + name="Semester", fields=[ - ('semester', models.CharField(max_length=5, primary_key=True, serialize=False)), - ('pages', models.ManyToManyField(blank=True, to='wrapped.page')), + ("semester", models.CharField(max_length=5, primary_key=True, serialize=False)), + ("pages", models.ManyToManyField(blank=True, to="wrapped.page")), ], ), migrations.CreateModel( - name='IndividualStat', + 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')), + ( + "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')}, + "unique_together": {("key", "semester", "user")}, }, ), migrations.CreateModel( - name='GlobalStat', + 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')), + ( + "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')}, + "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/models.py b/backend/wrapped/models.py index 5d015d4c..04445427 100644 --- a/backend/wrapped/models.py +++ b/backend/wrapped/models.py @@ -1,38 +1,39 @@ -from django.db import models -from django.contrib.auth import get_user_model -from django.core.exceptions import ValidationError -from django.db.models import Q from datetime import timedelta +from django.contrib.auth import get_user_model +from django.db import models + + User = get_user_model() -# Add a new model for keys -class IndividualStatKey(models.Model): - key = models.CharField(max_length=50, primary_key=True,null=False, blank=False) +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 GlobalStatKey(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) + 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) - + value = models.CharField(max_length=50, null=False, blank=False) semester = models.ForeignKey(Semester, on_delete=models.CASCADE) class Meta: @@ -42,48 +43,65 @@ 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) + value = models.CharField(max_length=50, null=False, blank=False) semester = models.ForeignKey(Semester, on_delete=models.CASCADE) - class Meta: + 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) + 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) + individual_stats = models.ManyToManyField( + IndividualStatKey, through="IndividualStatPageField", blank=True + ) global_stats = models.ManyToManyField(GlobalStatKey, through="GlobalStatPageField", blank=True) - duration = models.DurationField(blank=True, default=timedelta(minutes=0)) + 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) + 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}" + 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) + 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 __str__(self): - return f"{self.Page} -> {self.text_field_name} : {self.global_stat_key}" + 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 index f7a34c4a..23856e8d 100644 --- a/backend/wrapped/serializers.py +++ b/backend/wrapped/serializers.py @@ -1,66 +1,29 @@ from rest_framework import serializers -from .models import Page, IndividualStat, GlobalStat, IndividualStatPageField, GlobalStatPageField, Semester, User - -class IndividualStatSerializer(serializers.ModelSerializer): - key = serializers.SlugRelatedField( - slug_field='key', - read_only=True - ) - class Meta: - model = IndividualStat - fields = ['key', 'value', 'semester'] +from wrapped.models import GlobalStatPageField, IndividualStatPageField, Page, Semester -class GlobalStatSerializer(serializers.ModelSerializer): - key = serializers.SlugRelatedField( - slug_field='key', - read_only=True - ) - class Meta: - model = GlobalStat - fields = ['key', 'value', 'semester'] - -class IndividualStatPageFieldSerializer(serializers.ModelSerializer): - individual_stat_value = serializers.SerializerMethodField() +class PageFieldSerializer(serializers.ModelSerializer): + stat_value = serializers.SerializerMethodField() class Meta: - model = IndividualStatPageField - fields = ['text_field_name', 'individual_stat_value'] + abstract = True + fields = ["text_field_name", "stat_value"] - def get_individual_stat_value(self, obj): - user = self.context.get('user') - semester = self.context.get('semester') + def get_stat_value(self, obj): + user = self.context.get("user") + semester = self.context.get("semester") + return obj.get_value(user, semester) - try: - individual_stat = IndividualStat.objects.filter( - user=user, - key=obj.individual_stat_key, - semester=semester - ).first() - return individual_stat.value - except IndividualStat.DoesNotExist: - return None +class IndividualStatPageFieldSerializer(PageFieldSerializer): + class Meta(PageFieldSerializer.Meta): + model = IndividualStatPageField -class GlobalStatThroughSerializer(serializers.ModelSerializer): - global_stat_value = serializers.SerializerMethodField() - class Meta: +class GlobalStatPageFieldSerializer(PageFieldSerializer): + class Meta(PageFieldSerializer.Meta): model = GlobalStatPageField - fields = ['text_field_name', 'global_stat_value'] - - def get_global_stat_value(self, obj): - semester = self.context.get('semester') - try: - global_stat = GlobalStat.objects.filter( - key=obj.global_stat_key.key, - semester=semester - ).first() - return global_stat.value - except GlobalStat.DoesNotExist: - return None - class PageSerializer(serializers.ModelSerializer): @@ -69,26 +32,27 @@ class PageSerializer(serializers.ModelSerializer): class Meta: model = Page - fields = ['name', 'template_path', 'combined_stats', 'duration'] + fields = ["name", "template_path", "combined_stats", "duration"] def get_combined_stats(self, obj): - user = self.context.get('user') - semester = self.context.get('semester') - combined_stats = {} + 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 - for entry in obj.individualstatpagefield_set.all(): - individual_stat_serializer = IndividualStatPageFieldSerializer( - entry, context={'user': user, 'semester': semester} - ) - combined_stats[entry.text_field_name] = individual_stat_serializer.data.get('individual_stat_value') + global_stat_fields = GlobalStatPageFieldSerializer( + obj.globalstatpagefield_set.all(), + context={"semester": semester, "user": user}, + many=True, + ).data - for entry in obj.globalstatpagefield_set.all(): - global_stat_serializer = GlobalStatThroughSerializer( - entry, context={'semester': semester} - ) - combined_stats[entry.text_field_name] = global_stat_serializer.data.get('global_stat_value') + field_list = individual_stat_fields + global_stat_fields - return combined_stats + return {field["text_field_name"]: field["stat_value"] for field in field_list} class SemesterSerializer(serializers.ModelSerializer): @@ -96,9 +60,10 @@ class SemesterSerializer(serializers.ModelSerializer): class Meta: model = Semester - fields = ['semester', 'pages'] + fields = ["semester", "pages"] def get_pages(self, obj): - user = self.context.get('user') - return PageSerializer(obj.pages.all(), many=True, context={'user': user, 'semester': obj}).data - + user = self.context.get("user") + return PageSerializer( + obj.pages.all(), many=True, context={"semester": obj, "user": user} + ).data diff --git a/backend/wrapped/signals.py b/backend/wrapped/signals.py deleted file mode 100644 index e346e2f6..00000000 --- a/backend/wrapped/signals.py +++ /dev/null @@ -1,34 +0,0 @@ -# from django.db.models.signals import m2m_changed -# from django.core.exceptions import ValidationError -# from django.db import transaction -# from django.dispatch import receiver -# from wrapped.models import Page - -# @receiver(m2m_changed, sender=Page.IndividualStat.through) -# @receiver(m2m_changed, sender=Page.GlobalStat.through) -# def validate_stats(sender, instance, action, **kwargs): -# if action == "post_add" or action == "post_remove" or action == "post_clear": -# transaction.on_commit(lambda: perform_validation(instance)) - -# def perform_validation(instance): -# individual_stat_count = instance.IndividualStat.count() -# global_stat_count = instance.GlobalStat.count() -# total_stat_count = individual_stat_count + global_stat_count - -# print(f"Total stat change: {total_stat_count}") -# print(f"Individual stat change: {individual_stat_count}") -# print(f"Global stat change: {global_stat_count}") - -# if total_stat_count != instance.template.num_fields: -# raise ValidationError( -# f"The total number of stats (IndividualStat + GlobalStat) " -# f"must equal the template's num_fields value ({instance.template.num_fields})." -# ) - -# individual_semesters = set(instance.IndividualStat.values_list('semester', flat=True)) -# global_semesters = set(instance.GlobalStat.values_list('semester', flat=True)) - -# all_semesters = individual_semesters.union(global_semesters) - -# if len(all_semesters) > 1: -# raise ValidationError("All IndividualStat and GlobalStat entries must be from the same semester.") diff --git a/backend/wrapped/tests.py b/backend/wrapped/tests.py index 7ce503c2..e69de29b 100644 --- a/backend/wrapped/tests.py +++ b/backend/wrapped/tests.py @@ -1,3 +0,0 @@ -from django.test import TestCase - -# Create your tests here. diff --git a/backend/wrapped/urls.py b/backend/wrapped/urls.py index e00e9b04..4f1e5850 100644 --- a/backend/wrapped/urls.py +++ b/backend/wrapped/urls.py @@ -1,7 +1,8 @@ -from django.contrib import admin from django.urls import path -from .views import SemesterView + +from wrapped.views import SemesterView + urlpatterns = [ - path('semester//', SemesterView.as_view(), name='semester-detail'), -] \ No newline at end of file + path("semester//", SemesterView.as_view(), name="semester-detail"), +] diff --git a/backend/wrapped/views.py b/backend/wrapped/views.py index 77e16486..defb0769 100644 --- a/backend/wrapped/views.py +++ b/backend/wrapped/views.py @@ -1,14 +1,15 @@ from rest_framework.permissions import IsAuthenticated -from rest_framework.views import APIView from rest_framework.response import Response -from rest_framework import status -from .models import Page, Semester -from .serializers import SemesterSerializer +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}) + serializer = SemesterSerializer(semester, context={"user": request.user}) return Response(serializer.data)