大量修改UI
This commit is contained in:
@@ -21,6 +21,15 @@ class UserPreferenceSerializer(serializers.ModelSerializer):
|
||||
fields = ["notify", "two_factor_enabled", "creation_defaults", "display", "updated_at"]
|
||||
read_only_fields = ["updated_at"]
|
||||
|
||||
def update(self, instance, validated_data):
|
||||
# JSON 字段按 key 合并,避免消息中心写入的 mute-* 被设置页整包覆盖丢掉
|
||||
for field in ("notify", "creation_defaults", "display"):
|
||||
incoming = validated_data.get(field)
|
||||
current = getattr(instance, field)
|
||||
if isinstance(incoming, dict) and isinstance(current, dict):
|
||||
validated_data[field] = {**current, **incoming}
|
||||
return super().update(instance, validated_data)
|
||||
|
||||
|
||||
class LoginSessionSerializer(serializers.ModelSerializer):
|
||||
is_current = serializers.SerializerMethodField()
|
||||
|
||||
@@ -1,13 +1,15 @@
|
||||
from decimal import Decimal
|
||||
from unittest.mock import patch
|
||||
|
||||
from django.test import TestCase
|
||||
from rest_framework.test import APIClient
|
||||
|
||||
from apps.accounts.models import Team, TeamMember, User
|
||||
from apps.ai.models import AITask, ModelConfig, ModelProvider
|
||||
from apps.assets.models import Asset
|
||||
from apps.billing.models import CreditAccount, CreditLedger
|
||||
from apps.ops.models import Notification
|
||||
from apps.ops.views import ensure_team_notifications
|
||||
from apps.ops.views import NotificationViewSet, ensure_team_notifications
|
||||
from apps.products.models import Product
|
||||
from apps.projects.models import Project
|
||||
|
||||
@@ -84,3 +86,38 @@ class BillingNotificationTests(TestCase):
|
||||
ensure_team_notifications(self.team, self.user)
|
||||
note.refresh_from_db()
|
||||
self.assertEqual(note.cost_label, "¥0.32")
|
||||
|
||||
|
||||
class NotificationMuteFilterTests(TestCase):
|
||||
def setUp(self):
|
||||
self.user = User.objects.create_user(username="owner", password="pass")
|
||||
self.team = Team.objects.create(name="T", owner=self.user)
|
||||
TeamMember.objects.create(team=self.team, user=self.user, role=TeamMember.Role.OWNER)
|
||||
self.client = APIClient()
|
||||
self.client.force_authenticate(self.user)
|
||||
for kind, title in (
|
||||
(Notification.Type.BILLING, "计费 1"),
|
||||
(Notification.Type.BILLING, "计费 2"),
|
||||
(Notification.Type.TASK, "任务 1"),
|
||||
(Notification.Type.SYSTEM, "系统 1"),
|
||||
):
|
||||
Notification.objects.create(
|
||||
team=self.team,
|
||||
recipient=self.user,
|
||||
notification_type=kind,
|
||||
title=title,
|
||||
dedupe_key=f"mute-test:{title}",
|
||||
)
|
||||
|
||||
@patch.object(NotificationViewSet, "_refresh_notifications")
|
||||
def test_exclude_type_hides_muted_category(self, _refresh):
|
||||
res = self.client.get("/api/ops/notifications/", {"exclude_type": "billing", "page_size": 50})
|
||||
self.assertEqual(res.status_code, 200)
|
||||
titles = [row["title"] for row in res.data["results"]]
|
||||
self.assertNotIn("计费 1", titles)
|
||||
self.assertNotIn("计费 2", titles)
|
||||
self.assertIn("任务 1", titles)
|
||||
self.assertIn("系统 1", titles)
|
||||
# chip 计数仍是全量,不被静音筛掉
|
||||
self.assertEqual(res.data["type_counts"]["billing"], 2)
|
||||
self.assertEqual(res.data["type_counts"]["task"], 1)
|
||||
|
||||
@@ -257,6 +257,15 @@ class NotificationViewSet(TeamScopedViewSetMixin, ModelViewSet):
|
||||
user = self.request.user
|
||||
return queryset.filter(Q(recipient=user) | Q(recipient__isnull=True))
|
||||
|
||||
def _excluded_types(self):
|
||||
"""静音同类: ?exclude_type=billing&exclude_type=system 或逗号分隔。非法值丢弃。"""
|
||||
allowed = {choice[0] for choice in Notification.Type.choices}
|
||||
raw = self.request.query_params.getlist("exclude_type")
|
||||
types: list[str] = []
|
||||
for item in raw:
|
||||
types.extend(part.strip() for part in item.split(",") if part.strip())
|
||||
return [item for item in types if item in allowed]
|
||||
|
||||
def get_queryset(self):
|
||||
queryset = self._recipient_scope()
|
||||
notification_type = self.request.query_params.get("type")
|
||||
@@ -264,6 +273,9 @@ class NotificationViewSet(TeamScopedViewSetMixin, ModelViewSet):
|
||||
queryset = queryset.filter(notification_type=notification_type)
|
||||
if self.request.query_params.get("unread") in {"1", "true", "yes"}:
|
||||
queryset = queryset.filter(is_read=False)
|
||||
excluded = self._excluded_types()
|
||||
if excluded:
|
||||
queryset = queryset.exclude(notification_type__in=excluded)
|
||||
return queryset
|
||||
|
||||
def _refresh_notifications(self, request):
|
||||
|
||||
Reference in New Issue
Block a user