From ceb06281ecaf44b0e8772a07166aff2df3947b87 Mon Sep 17 00:00:00 2001 From: colons Date: Sat, 24 Feb 2024 17:26:19 +0000 Subject: [PATCH 1/7] Add a model for enforcing pro roulette commitment. --- .../migrations/0024_proroulettecommitment.py | 58 +++++++++++++++++++ nkdsu/apps/vote/models.py | 15 +++++ 2 files changed, 73 insertions(+) create mode 100644 nkdsu/apps/vote/migrations/0024_proroulettecommitment.py diff --git a/nkdsu/apps/vote/migrations/0024_proroulettecommitment.py b/nkdsu/apps/vote/migrations/0024_proroulettecommitment.py new file mode 100644 index 00000000..e7d73687 --- /dev/null +++ b/nkdsu/apps/vote/migrations/0024_proroulettecommitment.py @@ -0,0 +1,58 @@ +from django.conf import settings +from django.db import migrations, models +import django.db.models.deletion +import nkdsu.apps.vote.models + + +class Migration(migrations.Migration): + + dependencies = [ + migrations.swappable_dependency(settings.AUTH_USER_MODEL), + ('vote', '0023_show_unique_showtime_dates'), + ] + + operations = [ + migrations.CreateModel( + name='ProRouletteCommitment', + fields=[ + ( + 'id', + models.AutoField( + auto_created=True, + primary_key=True, + serialize=False, + verbose_name='ID', + ), + ), + ( + 'show', + models.ForeignKey( + on_delete=django.db.models.deletion.PROTECT, to='vote.show' + ), + ), + ( + 'track', + models.ForeignKey( + on_delete=django.db.models.deletion.CASCADE, to='vote.track' + ), + ), + ( + 'user', + models.ForeignKey( + on_delete=django.db.models.deletion.CASCADE, + to=settings.AUTH_USER_MODEL, + ), + ), + ], + bases=(nkdsu.apps.vote.models.CleanOnSaveMixin, models.Model), + ), + migrations.AddConstraint( + model_name='proroulettecommitment', + constraint=models.UniqueConstraint( + models.F('show'), + models.F('user'), + name='pro_roulette_commitment_unique', + violation_error_message='a user can only have one pro roulette commitment per show', + ), + ), + ] diff --git a/nkdsu/apps/vote/models.py b/nkdsu/apps/vote/models.py index d7816342..dda70197 100644 --- a/nkdsu/apps/vote/models.py +++ b/nkdsu/apps/vote/models.py @@ -1831,6 +1831,21 @@ def __str__(self) -> str: return self.content +class ProRouletteCommitment(CleanOnSaveMixin, models.Model): + show = models.ForeignKey(Show, on_delete=models.PROTECT) + user = models.ForeignKey(User, on_delete=models.CASCADE) + track = models.ForeignKey(Track, on_delete=models.CASCADE) + + class Meta: + constraints = [ + models.UniqueConstraint( + *('show', 'user'), + name='pro_roulette_commitment_unique', + violation_error_message='a user can only have one pro roulette commitment per show', + ) + ] + + @dataclass class Badge: slug: str From fa0968edaff16453f7fb28de537da085ef450727 Mon Sep 17 00:00:00 2001 From: colons Date: Sat, 24 Feb 2024 20:27:25 +0000 Subject: [PATCH 2/7] Tie pro roulette commitments to user accounts. --- nkdsu/apps/vote/views/__init__.py | 63 ++++++++++++++++--------------- 1 file changed, 33 insertions(+), 30 deletions(-) diff --git a/nkdsu/apps/vote/views/__init__.py b/nkdsu/apps/vote/views/__init__.py index 6c930cae..44b7966c 100644 --- a/nkdsu/apps/vote/views/__init__.py +++ b/nkdsu/apps/vote/views/__init__.py @@ -5,11 +5,11 @@ from functools import cached_property from itertools import chain from random import sample -from typing import Any, Iterable, Optional, Sequence, cast +from typing import Any, Iterable, Optional, Sequence, cast, overload from django.conf import settings from django.contrib import messages -from django.contrib.auth.mixins import LoginRequiredMixin +from django.contrib.auth.mixins import AccessMixin, LoginRequiredMixin from django.contrib.auth.models import AnonymousUser from django.core.mail import send_mail from django.core.paginator import InvalidPage, Paginator @@ -36,6 +36,7 @@ from ..anime import get_anime, suggest_anime from ..forms import BadMetadataForm, DarkModeForm, RequestForm, VoteForm from ..models import ( + ProRouletteCommitment, Profile, Request, Role, @@ -190,7 +191,7 @@ def get(self, *a, **k) -> HttpResponse: return redirect(cast(Show, self.object).get_absolute_url()) -class Roulette(ListView): +class Roulette(ListView, AccessMixin): section = 'roulette' model = Track template_name = 'roulette.html' @@ -208,11 +209,13 @@ class Roulette(ListView): ] def get(self, request, *args, **kwargs) -> HttpResponse: - if kwargs.get('mode') != 'pro' and self.request.session.get( - self.pro_roulette_session_key() - ): + if kwargs.get('mode') != 'pro' and self.commitment() is not None: return redirect(reverse('vote:roulette', kwargs={'mode': 'pro'})) + elif kwargs.get('mode') == 'pro' and not request.user.is_authenticated: + messages.warning(self.request, 'pro roulette requires you to log in') + return self.handle_no_permission() + elif kwargs.get('mode') is None: if request.user.is_staff: mode = 'short' @@ -223,39 +226,39 @@ def get(self, request, *args, **kwargs) -> HttpResponse: else: return super().get(request, *args, **kwargs) - def pro_roulette_session_key(self) -> str: - return PRO_ROULETTE.format(Show.current().pk) + @overload + def commitment(self, commit_from: TrackQuerySet) -> ProRouletteCommitment: ... - def pro_pk(self) -> int: - sk = self.pro_roulette_session_key() - pk = self.request.session.get(self.pro_roulette_session_key()) + @overload + def commitment(self) -> Optional[ProRouletteCommitment]: ... - if pk is None: - for i in range(100): - track = self.get_base_queryset().order_by('?')[0] - if track.eligible(): - break + def commitment( + self, commit_from: Optional[TrackQuerySet] = None + ) -> Optional[ProRouletteCommitment]: + if not self.request.user.is_authenticated: + assert commit_from is None, 'cannot commit if not authenticated' + return None + try: + return ProRouletteCommitment.objects.get( + user=self.request.user, show=Show.current() + ) + except ProRouletteCommitment.DoesNotExist: + if commit_from: + return ProRouletteCommitment.objects.create( + user=self.request.user, + show=Show.current(), + track=next(t for t in commit_from.order_by('?') if t.eligible()), + ) else: - raise RuntimeError('are you sure anything is eligible') + return None - pk = track.pk - session = self.request.session - session[sk] = pk - session.save() - - return pk - - def pro_queryset(self, qs): - return qs.filter(pk=self.pro_pk()) - - def get_base_queryset(self): + def get_base_queryset(self) -> TrackQuerySet: return self.model.objects.public() def get_tracks(self) -> tuple[Iterable[Track], int]: qs = self.get_base_queryset() - if self.kwargs.get('mode') == 'pro': - qs = self.pro_queryset(qs) + qs = qs.filter(pk=self.commitment(commit_from=qs).track.pk) elif self.kwargs.get('mode') == 'hipster': qs = qs.filter(play=None) elif self.kwargs.get('mode') == 'almost-100': From 5b0657cd5eb1f38da1dd62b15d78d0ad112b8efd Mon Sep 17 00:00:00 2001 From: colons Date: Sat, 24 Feb 2024 20:38:26 +0000 Subject: [PATCH 3/7] Tell people when they committed to pro roulette. --- nkdsu/apps/vote/migrations/0024_proroulettecommitment.py | 3 ++- nkdsu/apps/vote/models.py | 1 + nkdsu/apps/vote/views/__init__.py | 1 + nkdsu/templates/roulette.html | 8 ++++++++ 4 files changed, 12 insertions(+), 1 deletion(-) diff --git a/nkdsu/apps/vote/migrations/0024_proroulettecommitment.py b/nkdsu/apps/vote/migrations/0024_proroulettecommitment.py index e7d73687..05149d99 100644 --- a/nkdsu/apps/vote/migrations/0024_proroulettecommitment.py +++ b/nkdsu/apps/vote/migrations/0024_proroulettecommitment.py @@ -1,6 +1,6 @@ +import django.db.models.deletion from django.conf import settings from django.db import migrations, models -import django.db.models.deletion import nkdsu.apps.vote.models @@ -43,6 +43,7 @@ class Migration(migrations.Migration): to=settings.AUTH_USER_MODEL, ), ), + ('created_at', models.DateTimeField(auto_now_add=True)), ], bases=(nkdsu.apps.vote.models.CleanOnSaveMixin, models.Model), ), diff --git a/nkdsu/apps/vote/models.py b/nkdsu/apps/vote/models.py index dda70197..aa45085f 100644 --- a/nkdsu/apps/vote/models.py +++ b/nkdsu/apps/vote/models.py @@ -1835,6 +1835,7 @@ class ProRouletteCommitment(CleanOnSaveMixin, models.Model): show = models.ForeignKey(Show, on_delete=models.PROTECT) user = models.ForeignKey(User, on_delete=models.CASCADE) track = models.ForeignKey(Track, on_delete=models.CASCADE) + created_at = models.DateTimeField(auto_now_add=True) class Meta: constraints = [ diff --git a/nkdsu/apps/vote/views/__init__.py b/nkdsu/apps/vote/views/__init__.py index 44b7966c..c28757d4 100644 --- a/nkdsu/apps/vote/views/__init__.py +++ b/nkdsu/apps/vote/views/__init__.py @@ -301,6 +301,7 @@ def get_context_data(self, **kwargs) -> dict[str, Any]: context.update( { + 'pro_roulette_commitment': self.commitment(), 'decades': Track.all_decades(), 'decade': int(decade_str) if decade_str else None, 'minutes': int(minutes_str) if minutes_str else None, diff --git a/nkdsu/templates/roulette.html b/nkdsu/templates/roulette.html index af206fd2..256b7e4d 100644 --- a/nkdsu/templates/roulette.html +++ b/nkdsu/templates/roulette.html @@ -30,6 +30,14 @@

roulette

{% endif %} + {% if pro_roulette_commitment %} +

+ you committed to pro roulette + on {{ pro_roulette_commitment.created_at|date:"F jS"|lower }} + at {{ pro_roulette_commitment.created_at|date:"g:i A"|lower }} +

+ {% endif %} + {% if mode == "short" %}

{% for minute_slug in allowed_minutes %} From 6f299c250e443d7f88b45b636b376f291dc03317 Mon Sep 17 00:00:00 2001 From: colons Date: Sat, 24 Feb 2024 20:39:57 +0000 Subject: [PATCH 4/7] Remove an unnecessary query. --- nkdsu/apps/vote/views/__init__.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/nkdsu/apps/vote/views/__init__.py b/nkdsu/apps/vote/views/__init__.py index c28757d4..aa56578d 100644 --- a/nkdsu/apps/vote/views/__init__.py +++ b/nkdsu/apps/vote/views/__init__.py @@ -258,7 +258,7 @@ def get_base_queryset(self) -> TrackQuerySet: def get_tracks(self) -> tuple[Iterable[Track], int]: qs = self.get_base_queryset() if self.kwargs.get('mode') == 'pro': - qs = qs.filter(pk=self.commitment(commit_from=qs).track.pk) + return ([self.commitment(commit_from=qs).track], 1) elif self.kwargs.get('mode') == 'hipster': qs = qs.filter(play=None) elif self.kwargs.get('mode') == 'almost-100': From 1c6571c420badc4e02c0c4c312d9dd5efcc6468e Mon Sep 17 00:00:00 2001 From: colons Date: Sat, 24 Feb 2024 20:47:22 +0000 Subject: [PATCH 5/7] Mention pro roulette commitments in the privacy policy. --- PRIVACY.md | 1 + 1 file changed, 1 insertion(+) diff --git a/PRIVACY.md b/PRIVACY.md index 3a2cdb1b..e48a8e35 100644 --- a/PRIVACY.md +++ b/PRIVACY.md @@ -60,6 +60,7 @@ This can include: (like Twitter, for instance) - a [hashed][django-password-storage] password - a screen name and a display name +- any commitments you have made under pro roulette - an avatar - email addresses - URLs of websites you choose to show on your profile page From 581653f3eb08400491545c7f74f96c015c2ba299 Mon Sep 17 00:00:00 2001 From: colons Date: Sat, 24 Feb 2024 20:57:32 +0000 Subject: [PATCH 6/7] Hit pro roulette last in the tests. This allows the tests to find breakages in other roulettes. --- nkdsu/tests.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/nkdsu/tests.py b/nkdsu/tests.py index 0580f2a8..fa4b8e1c 100644 --- a/nkdsu/tests.py +++ b/nkdsu/tests.py @@ -78,11 +78,11 @@ class EverythingTest( '/roulette/', '/roulette/hipster/', '/roulette/indiscriminate/', - '/roulette/pro/', '/roulette/staple/', '/roulette/decade/', '/roulette/decade/1970/', '/roulette/short/1/', + '/roulette/pro/', '/archive/', '/archive/2014/', '/stats/', From c0331b8899148aadc4deb4fa5638311edbb92eb5 Mon Sep 17 00:00:00 2001 From: colons Date: Sat, 24 Feb 2024 21:00:43 +0000 Subject: [PATCH 7/7] Handle cases where there are fewer than five staples. --- nkdsu/apps/vote/views/__init__.py | 3 ++- nkdsu/templates/roulette.html | 4 +++- 2 files changed, 5 insertions(+), 2 deletions(-) diff --git a/nkdsu/apps/vote/views/__init__.py b/nkdsu/apps/vote/views/__init__.py index aa56578d..f313b53a 100644 --- a/nkdsu/apps/vote/views/__init__.py +++ b/nkdsu/apps/vote/views/__init__.py @@ -283,7 +283,8 @@ def get_tracks(self) -> tuple[Iterable[Track], int]: .filter(time_per_play__lt=parse_duration('365 days')) ) # order_by('?') fails when annotate() has been used - return (sample(list(qs), 5), qs.count()) + staples = list(qs) + return (sample(staples, min(len(staples), 5)), len(staples)) elif self.kwargs.get('mode') == 'short': length_msec = ( int(self.kwargs.get('minutes', self.default_minutes_count)) * 60 * 1000 diff --git a/nkdsu/templates/roulette.html b/nkdsu/templates/roulette.html index 256b7e4d..0b42b85b 100644 --- a/nkdsu/templates/roulette.html +++ b/nkdsu/templates/roulette.html @@ -64,8 +64,10 @@

roulette

these are all terrible {% elif option_count > 1 %} i would like to see these {{ option_count }} tracks in a new, random order - {% else %} + {% elif option_count == 1 %} oh, just one, huh? can… can i try again anyway? + {% else %} + if there's nothing, it's probably not worth trying again. but i still want to {% endif %}