[scrobbles] Add PersonScrobble junction table
This commit is contained in:
@ -1,5 +1,5 @@
|
|||||||
from django.contrib import admin
|
from django.contrib import admin
|
||||||
from people.models import Person
|
from people.models import Person, PersonScrobble
|
||||||
|
|
||||||
|
|
||||||
@admin.register(Person)
|
@admin.register(Person)
|
||||||
@ -8,3 +8,11 @@ class PersonAdmin(admin.ModelAdmin):
|
|||||||
list_display = ("name", "bgg_username", "bgstats_id")
|
list_display = ("name", "bgg_username", "bgstats_id")
|
||||||
ordering = ("-created",)
|
ordering = ("-created",)
|
||||||
search_fields = ("name",)
|
search_fields = ("name",)
|
||||||
|
|
||||||
|
|
||||||
|
@admin.register(PersonScrobble)
|
||||||
|
class PersonScrobbleAdmin(admin.ModelAdmin):
|
||||||
|
list_display = ("person", "user", "scrobble", "created")
|
||||||
|
list_filter = ("created",)
|
||||||
|
ordering = ("-created",)
|
||||||
|
raw_id_fields = ("person", "user", "scrobble")
|
||||||
|
|||||||
@ -1,15 +1,24 @@
|
|||||||
from django.core.management.base import BaseCommand
|
from django.core.management.base import BaseCommand
|
||||||
|
|
||||||
from people.models import Person
|
from people.models import PersonScrobble
|
||||||
|
from people.signals import get_person_ids_from_log
|
||||||
|
from scrobbles.models import Scrobble
|
||||||
|
|
||||||
|
|
||||||
class Command(BaseCommand):
|
class Command(BaseCommand):
|
||||||
help = "Backfill scrobble counts for all people"
|
help = "Backfill PersonScrobble junction table from existing scrobbles"
|
||||||
|
|
||||||
def handle(self, *args, **options):
|
def handle(self, *args, **options):
|
||||||
for person in Person.objects.all():
|
count = 0
|
||||||
person.update_scrobble_count()
|
for scrobble in Scrobble.objects.all():
|
||||||
self.stdout.write(
|
person_ids = get_person_ids_from_log(scrobble.log)
|
||||||
f"{person.name}: {person.scrobble_count} scrobbles"
|
for person_id in person_ids:
|
||||||
)
|
obj, created = PersonScrobble.objects.get_or_create(
|
||||||
|
person_id=person_id,
|
||||||
|
user=scrobble.user,
|
||||||
|
scrobble=scrobble,
|
||||||
|
)
|
||||||
|
if created:
|
||||||
|
count += 1
|
||||||
|
self.stdout.write(f"Created {count} PersonScrobble records")
|
||||||
self.stdout.write(self.style.SUCCESS("Done!"))
|
self.stdout.write(self.style.SUCCESS("Done!"))
|
||||||
|
|||||||
@ -0,0 +1,84 @@
|
|||||||
|
# Generated by Django 4.2.29 on 2026-03-21 17:14
|
||||||
|
|
||||||
|
from django.conf import settings
|
||||||
|
from django.db import migrations, models
|
||||||
|
import django.db.models.deletion
|
||||||
|
import django_extensions.db.fields
|
||||||
|
|
||||||
|
|
||||||
|
class Migration(migrations.Migration):
|
||||||
|
|
||||||
|
dependencies = [
|
||||||
|
("scrobbles", "0072_add_scrobble_indexes"),
|
||||||
|
migrations.swappable_dependency(settings.AUTH_USER_MODEL),
|
||||||
|
("people", "0005_person_boardgamearena_id"),
|
||||||
|
]
|
||||||
|
|
||||||
|
operations = [
|
||||||
|
migrations.RemoveField(
|
||||||
|
model_name="person",
|
||||||
|
name="scrobble_count",
|
||||||
|
),
|
||||||
|
migrations.CreateModel(
|
||||||
|
name="PersonScrobble",
|
||||||
|
fields=[
|
||||||
|
(
|
||||||
|
"id",
|
||||||
|
models.BigAutoField(
|
||||||
|
auto_created=True,
|
||||||
|
primary_key=True,
|
||||||
|
serialize=False,
|
||||||
|
verbose_name="ID",
|
||||||
|
),
|
||||||
|
),
|
||||||
|
(
|
||||||
|
"created",
|
||||||
|
django_extensions.db.fields.CreationDateTimeField(
|
||||||
|
auto_now_add=True, verbose_name="created"
|
||||||
|
),
|
||||||
|
),
|
||||||
|
(
|
||||||
|
"modified",
|
||||||
|
django_extensions.db.fields.ModificationDateTimeField(
|
||||||
|
auto_now=True, verbose_name="modified"
|
||||||
|
),
|
||||||
|
),
|
||||||
|
(
|
||||||
|
"person",
|
||||||
|
models.ForeignKey(
|
||||||
|
on_delete=django.db.models.deletion.CASCADE,
|
||||||
|
related_name="scrobble_associations",
|
||||||
|
to="people.person",
|
||||||
|
),
|
||||||
|
),
|
||||||
|
(
|
||||||
|
"scrobble",
|
||||||
|
models.ForeignKey(
|
||||||
|
on_delete=django.db.models.deletion.CASCADE,
|
||||||
|
to="scrobbles.scrobble",
|
||||||
|
),
|
||||||
|
),
|
||||||
|
(
|
||||||
|
"user",
|
||||||
|
models.ForeignKey(
|
||||||
|
on_delete=django.db.models.deletion.CASCADE,
|
||||||
|
related_name="person_scrobbles",
|
||||||
|
to=settings.AUTH_USER_MODEL,
|
||||||
|
),
|
||||||
|
),
|
||||||
|
],
|
||||||
|
options={
|
||||||
|
"indexes": [
|
||||||
|
models.Index(
|
||||||
|
fields=["person", "user"],
|
||||||
|
name="people_pers_person__b7ebc7_idx",
|
||||||
|
),
|
||||||
|
models.Index(
|
||||||
|
fields=["user", "scrobble"],
|
||||||
|
name="people_pers_user_id_4f5fe7_idx",
|
||||||
|
),
|
||||||
|
],
|
||||||
|
"unique_together": {("person", "scrobble")},
|
||||||
|
},
|
||||||
|
),
|
||||||
|
]
|
||||||
@ -1,5 +1,6 @@
|
|||||||
from django.contrib.auth import get_user_model
|
from django.contrib.auth import get_user_model
|
||||||
from django.db import models
|
from django.db import models
|
||||||
|
from django.shortcuts import get_object_or_404
|
||||||
from django_extensions.db.models import TimeStampedModel
|
from django_extensions.db.models import TimeStampedModel
|
||||||
|
|
||||||
User = get_user_model()
|
User = get_user_model()
|
||||||
@ -22,25 +23,51 @@ class Person(TimeStampedModel):
|
|||||||
bgg_username = models.CharField(max_length=100, **BNULL)
|
bgg_username = models.CharField(max_length=100, **BNULL)
|
||||||
lichess_username = models.CharField(max_length=100, **BNULL)
|
lichess_username = models.CharField(max_length=100, **BNULL)
|
||||||
bio = models.TextField(**BNULL)
|
bio = models.TextField(**BNULL)
|
||||||
scrobble_count = models.IntegerField(default=0)
|
|
||||||
|
|
||||||
def __str__(self):
|
def __str__(self):
|
||||||
return self.name
|
return self.name
|
||||||
|
|
||||||
def update_scrobble_count(self):
|
def get_scrobble_count(self, user=None):
|
||||||
|
if user is None:
|
||||||
|
user = self.created_by
|
||||||
|
return PersonScrobble.objects.filter(person=self, user=user).count()
|
||||||
|
|
||||||
|
def get_scrobbles(self, user=None):
|
||||||
|
if user is None:
|
||||||
|
user = self.created_by
|
||||||
from scrobbles.models import Scrobble
|
from scrobbles.models import Scrobble
|
||||||
|
|
||||||
count = 0
|
return Scrobble.objects.filter(
|
||||||
for scrobble in Scrobble.objects.filter(user=self.created_by):
|
id__in=PersonScrobble.objects.filter(person=self, user=user).values_list(
|
||||||
if scrobble.log and isinstance(scrobble.log, dict):
|
"scrobble_id", flat=True
|
||||||
person_ids = scrobble.log.get("with_people_ids") or []
|
)
|
||||||
if person_ids and self.id in person_ids:
|
).order_by("-timestamp")
|
||||||
count += 1
|
|
||||||
continue
|
|
||||||
players = scrobble.log.get("players") or []
|
class PersonScrobble(TimeStampedModel):
|
||||||
for player in players:
|
"""Tracks the relationship between a Person and a Scrobble."""
|
||||||
if isinstance(player, dict) and player.get("person_id") == self.id:
|
|
||||||
count += 1
|
person = models.ForeignKey(
|
||||||
break
|
Person,
|
||||||
self.scrobble_count = count
|
on_delete=models.CASCADE,
|
||||||
self.save(update_fields=["scrobble_count"])
|
related_name="scrobble_associations",
|
||||||
|
)
|
||||||
|
user = models.ForeignKey(
|
||||||
|
User,
|
||||||
|
on_delete=models.CASCADE,
|
||||||
|
related_name="person_scrobbles",
|
||||||
|
)
|
||||||
|
scrobble = models.ForeignKey(
|
||||||
|
"scrobbles.Scrobble",
|
||||||
|
on_delete=models.CASCADE,
|
||||||
|
)
|
||||||
|
|
||||||
|
class Meta:
|
||||||
|
unique_together = ["person", "scrobble"]
|
||||||
|
indexes = [
|
||||||
|
models.Index(fields=["person", "user"]),
|
||||||
|
models.Index(fields=["user", "scrobble"]),
|
||||||
|
]
|
||||||
|
|
||||||
|
def __str__(self):
|
||||||
|
return f"{self.person.name} - {self.scrobble_id}"
|
||||||
|
|||||||
@ -4,25 +4,55 @@ from django.dispatch import receiver
|
|||||||
from scrobbles.models import Scrobble
|
from scrobbles.models import Scrobble
|
||||||
|
|
||||||
|
|
||||||
@receiver(post_save, sender=Scrobble)
|
def get_person_ids_from_log(log):
|
||||||
def update_person_scrobble_count_on_save(sender, instance, **kwargs):
|
if not log or not isinstance(log, dict):
|
||||||
if instance.log and isinstance(instance.log, dict):
|
return set()
|
||||||
person_ids = instance.log.get("with_people_ids", [])
|
|
||||||
for person_id in person_ids:
|
|
||||||
from people.models import Person
|
|
||||||
|
|
||||||
person = Person.objects.filter(id=person_id).first()
|
person_ids = set()
|
||||||
if person:
|
|
||||||
person.update_scrobble_count()
|
with_people_ids = log.get("with_people_ids") or []
|
||||||
|
if isinstance(with_people_ids, list):
|
||||||
|
person_ids.update(with_people_ids)
|
||||||
|
|
||||||
|
players = log.get("players") or []
|
||||||
|
if isinstance(players, list):
|
||||||
|
for player in players:
|
||||||
|
if isinstance(player, dict) and player.get("person_id"):
|
||||||
|
person_ids.add(player["person_id"])
|
||||||
|
|
||||||
|
return person_ids
|
||||||
|
|
||||||
|
|
||||||
|
@receiver(post_save, sender=Scrobble)
|
||||||
|
def sync_person_scrobbles_on_save(sender, instance, **kwargs):
|
||||||
|
from people.models import Person, PersonScrobble
|
||||||
|
|
||||||
|
person_ids = get_person_ids_from_log(instance.log)
|
||||||
|
|
||||||
|
existing = set(
|
||||||
|
PersonScrobble.objects.filter(scrobble=instance).values_list(
|
||||||
|
"person_id", flat=True
|
||||||
|
)
|
||||||
|
)
|
||||||
|
|
||||||
|
for person_id in person_ids:
|
||||||
|
person = Person.objects.filter(id=person_id).first()
|
||||||
|
if person and person.user and person.user == instance.user:
|
||||||
|
continue
|
||||||
|
if person_id not in existing:
|
||||||
|
PersonScrobble.objects.get_or_create(
|
||||||
|
person_id=person_id,
|
||||||
|
user=instance.user,
|
||||||
|
scrobble=instance,
|
||||||
|
)
|
||||||
|
|
||||||
|
PersonScrobble.objects.filter(scrobble=instance).exclude(
|
||||||
|
person_id__in=person_ids
|
||||||
|
).delete()
|
||||||
|
|
||||||
|
|
||||||
@receiver(post_delete, sender=Scrobble)
|
@receiver(post_delete, sender=Scrobble)
|
||||||
def update_person_scrobble_count_on_delete(sender, instance, **kwargs):
|
def sync_person_scrobbles_on_delete(sender, instance, **kwargs):
|
||||||
if instance.log and isinstance(instance.log, dict):
|
from people.models import PersonScrobble
|
||||||
person_ids = instance.log.get("with_people_ids", [])
|
|
||||||
for person_id in person_ids:
|
|
||||||
from people.models import Person
|
|
||||||
|
|
||||||
person = Person.objects.filter(id=person_id).first()
|
PersonScrobble.objects.filter(scrobble=instance).delete()
|
||||||
if person:
|
|
||||||
person.update_scrobble_count()
|
|
||||||
|
|||||||
0
vrobbler/apps/people/tests/__init__.py
Normal file
0
vrobbler/apps/people/tests/__init__.py
Normal file
180
vrobbler/apps/people/tests/test_person_scrobble.py
Normal file
180
vrobbler/apps/people/tests/test_person_scrobble.py
Normal file
@ -0,0 +1,180 @@
|
|||||||
|
import pytest
|
||||||
|
|
||||||
|
from django.contrib.auth import get_user_model
|
||||||
|
from scrobbles.models import Scrobble
|
||||||
|
|
||||||
|
from people.models import Person, PersonScrobble
|
||||||
|
|
||||||
|
User = get_user_model()
|
||||||
|
|
||||||
|
|
||||||
|
@pytest.mark.django_db
|
||||||
|
def test_signal_creates_person_scrobble_on_save_with_people_ids():
|
||||||
|
creator = User.objects.create_user(username="creator", password="testpass123")
|
||||||
|
other_user = User.objects.create_user(username="other", password="testpass123")
|
||||||
|
person = Person.objects.create(name="Alice", created_by=creator, user=creator)
|
||||||
|
|
||||||
|
scrobble = Scrobble.objects.create(
|
||||||
|
user=other_user,
|
||||||
|
log={"with_people_ids": [person.id]},
|
||||||
|
)
|
||||||
|
|
||||||
|
assert PersonScrobble.objects.filter(person=person, user=other_user).count() == 1
|
||||||
|
|
||||||
|
|
||||||
|
@pytest.mark.django_db
|
||||||
|
def test_signal_creates_person_scrobble_on_save_with_players():
|
||||||
|
creator = User.objects.create_user(username="creator", password="testpass123")
|
||||||
|
other_user = User.objects.create_user(username="other", password="testpass123")
|
||||||
|
person = Person.objects.create(name="Bob", created_by=creator, user=creator)
|
||||||
|
|
||||||
|
scrobble = Scrobble.objects.create(
|
||||||
|
user=other_user,
|
||||||
|
log={
|
||||||
|
"players": [
|
||||||
|
{"person_id": person.id, "win": True, "rank": 1},
|
||||||
|
]
|
||||||
|
},
|
||||||
|
)
|
||||||
|
|
||||||
|
assert PersonScrobble.objects.filter(person=person, user=other_user).count() == 1
|
||||||
|
|
||||||
|
|
||||||
|
@pytest.mark.django_db
|
||||||
|
def test_signal_removes_person_scrobble_when_person_removed_from_with_people_ids():
|
||||||
|
creator = User.objects.create_user(username="creator", password="testpass123")
|
||||||
|
other_user = User.objects.create_user(username="other", password="testpass123")
|
||||||
|
person = Person.objects.create(name="Charlie", created_by=creator, user=creator)
|
||||||
|
|
||||||
|
scrobble = Scrobble.objects.create(
|
||||||
|
user=other_user,
|
||||||
|
log={"with_people_ids": [person.id]},
|
||||||
|
)
|
||||||
|
assert PersonScrobble.objects.filter(person=person).count() == 1
|
||||||
|
|
||||||
|
scrobble.log = {}
|
||||||
|
scrobble.save()
|
||||||
|
|
||||||
|
assert PersonScrobble.objects.filter(person=person).count() == 0
|
||||||
|
|
||||||
|
|
||||||
|
@pytest.mark.django_db
|
||||||
|
def test_signal_removes_person_scrobble_when_person_removed_from_players():
|
||||||
|
creator = User.objects.create_user(username="creator", password="testpass123")
|
||||||
|
other_user = User.objects.create_user(username="other", password="testpass123")
|
||||||
|
person = Person.objects.create(name="Diana", created_by=creator, user=creator)
|
||||||
|
|
||||||
|
scrobble = Scrobble.objects.create(
|
||||||
|
user=other_user,
|
||||||
|
log={
|
||||||
|
"players": [
|
||||||
|
{"person_id": person.id, "win": True, "rank": 1},
|
||||||
|
]
|
||||||
|
},
|
||||||
|
)
|
||||||
|
assert PersonScrobble.objects.filter(person=person).count() == 1
|
||||||
|
|
||||||
|
scrobble.log = {"players": []}
|
||||||
|
scrobble.save()
|
||||||
|
|
||||||
|
assert PersonScrobble.objects.filter(person=person).count() == 0
|
||||||
|
|
||||||
|
|
||||||
|
@pytest.mark.django_db
|
||||||
|
def test_signal_deletes_person_scrobble_on_scrobble_delete():
|
||||||
|
creator = User.objects.create_user(username="creator", password="testpass123")
|
||||||
|
other_user = User.objects.create_user(username="other", password="testpass123")
|
||||||
|
person = Person.objects.create(name="Eve", created_by=creator, user=creator)
|
||||||
|
|
||||||
|
scrobble = Scrobble.objects.create(
|
||||||
|
user=other_user,
|
||||||
|
log={"with_people_ids": [person.id]},
|
||||||
|
)
|
||||||
|
assert PersonScrobble.objects.filter(person=person).count() == 1
|
||||||
|
|
||||||
|
scrobble.delete()
|
||||||
|
|
||||||
|
assert PersonScrobble.objects.filter(person=person).count() == 0
|
||||||
|
|
||||||
|
|
||||||
|
@pytest.mark.django_db
|
||||||
|
def test_signal_skips_when_person_user_matches_scrobble_user():
|
||||||
|
user = User.objects.create_user(username="testuser", password="testpass123")
|
||||||
|
person = Person.objects.create(name="Frank", created_by=user, user=user)
|
||||||
|
|
||||||
|
scrobble = Scrobble.objects.create(
|
||||||
|
user=user,
|
||||||
|
log={"with_people_ids": [person.id]},
|
||||||
|
)
|
||||||
|
|
||||||
|
assert PersonScrobble.objects.filter(person=person).count() == 0
|
||||||
|
|
||||||
|
|
||||||
|
@pytest.mark.django_db
|
||||||
|
def test_signal_works_with_different_users():
|
||||||
|
user1 = User.objects.create_user(username="user1", password="testpass123")
|
||||||
|
user2 = User.objects.create_user(username="user2", password="testpass123")
|
||||||
|
person = Person.objects.create(name="Grace", created_by=user1, user=user1)
|
||||||
|
|
||||||
|
scrobble1 = Scrobble.objects.create(
|
||||||
|
user=user1,
|
||||||
|
log={"with_people_ids": [person.id]},
|
||||||
|
)
|
||||||
|
scrobble2 = Scrobble.objects.create(
|
||||||
|
user=user2,
|
||||||
|
log={"with_people_ids": [person.id]},
|
||||||
|
)
|
||||||
|
|
||||||
|
assert PersonScrobble.objects.filter(person=person, user=user1).count() == 0
|
||||||
|
assert PersonScrobble.objects.filter(person=person, user=user2).count() == 1
|
||||||
|
|
||||||
|
|
||||||
|
@pytest.mark.django_db
|
||||||
|
def test_person_get_scrobble_count():
|
||||||
|
creator = User.objects.create_user(username="creator", password="testpass123")
|
||||||
|
other_user = User.objects.create_user(username="other", password="testpass123")
|
||||||
|
person = Person.objects.create(name="Henry", created_by=creator, user=creator)
|
||||||
|
|
||||||
|
Scrobble.objects.create(user=other_user, log={"with_people_ids": [person.id]})
|
||||||
|
Scrobble.objects.create(user=other_user, log={"with_people_ids": [person.id]})
|
||||||
|
Scrobble.objects.create(user=other_user, log={})
|
||||||
|
|
||||||
|
assert person.get_scrobble_count(user=other_user) == 2
|
||||||
|
|
||||||
|
|
||||||
|
@pytest.mark.django_db
|
||||||
|
def test_person_get_scrobbles():
|
||||||
|
creator = User.objects.create_user(username="creator", password="testpass123")
|
||||||
|
other_user = User.objects.create_user(username="other", password="testpass123")
|
||||||
|
person = Person.objects.create(name="Iris", created_by=creator, user=creator)
|
||||||
|
|
||||||
|
scrobble1 = Scrobble.objects.create(
|
||||||
|
user=other_user, log={"with_people_ids": [person.id]}
|
||||||
|
)
|
||||||
|
scrobble2 = Scrobble.objects.create(
|
||||||
|
user=other_user, log={"with_people_ids": [person.id]}
|
||||||
|
)
|
||||||
|
Scrobble.objects.create(user=other_user, log={})
|
||||||
|
|
||||||
|
scrobbles = person.get_scrobbles(user=other_user)
|
||||||
|
|
||||||
|
assert scrobbles.count() == 2
|
||||||
|
assert scrobble1 in scrobbles
|
||||||
|
assert scrobble2 in scrobbles
|
||||||
|
|
||||||
|
|
||||||
|
@pytest.mark.django_db
|
||||||
|
def test_person_scrobble_unique_together():
|
||||||
|
creator = User.objects.create_user(username="creator", password="testpass123")
|
||||||
|
other_user = User.objects.create_user(username="other", password="testpass123")
|
||||||
|
person = Person.objects.create(name="Jack", created_by=creator, user=creator)
|
||||||
|
|
||||||
|
scrobble = Scrobble.objects.create(
|
||||||
|
user=other_user,
|
||||||
|
log={
|
||||||
|
"with_people_ids": [person.id],
|
||||||
|
"players": [{"person_id": person.id}],
|
||||||
|
},
|
||||||
|
)
|
||||||
|
|
||||||
|
assert PersonScrobble.objects.filter(person=person, scrobble=scrobble).count() == 1
|
||||||
@ -1,7 +1,8 @@
|
|||||||
from django.views import generic
|
from django.views import generic
|
||||||
from django.urls import reverse_lazy
|
from django.urls import reverse_lazy
|
||||||
|
from django.db.models import Count, Q
|
||||||
|
|
||||||
from people.models import Person
|
from people.models import Person, PersonScrobble
|
||||||
|
|
||||||
|
|
||||||
class PersonCreateUpdateView(generic.CreateView):
|
class PersonCreateUpdateView(generic.CreateView):
|
||||||
@ -24,9 +25,16 @@ class PersonCreateUpdateView(generic.CreateView):
|
|||||||
|
|
||||||
def get_context_data(self, **kwargs):
|
def get_context_data(self, **kwargs):
|
||||||
context_data = super().get_context_data(**kwargs)
|
context_data = super().get_context_data(**kwargs)
|
||||||
context_data["people"] = Person.objects.filter(
|
context_data["people"] = (
|
||||||
created_by=self.request.user
|
Person.objects.filter(created_by=self.request.user)
|
||||||
).order_by("-scrobble_count", "name")
|
.annotate(
|
||||||
|
scrobble_count=Count(
|
||||||
|
"scrobble_associations",
|
||||||
|
filter=Q(scrobble_associations__user=self.request.user),
|
||||||
|
)
|
||||||
|
)
|
||||||
|
.order_by("-scrobble_count", "name")
|
||||||
|
)
|
||||||
return context_data
|
return context_data
|
||||||
|
|
||||||
def form_valid(self, form):
|
def form_valid(self, form):
|
||||||
@ -49,7 +57,14 @@ class PersonUpdateView(generic.UpdateView):
|
|||||||
|
|
||||||
def get_context_data(self, **kwargs):
|
def get_context_data(self, **kwargs):
|
||||||
context_data = super().get_context_data(**kwargs)
|
context_data = super().get_context_data(**kwargs)
|
||||||
context_data["people"] = Person.objects.filter(
|
context_data["people"] = (
|
||||||
created_by=self.request.user
|
Person.objects.filter(created_by=self.request.user)
|
||||||
).order_by("-scrobble_count", "name")
|
.annotate(
|
||||||
|
scrobble_count=Count(
|
||||||
|
"scrobble_associations",
|
||||||
|
filter=Q(scrobble_associations__user=self.request.user),
|
||||||
|
)
|
||||||
|
)
|
||||||
|
.order_by("-scrobble_count", "name")
|
||||||
|
)
|
||||||
return context_data
|
return context_data
|
||||||
|
|||||||
Reference in New Issue
Block a user