v0.7.1: in-app password change, custom labels, always-on folders
- Settings -> Account: change your own password (current + new), session
preserved via update_session_auth_hash.
- Custom labels: per-user palette in Settings -> Labels; apply/remove from a
message's ... menu (menu stays open, checkmark = applied). Labels are unified
with tags; chip colours sync to the global TagColor. New Label model +
migration 0017; POST /a/{id}/label (session, owner-scoped, palette-validated).
- Sidebar Keep/Deleted folders are now always shown; count badge only when
non-empty. Updates the stale 'hide when empty' note in CLAUDE.md.
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
This commit is contained in:
parent
fcd7da853f
commit
c2a354809a
14 changed files with 562 additions and 23 deletions
|
|
@ -15,8 +15,9 @@ import nh3
|
|||
import openpyxl
|
||||
from django.conf import settings
|
||||
from django.contrib import messages
|
||||
from django.contrib.auth import views as auth_views
|
||||
from django.contrib.auth import update_session_auth_hash, views as auth_views
|
||||
from django.contrib.auth.decorators import login_required
|
||||
from django.contrib.auth.forms import PasswordChangeForm
|
||||
from django.contrib.auth.models import User
|
||||
from django.db import OperationalError, transaction
|
||||
from django.db.models import Count, Max, Q, Sum
|
||||
|
|
@ -36,7 +37,7 @@ from rest_framework.views import APIView
|
|||
|
||||
from .demo_data import create_demo_artifacts
|
||||
from .forms import AgentFeedLoginForm
|
||||
from .models import ApiKey, Artifact, Asset, TagColor
|
||||
from .models import ApiKey, Artifact, Asset, Label, TagColor
|
||||
from .storage import (
|
||||
QuotaExceeded,
|
||||
SIDECAR_SUFFIXES,
|
||||
|
|
@ -625,6 +626,7 @@ def feed(request):
|
|||
"today": today,
|
||||
"title_query": title_query,
|
||||
"kind_choices": Artifact.KIND_CHOICES,
|
||||
"label_palette": request.user.labels.all(),
|
||||
},
|
||||
)
|
||||
|
||||
|
|
@ -639,6 +641,31 @@ def mark_artifact_read(request, artifact_id):
|
|||
return JsonResponse({"id": artifact.id, "read_at": artifact.read_at.isoformat()})
|
||||
|
||||
|
||||
@login_required
|
||||
@require_POST
|
||||
def toggle_artifact_label(request, artifact_id):
|
||||
"""Toggle a palette label on/off for one of the user's own messages. The
|
||||
label name is written into/removed from the artifact's `tags` list (labels
|
||||
are unified with tags). Only names in the user's palette are accepted."""
|
||||
artifact = get_object_or_404(Artifact, pk=artifact_id, owner=request.user, deleted_at__isnull=True)
|
||||
name = (request.POST.get("name") or "").strip()
|
||||
label = Label.objects.filter(user=request.user, name=name).first()
|
||||
if label is None:
|
||||
return JsonResponse({"error": "No such label in your palette."}, status=400)
|
||||
tags = list(artifact.tags)
|
||||
if name in tags:
|
||||
tags.remove(name)
|
||||
applied = False
|
||||
else:
|
||||
tags.append(name)
|
||||
applied = True
|
||||
artifact.tags = tags
|
||||
artifact.save(update_fields=["tags"])
|
||||
# Keep the chip colour in sync with the palette (TagColor is global).
|
||||
TagColor.objects.update_or_create(name=name, defaults={"color": label.color})
|
||||
return JsonResponse({"applied": applied, "name": name, "color": label.color, "tags": tags})
|
||||
|
||||
|
||||
@login_required
|
||||
@require_POST
|
||||
def toggle_artifact_keep(request, artifact_id):
|
||||
|
|
@ -1507,10 +1534,25 @@ def settings_home(request):
|
|||
|
||||
@login_required
|
||||
def settings_account(request):
|
||||
if request.method == "POST":
|
||||
password_form = PasswordChangeForm(request.user, request.POST)
|
||||
if password_form.is_valid():
|
||||
user = password_form.save()
|
||||
# Keep the current session valid — changing the password rotates the
|
||||
# session auth hash, which would otherwise log the user out.
|
||||
update_session_auth_hash(request, user)
|
||||
messages.success(request, "Password changed.")
|
||||
return redirect("settings-account")
|
||||
else:
|
||||
password_form = PasswordChangeForm(request.user)
|
||||
return render(
|
||||
request,
|
||||
"artifacts/settings_account.html",
|
||||
{"active_section": "account", "key_count": request.user.api_keys.count()},
|
||||
{
|
||||
"active_section": "account",
|
||||
"key_count": request.user.api_keys.count(),
|
||||
"password_form": password_form,
|
||||
},
|
||||
)
|
||||
|
||||
|
||||
|
|
@ -1540,6 +1582,49 @@ def settings_api_keys(request):
|
|||
)
|
||||
|
||||
|
||||
LABEL_HEX_RE = re.compile(r"^#[0-9a-fA-F]{6}$")
|
||||
|
||||
|
||||
@login_required
|
||||
def settings_labels(request):
|
||||
if request.method == "POST":
|
||||
action = request.POST.get("action")
|
||||
if action == "create":
|
||||
name = (request.POST.get("name") or "").strip()
|
||||
color = (request.POST.get("color") or "").strip()
|
||||
if not name:
|
||||
messages.error(request, "Give the label a name.")
|
||||
elif not LABEL_HEX_RE.match(color):
|
||||
messages.error(request, "Pick a colour (#RRGGBB).")
|
||||
elif Label.objects.filter(user=request.user, name=name).exists():
|
||||
messages.error(request, f"You already have a label called '{name}'.")
|
||||
else:
|
||||
Label.objects.create(user=request.user, name=name, color=color)
|
||||
TagColor.objects.update_or_create(name=name, defaults={"color": color})
|
||||
messages.success(request, f"Created label '{name}'.")
|
||||
elif action == "update":
|
||||
color = (request.POST.get("color") or "").strip()
|
||||
label = Label.objects.filter(pk=request.POST.get("label_id") or "", user=request.user).first()
|
||||
if label is None:
|
||||
messages.error(request, "No such label.")
|
||||
elif not LABEL_HEX_RE.match(color):
|
||||
messages.error(request, "Pick a colour (#RRGGBB).")
|
||||
else:
|
||||
label.color = color
|
||||
label.save(update_fields=["color"])
|
||||
TagColor.objects.update_or_create(name=label.name, defaults={"color": color})
|
||||
messages.success(request, f"Updated '{label.name}'.")
|
||||
elif action == "delete":
|
||||
deleted, _ = Label.objects.filter(pk=request.POST.get("label_id") or "", user=request.user).delete()
|
||||
messages.success(request, "Label removed from your palette.") if deleted else messages.error(request, "No such label.")
|
||||
return redirect("settings-labels")
|
||||
return render(
|
||||
request,
|
||||
"artifacts/settings_labels.html",
|
||||
{"active_section": "labels", "labels": request.user.labels.all()},
|
||||
)
|
||||
|
||||
|
||||
@login_required
|
||||
def settings_users(request):
|
||||
if not request.user.is_superuser:
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue