diff --git a/backend/pennmobile/settings/base.py b/backend/pennmobile/settings/base.py index c54b3e21..058ff4ea 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..1f45902e 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..8647d64c --- /dev/null +++ b/backend/wrapped/admin.py @@ -0,0 +1,44 @@ +from django.contrib import admin +from django.core.exceptions import ValidationError + +from wrapped.models import GlobalStatKey, GlobalStat,IndividualStat, IndividualStatKey, Page, IndividualStatPageField, GlobalStatPageField, 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(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 new file mode 100644 index 00000000..e7886443 --- /dev/null +++ b/backend/wrapped/apps.py @@ -0,0 +1,8 @@ +from django.apps import AppConfig + + +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 diff --git a/backend/wrapped/migrations/0001_initial.py b/backend/wrapped/migrations/0001_initial.py new file mode 100644 index 00000000..52c7bc68 --- /dev/null +++ b/backend/wrapped/migrations/0001_initial.py @@ -0,0 +1,98 @@ +# 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/__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..5d015d4c --- /dev/null +++ b/backend/wrapped/models.py @@ -0,0 +1,89 @@ +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 + +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) + + 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 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, 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 __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 __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..f7a34c4a --- /dev/null +++ b/backend/wrapped/serializers.py @@ -0,0 +1,104 @@ +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'] + + +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 Meta: + model = IndividualStatPageField + fields = ['text_field_name', 'individual_stat_value'] + + def get_individual_stat_value(self, obj): + user = self.context.get('user') + semester = self.context.get('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 GlobalStatThroughSerializer(serializers.ModelSerializer): + global_stat_value = serializers.SerializerMethodField() + + class 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): + + combined_stats = serializers.SerializerMethodField() + + class Meta: + model = Page + 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 = {} + + 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') + + 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') + + return combined_stats + + +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={'user': user, 'semester': obj}).data + diff --git a/backend/wrapped/signals.py b/backend/wrapped/signals.py new file mode 100644 index 00000000..e346e2f6 --- /dev/null +++ b/backend/wrapped/signals.py @@ -0,0 +1,34 @@ +# 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 new file mode 100644 index 00000000..7ce503c2 --- /dev/null +++ b/backend/wrapped/tests.py @@ -0,0 +1,3 @@ +from django.test import TestCase + +# Create your tests here. diff --git a/backend/wrapped/urls.py b/backend/wrapped/urls.py new file mode 100644 index 00000000..e00e9b04 --- /dev/null +++ b/backend/wrapped/urls.py @@ -0,0 +1,7 @@ +from django.contrib import admin +from django.urls import path +from .views import SemesterView + +urlpatterns = [ + path('semester//', SemesterView.as_view(), name='semester-detail'), +] \ No newline at end of file diff --git a/backend/wrapped/views.py b/backend/wrapped/views.py new file mode 100644 index 00000000..77e16486 --- /dev/null +++ b/backend/wrapped/views.py @@ -0,0 +1,14 @@ +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 + +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)