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 %}