[sharing] Add bulk scrobble share management
This commit is contained in:
@ -1,6 +1,8 @@
|
||||
from django import forms
|
||||
|
||||
from profiles.models import UserProfile
|
||||
from scrobbles.constants import Visibility
|
||||
from scrobbles.models import Scrobble
|
||||
|
||||
|
||||
class UserProfileForm(forms.ModelForm):
|
||||
@ -45,3 +47,51 @@ class UserProfileForm(forms.ModelForm):
|
||||
"archivebox_password": forms.PasswordInput(render_value=True),
|
||||
"webdav_pass": forms.PasswordInput(render_value=True),
|
||||
}
|
||||
|
||||
|
||||
MEDIA_TYPE_LABELS = {
|
||||
mt.value: mt.label for mt in Scrobble.MediaType
|
||||
}
|
||||
|
||||
INHERIT = ""
|
||||
|
||||
|
||||
class BulkVisibilityForm(forms.Form):
|
||||
bulk_action = forms.ChoiceField(
|
||||
choices=[
|
||||
(Visibility.PUBLIC, "Public"),
|
||||
(Visibility.PRIVATE, "Private"),
|
||||
],
|
||||
widget=forms.RadioSelect,
|
||||
required=False,
|
||||
label="Set all non-shared scrobbles to",
|
||||
)
|
||||
|
||||
def __init__(self, *args, **kwargs):
|
||||
self.profile = kwargs.pop("profile")
|
||||
super().__init__(*args, **kwargs)
|
||||
media_types = Scrobble.MediaType.values
|
||||
choices = [
|
||||
(Visibility.PUBLIC, "Public"),
|
||||
(Visibility.SHARED, "Shared"),
|
||||
(Visibility.PRIVATE, "Private"),
|
||||
]
|
||||
existing_overrides = self.profile.media_type_visibility or {}
|
||||
for mt in sorted(media_types):
|
||||
label = MEDIA_TYPE_LABELS.get(mt, mt)
|
||||
self.fields[f"media_type_{mt}"] = forms.ChoiceField(
|
||||
choices=choices,
|
||||
required=False,
|
||||
label=label,
|
||||
initial=existing_overrides.get(mt, Visibility.PRIVATE),
|
||||
)
|
||||
|
||||
def clean(self):
|
||||
cleaned = super().clean()
|
||||
overrides = {}
|
||||
for mt in Scrobble.MediaType.values:
|
||||
val = cleaned.get(f"media_type_{mt}")
|
||||
if val:
|
||||
overrides[mt] = val
|
||||
cleaned["media_type_visibility"] = overrides
|
||||
return cleaned
|
||||
|
||||
@ -0,0 +1,20 @@
|
||||
from django.db import migrations, models
|
||||
|
||||
|
||||
class Migration(migrations.Migration):
|
||||
|
||||
dependencies = [
|
||||
("profiles", "0037_alter_userprofile_default_scrobble_visibility"),
|
||||
]
|
||||
|
||||
operations = [
|
||||
migrations.AddField(
|
||||
model_name="userprofile",
|
||||
name="media_type_visibility",
|
||||
field=models.JSONField(
|
||||
blank=True,
|
||||
default=dict,
|
||||
help_text='Per-media-type visibility overrides, e.g. {"Video": "public", "Track": "private"}',
|
||||
),
|
||||
),
|
||||
]
|
||||
@ -90,6 +90,12 @@ class UserProfile(TimeStampedModel):
|
||||
default="private",
|
||||
)
|
||||
|
||||
media_type_visibility = models.JSONField(
|
||||
default=dict,
|
||||
blank=True,
|
||||
help_text="Per-media-type visibility overrides, e.g. {\"Video\": \"public\", \"Track\": \"private\"}",
|
||||
)
|
||||
|
||||
home_scrobble_limit = models.IntegerField(default=20)
|
||||
|
||||
weigh_in_units = models.CharField(
|
||||
|
||||
@ -6,4 +6,9 @@ app_name = "profiles"
|
||||
|
||||
urlpatterns = [
|
||||
path("settings/", views.ProfileFormView.as_view(), name="profile_settings"),
|
||||
path(
|
||||
"settings/visibility/",
|
||||
views.BulkVisibilityView.as_view(),
|
||||
name="bulk_visibility",
|
||||
),
|
||||
]
|
||||
|
||||
@ -1,8 +1,12 @@
|
||||
from django.contrib import messages
|
||||
from django.contrib.auth.mixins import LoginRequiredMixin
|
||||
from django.db.models import Count, Q
|
||||
from django.http.response import HttpResponseBadRequest
|
||||
from django.urls import reverse_lazy
|
||||
from django.views.generic import FormView
|
||||
from profiles.forms import UserProfileForm
|
||||
from profiles.forms import BulkVisibilityForm, UserProfileForm
|
||||
from scrobbles.constants import Visibility
|
||||
from scrobbles.models import Scrobble
|
||||
from tasks.todoist import generate_todoist_oauth_url
|
||||
|
||||
|
||||
@ -30,3 +34,46 @@ class ProfileFormView(LoginRequiredMixin, FormView):
|
||||
context["profile"] = self.request.user.profile
|
||||
context["todoist_oauth_url"] = generate_todoist_oauth_url(self.request.user.id)
|
||||
return context
|
||||
|
||||
|
||||
class BulkVisibilityView(LoginRequiredMixin, FormView):
|
||||
template_name = "profiles/visibility_settings.html"
|
||||
form_class = BulkVisibilityForm
|
||||
success_url = reverse_lazy("profiles:bulk_visibility")
|
||||
|
||||
def get_form_kwargs(self):
|
||||
kwargs = super().get_form_kwargs()
|
||||
kwargs["profile"] = self.request.user.profile
|
||||
return kwargs
|
||||
|
||||
def form_valid(self, form):
|
||||
request = self.request
|
||||
profile = request.user.profile
|
||||
|
||||
bulk_action = form.cleaned_data.get("bulk_action")
|
||||
if bulk_action:
|
||||
qs = Scrobble.objects.filter(
|
||||
user=request.user,
|
||||
).exclude(visibility=Visibility.SHARED)
|
||||
total = qs.count()
|
||||
qs.update(visibility=bulk_action)
|
||||
messages.success(
|
||||
request,
|
||||
f"Updated {total} scrobble(s) to {bulk_action}.",
|
||||
)
|
||||
|
||||
profile.media_type_visibility = form.cleaned_data["media_type_visibility"]
|
||||
profile.save(update_fields=["media_type_visibility"])
|
||||
messages.success(request, "Per-media-type visibility overrides saved.")
|
||||
return super().form_valid(form)
|
||||
|
||||
def get_context_data(self, **kwargs):
|
||||
ctx = super().get_context_data(**kwargs)
|
||||
profile = self.request.user.profile
|
||||
qs = Scrobble.objects.filter(user=self.request.user)
|
||||
ctx["scrobble_count"] = qs.count()
|
||||
ctx["visibility_counts"] = qs.values("visibility").annotate(
|
||||
count=Count("id")
|
||||
)
|
||||
ctx["profile"] = profile
|
||||
return ctx
|
||||
|
||||
@ -1613,7 +1613,17 @@ class Scrobble(TimeStampedModel):
|
||||
scrobble_data: dict,
|
||||
) -> "Scrobble":
|
||||
if "visibility" not in scrobble_data:
|
||||
scrobble_data["visibility"] = Visibility.PRIVATE
|
||||
user = scrobble_data.get("user")
|
||||
media_type = scrobble_data.get("media_type")
|
||||
override = None
|
||||
if user and media_type:
|
||||
try:
|
||||
profile = user.profile
|
||||
overrides = profile.media_type_visibility or {}
|
||||
override = overrides.get(media_type)
|
||||
except user.__class__.profile.RelatedObjectDoesNotExist:
|
||||
pass
|
||||
scrobble_data["visibility"] = override or Visibility.PRIVATE
|
||||
scrobble = cls.objects.create(**scrobble_data)
|
||||
if not (
|
||||
scrobble.media_type == cls.MediaType.GEO_LOCATION
|
||||
|
||||
@ -371,6 +371,9 @@ class ScrobbleListView(LoginRequiredMixin, ListView):
|
||||
qs = qs.filter(id__in=matching_ids).distinct()
|
||||
else:
|
||||
tag_list = []
|
||||
visibility_param = self.request.GET.get("visibility", "")
|
||||
if visibility_param in ("public", "shared", "private"):
|
||||
qs = qs.filter(visibility=visibility_param)
|
||||
self.tag_list = tag_list
|
||||
self._full_queryset = qs
|
||||
return qs
|
||||
|
||||
@ -106,6 +106,7 @@
|
||||
{% block title %}Settings{% endblock %}
|
||||
{% block details %}
|
||||
<p class="settings-link"><a href="{% url 'people:person_form' %}">Manage People</a></p>
|
||||
<p class="settings-link"><a href="{% url 'profiles:bulk_visibility' %}">Scrobble Visibility Settings</a></p>
|
||||
<form method="post" class="settings-form">{% csrf_token %}
|
||||
{% for field in form %}
|
||||
{% if field.name == "enable_public_widgets" %}
|
||||
|
||||
117
vrobbler/templates/profiles/visibility_settings.html
Normal file
117
vrobbler/templates/profiles/visibility_settings.html
Normal file
@ -0,0 +1,117 @@
|
||||
{% extends "base_detail.html" %}
|
||||
|
||||
{% block title %}Scrobble Visibility Settings{% endblock %}
|
||||
|
||||
{% block head_extra %}
|
||||
<style>
|
||||
.vis-form {
|
||||
max-width: 700px;
|
||||
}
|
||||
.vis-form h3 {
|
||||
margin-top: 24px;
|
||||
margin-bottom: 12px;
|
||||
}
|
||||
.vis-form h4 {
|
||||
margin-top: 20px;
|
||||
margin-bottom: 8px;
|
||||
font-size: 1.1rem;
|
||||
}
|
||||
.vis-form label {
|
||||
font-weight: 600;
|
||||
margin-right: 12px;
|
||||
}
|
||||
.vis-form .bulk-radio-group {
|
||||
margin: 8px 0 16px 0;
|
||||
}
|
||||
.vis-form .bulk-radio-group label {
|
||||
font-weight: normal;
|
||||
display: block;
|
||||
margin: 4px 0;
|
||||
}
|
||||
.vis-form .media-type-row {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: 12px;
|
||||
margin: 6px 0;
|
||||
padding: 4px 8px;
|
||||
border-radius: 4px;
|
||||
}
|
||||
.vis-form .media-type-row:nth-child(even) {
|
||||
background: #f8f9fa;
|
||||
}
|
||||
.vis-form .media-type-row label {
|
||||
font-weight: normal;
|
||||
min-width: 130px;
|
||||
}
|
||||
.vis-form select {
|
||||
padding: 4px 8px;
|
||||
border: 1px solid #ccc;
|
||||
border-radius: 4px;
|
||||
}
|
||||
.vis-form input[type="submit"] {
|
||||
margin-top: 20px;
|
||||
padding: 10px 24px;
|
||||
background: #007bff;
|
||||
color: white;
|
||||
border: none;
|
||||
border-radius: 4px;
|
||||
cursor: pointer;
|
||||
font-size: 14px;
|
||||
}
|
||||
.vis-form input[type="submit"]:hover {
|
||||
background: #0056b3;
|
||||
}
|
||||
.vis-stats {
|
||||
margin: 16px 0;
|
||||
padding: 12px;
|
||||
background: #f8f9fa;
|
||||
border-radius: 4px;
|
||||
}
|
||||
.vis-stats strong {
|
||||
display: block;
|
||||
margin-bottom: 6px;
|
||||
}
|
||||
.vis-stats ul {
|
||||
margin: 0;
|
||||
padding-left: 18px;
|
||||
}
|
||||
</style>
|
||||
{% endblock %}
|
||||
|
||||
{% block details %}
|
||||
<p><a href="{% url 'profiles:profile_settings' %}">« Back to settings</a></p>
|
||||
|
||||
<div class="vis-stats">
|
||||
<strong>Current scrobble visibility ({{ scrobble_count }} total)</strong>
|
||||
<ul>
|
||||
{% for item in visibility_counts %}
|
||||
<li><a href="{% url 'scrobbles:scrobble-list' %}?visibility={{ item.visibility }}">{{ item.visibility|title }}</a>: {{ item.count }}</li>
|
||||
{% endfor %}
|
||||
</ul>
|
||||
</div>
|
||||
|
||||
<form method="post" class="vis-form">{% csrf_token %}
|
||||
<h3>Bulk Update</h3>
|
||||
<p>Choose a visibility to apply to all scrobbles that are <strong>not</strong> currently "Shared".</p>
|
||||
<div class="bulk-radio-group">
|
||||
{% for radio in form.bulk_action %}
|
||||
<label>{{ radio.tag }} {{ radio.choice_label }}</label>
|
||||
{% endfor %}
|
||||
</div>
|
||||
|
||||
<details>
|
||||
<summary><h3 style="display:inline">Per-Media-Type Defaults</h3></summary>
|
||||
<p>Override default visibility for new scrobbles of specific types.</p>
|
||||
{% for field in form %}
|
||||
{% if field.name != "bulk_action" %}
|
||||
<div class="media-type-row">
|
||||
<label for="{{ field.id_for_label }}">{{ field.label }}</label>
|
||||
{{ field }}
|
||||
</div>
|
||||
{% endif %}
|
||||
{% endfor %}
|
||||
</details>
|
||||
|
||||
<input type="submit" value="Save Visibility Settings">
|
||||
</form>
|
||||
{% endblock %}
|
||||
@ -13,6 +13,9 @@
|
||||
<p class="text-muted small mb-0">Total time: {{ total_time_seconds|natural_duration }}</p>
|
||||
{% endif %}
|
||||
{% endif %}
|
||||
{% if request.GET.visibility %}
|
||||
<h6 class="text-muted">Filter: {{ request.GET.visibility|title }} scrobbles only</h6>
|
||||
{% endif %}
|
||||
</div>
|
||||
</div>
|
||||
|
||||
|
||||
Reference in New Issue
Block a user