From b98af14f380ca0545181727771eead75334cdf78 Mon Sep 17 00:00:00 2001 From: Marcin Kurczewski Date: Sun, 6 Aug 2023 15:42:12 +0200 Subject: [PATCH] playlists: add played level count --- backend/trcustoms/playlists/apps.py | 3 ++ ...ylistitem_level_alter_playlistitem_user.py | 34 +++++++++++++++++++ backend/trcustoms/playlists/models.py | 20 +++++++++-- backend/trcustoms/playlists/signals.py | 19 +++++++++++ .../tests/test_playlist_items_create.py | 8 +++++ .../tests/test_playlist_items_delete.py | 25 +++++++++++--- .../0014_user_played_level_count.py | 17 ++++++++++ backend/trcustoms/users/models.py | 5 +++ backend/trcustoms/users/serializers.py | 2 ++ frontend/src/services/UserService.ts | 1 + 10 files changed, 128 insertions(+), 6 deletions(-) create mode 100644 backend/trcustoms/playlists/migrations/0002_alter_playlistitem_level_alter_playlistitem_user.py create mode 100644 backend/trcustoms/playlists/signals.py create mode 100644 backend/trcustoms/users/migrations/0014_user_played_level_count.py diff --git a/backend/trcustoms/playlists/apps.py b/backend/trcustoms/playlists/apps.py index 0cb54597..fff464d3 100644 --- a/backend/trcustoms/playlists/apps.py +++ b/backend/trcustoms/playlists/apps.py @@ -5,3 +5,6 @@ class PlaylistsConfig(AppConfig): default_auto_field = "django.db.models.BigAutoField" name = "trcustoms.playlists" + + def ready(self): + import trcustoms.playlists.signals # noqa: F401 diff --git a/backend/trcustoms/playlists/migrations/0002_alter_playlistitem_level_alter_playlistitem_user.py b/backend/trcustoms/playlists/migrations/0002_alter_playlistitem_level_alter_playlistitem_user.py new file mode 100644 index 00000000..1efa2db3 --- /dev/null +++ b/backend/trcustoms/playlists/migrations/0002_alter_playlistitem_level_alter_playlistitem_user.py @@ -0,0 +1,34 @@ +# Generated by Django 4.2.3 on 2023-08-06 13:36 + +import django.db.models.deletion +from django.conf import settings +from django.db import migrations, models + + +class Migration(migrations.Migration): + dependencies = [ + migrations.swappable_dependency(settings.AUTH_USER_MODEL), + ("levels", "0014_auto_20230509_1033"), + ("playlists", "0001_initial"), + ] + + operations = [ + migrations.AlterField( + model_name="playlistitem", + name="level", + field=models.ForeignKey( + on_delete=django.db.models.deletion.CASCADE, + related_name="playlist_items", + to="levels.level", + ), + ), + migrations.AlterField( + model_name="playlistitem", + name="user", + field=models.ForeignKey( + on_delete=django.db.models.deletion.CASCADE, + related_name="playlist_items", + to=settings.AUTH_USER_MODEL, + ), + ), + ] diff --git a/backend/trcustoms/playlists/models.py b/backend/trcustoms/playlists/models.py index 92778db9..7a19590e 100644 --- a/backend/trcustoms/playlists/models.py +++ b/backend/trcustoms/playlists/models.py @@ -7,10 +7,26 @@ from trcustoms.users.models import User +class PlaylistItemQuerySet(models.QuerySet): + def played(self) -> models.QuerySet: + return self.filter( + status__in=[ + PlaylistStatus.FINISHED, + PlaylistStatus.PLAYING, + PlaylistStatus.DROPPED, + PlaylistStatus.ON_HOLD, + ] + ) + + class PlaylistItem(DatesInfo): - user = models.ForeignKey(User, on_delete=models.CASCADE, related_name="+") + objects = PlaylistItemQuerySet.as_manager() + + user = models.ForeignKey( + User, on_delete=models.CASCADE, related_name="playlist_items" + ) level = models.ForeignKey( - Level, on_delete=models.CASCADE, related_name="+" + Level, on_delete=models.CASCADE, related_name="playlist_items" ) status = models.CharField( choices=PlaylistStatus.choices, diff --git a/backend/trcustoms/playlists/signals.py b/backend/trcustoms/playlists/signals.py new file mode 100644 index 00000000..1823c1c3 --- /dev/null +++ b/backend/trcustoms/playlists/signals.py @@ -0,0 +1,19 @@ +from django.db.models.signals import post_delete, post_save, pre_delete +from django.dispatch import receiver + +from trcustoms.playlists.models import PlaylistItem + + +@receiver(post_save, sender=PlaylistItem) +def update_level_player_on_playlist_item_change(sender, instance, **kwargs): + instance.user.update_played_level_count() + + +@receiver(pre_delete, sender=PlaylistItem) +def remember_playlist_item_user(sender, instance, using, **kwargs): + instance._old_user = instance.user + + +@receiver(post_delete, sender=PlaylistItem) +def update_level_player_on_playlist_item_delete(sender, instance, **kwargs): + instance._old_user.update_played_level_count() diff --git a/backend/trcustoms/playlists/tests/test_playlist_items_create.py b/backend/trcustoms/playlists/tests/test_playlist_items_create.py index 6899e8e8..e197dd12 100644 --- a/backend/trcustoms/playlists/tests/test_playlist_items_create.py +++ b/backend/trcustoms/playlists/tests/test_playlist_items_create.py @@ -178,3 +178,11 @@ def test_playlist_item_creation_success( assert playlist_item.user.pk == auth_api_client.user.pk assert playlist_item.level.pk == level.pk assert playlist_item.status == PlaylistStatus.ON_HOLD + + +@pytest.mark.django_db +def test_playlist_item_creation_updates_played_level_count() -> None: + user = UserFactory() + PlaylistItemFactory(user=user, status=PlaylistStatus.FINISHED) + user.refresh_from_db() + assert user.played_level_count == 1 diff --git a/backend/trcustoms/playlists/tests/test_playlist_items_delete.py b/backend/trcustoms/playlists/tests/test_playlist_items_delete.py index 1f3344c6..2385df82 100644 --- a/backend/trcustoms/playlists/tests/test_playlist_items_delete.py +++ b/backend/trcustoms/playlists/tests/test_playlist_items_delete.py @@ -2,12 +2,14 @@ from rest_framework import status from rest_framework.test import APIClient +from trcustoms.playlists.consts import PlaylistStatus from trcustoms.playlists.models import PlaylistItem from trcustoms.playlists.tests.factories import PlaylistItemFactory +from trcustoms.users.tests.factories import UserFactory @pytest.mark.django_db -def test_walkthrough_deletion_requires_login( +def test_playlist_item_deletion_requires_login( api_client: APIClient, ) -> None: playlist_item = PlaylistItemFactory() @@ -22,7 +24,7 @@ def test_walkthrough_deletion_requires_login( @pytest.mark.django_db -def test_walkthrough_deletion_rejects_non_owner( +def test_playlist_item_deletion_rejects_non_owner( auth_api_client: APIClient, ) -> None: playlist_item = PlaylistItemFactory(user__username="unique user") @@ -37,7 +39,7 @@ def test_walkthrough_deletion_rejects_non_owner( @pytest.mark.django_db -def test_walkthrough_deletion_accepts_owner( +def test_playlist_item_deletion_accepts_owner( auth_api_client: APIClient, ) -> None: playlist_item = PlaylistItemFactory(user=auth_api_client.user) @@ -49,7 +51,7 @@ def test_walkthrough_deletion_accepts_owner( @pytest.mark.django_db -def test_walkthrough_deletion_accepts_admin( +def test_playlist_item_deletion_accepts_admin( staff_api_client: APIClient, ) -> None: playlist_item = PlaylistItemFactory() @@ -58,3 +60,18 @@ def test_walkthrough_deletion_accepts_admin( ) assert resp.status_code == status.HTTP_204_NO_CONTENT assert not PlaylistItem.objects.filter(pk=playlist_item.pk).exists() + + +@pytest.mark.django_db +def test_playlist_item_deletion_updates_played_level_count( + superuser_api_client: APIClient, +) -> None: + user = UserFactory() + playlist_item = PlaylistItemFactory( + user=user, status=PlaylistStatus.FINISHED + ) + user.refresh_from_db() + assert user.played_level_count == 1 + playlist_item.delete() + user.refresh_from_db() + assert user.played_level_count == 0 diff --git a/backend/trcustoms/users/migrations/0014_user_played_level_count.py b/backend/trcustoms/users/migrations/0014_user_played_level_count.py new file mode 100644 index 00000000..04d74d37 --- /dev/null +++ b/backend/trcustoms/users/migrations/0014_user_played_level_count.py @@ -0,0 +1,17 @@ +# Generated by Django 4.2.3 on 2023-08-06 13:36 + +from django.db import migrations, models + + +class Migration(migrations.Migration): + dependencies = [ + ("users", "0013_alter_user_options"), + ] + + operations = [ + migrations.AddField( + model_name="user", + name="played_level_count", + field=models.PositiveIntegerField(default=0), + ), + ] diff --git a/backend/trcustoms/users/models.py b/backend/trcustoms/users/models.py index 58809043..15086725 100644 --- a/backend/trcustoms/users/models.py +++ b/backend/trcustoms/users/models.py @@ -106,6 +106,7 @@ class Meta(AbstractUser.Meta): Country, null=True, blank=True, on_delete=models.SET_NULL ) + played_level_count = models.PositiveIntegerField(default=0) authored_level_count = models.PositiveIntegerField(default=0) reviewed_level_count = models.PositiveIntegerField(default=0) authored_walkthrough_count = models.PositiveIntegerField(default=0) @@ -126,6 +127,10 @@ def update_reviewed_level_count(self) -> None: ).count() self.save(update_fields=["reviewed_level_count"]) + def update_played_level_count(self) -> None: + self.played_level_count = self.playlist_items.played().count() + self.save(update_fields=["played_level_count"]) + def update_authored_level_count(self) -> None: self.authored_level_count = self.authored_levels.filter( is_approved=True diff --git a/backend/trcustoms/users/serializers.py b/backend/trcustoms/users/serializers.py index 6f354845..f46c895e 100644 --- a/backend/trcustoms/users/serializers.py +++ b/backend/trcustoms/users/serializers.py @@ -42,6 +42,7 @@ class UserListingSerializer(serializers.ModelSerializer): permissions = serializers.SerializerMethodField(read_only=True) date_joined = serializers.ReadOnlyField() last_login = serializers.ReadOnlyField() + played_level_count = serializers.ReadOnlyField() authored_level_count = serializers.ReadOnlyField() authored_walkthrough_count = serializers.ReadOnlyField() reviewed_level_count = serializers.ReadOnlyField() @@ -62,6 +63,7 @@ class Meta: "is_active", "is_banned", "is_pending_activation", + "played_level_count", "authored_level_count", "authored_walkthrough_count", "reviewed_level_count", diff --git a/frontend/src/services/UserService.ts b/frontend/src/services/UserService.ts index 6f4b11d5..704988bc 100644 --- a/frontend/src/services/UserService.ts +++ b/frontend/src/services/UserService.ts @@ -52,6 +52,7 @@ interface UserListing extends UserNested { is_active: boolean; is_banned: boolean; is_pending_activation: boolean; + played_level_count: number; authored_level_count: number; reviewed_level_count: number; authored_walkthrough_count: number;