feat: improve operations admin
This commit is contained in:
+11
-1
@@ -29,8 +29,10 @@ class PointsLedgerAdmin(ReadOnlyLedgerAdmin):
|
||||
"ref_call",
|
||||
)
|
||||
list_filter = ("change_type", "created_at")
|
||||
search_fields = ("user__username", "user__email", "reason")
|
||||
search_fields = ("user__username", "user__email", "=ref_order_id", "reason")
|
||||
ordering = ("-created_at", "-id")
|
||||
date_hierarchy = "created_at"
|
||||
list_select_related = ("user", "ref_call")
|
||||
|
||||
|
||||
@admin.register(PricingRule)
|
||||
@@ -47,6 +49,7 @@ class PricingRuleAdmin(admin.ModelAdmin):
|
||||
search_fields = ("alias", "resolution")
|
||||
ordering = ("operation_type", "alias", "resolution")
|
||||
readonly_fields = ("created_at", "updated_at")
|
||||
list_editable = ("points_cost", "is_active")
|
||||
|
||||
@admin.display(description="resolution")
|
||||
def resolution_display(self, obj):
|
||||
@@ -66,6 +69,8 @@ class ExchangeRateAdmin(admin.ModelAdmin):
|
||||
search_fields = ("currency", "note")
|
||||
ordering = ("-effective_from", "-id")
|
||||
readonly_fields = ("created_at", "updated_at")
|
||||
list_editable = ("is_active",)
|
||||
date_hierarchy = "effective_from"
|
||||
|
||||
|
||||
@admin.register(RechargeOrder)
|
||||
@@ -90,6 +95,8 @@ class RechargeOrderAdmin(ReadOnlyLedgerAdmin):
|
||||
"payment_txn_no",
|
||||
)
|
||||
ordering = ("-created_at", "-id")
|
||||
date_hierarchy = "created_at"
|
||||
list_select_related = ("user",)
|
||||
|
||||
|
||||
@admin.register(CallRecord)
|
||||
@@ -112,8 +119,11 @@ class CallRecordAdmin(ReadOnlyLedgerAdmin):
|
||||
"api_key__key_prefix",
|
||||
"alias",
|
||||
"model_used",
|
||||
"prompt",
|
||||
"error_message",
|
||||
"result_ref",
|
||||
"result_summary",
|
||||
)
|
||||
ordering = ("-created_at", "-id")
|
||||
date_hierarchy = "created_at"
|
||||
list_select_related = ("user", "api_key")
|
||||
|
||||
@@ -85,6 +85,19 @@ class InvalidRechargePayMethodError(RechargeOrderCreateError):
|
||||
super().__init__("Unsupported recharge payment method.")
|
||||
|
||||
|
||||
class WalletAdjustmentError(BillingOperationError):
|
||||
code = "invalid_wallet_adjustment"
|
||||
|
||||
|
||||
class WalletAdjustmentWouldOverdrawError(WalletAdjustmentError):
|
||||
code = "adjustment_would_overdraw"
|
||||
|
||||
def __init__(self, *, points_delta: int, available_points: int):
|
||||
self.points_delta = points_delta
|
||||
self.available_points = available_points
|
||||
super().__init__("Wallet adjustment would make the balance negative.")
|
||||
|
||||
|
||||
@dataclass(frozen=True)
|
||||
class CallCharge:
|
||||
call_record: CallRecord
|
||||
@@ -126,12 +139,56 @@ class RechargeResult:
|
||||
applied: bool
|
||||
|
||||
|
||||
@dataclass(frozen=True)
|
||||
class WalletAdjustment:
|
||||
wallet: UserWallet
|
||||
ledger_entry: PointsLedger
|
||||
points_delta: int
|
||||
balance_after: int
|
||||
|
||||
|
||||
def _validate_positive_points(points: int) -> int:
|
||||
if isinstance(points, bool) or not isinstance(points, int) or points <= 0:
|
||||
raise ValueError("points must be a positive integer")
|
||||
return points
|
||||
|
||||
|
||||
def _validate_adjustment_delta(points_delta: int) -> int:
|
||||
if (
|
||||
isinstance(points_delta, bool)
|
||||
or not isinstance(points_delta, int)
|
||||
or points_delta == 0
|
||||
):
|
||||
raise WalletAdjustmentError("points_delta must be a non-zero integer.")
|
||||
return points_delta
|
||||
|
||||
|
||||
def _normalize_adjustment_reason(reason: str) -> str:
|
||||
normalized_reason = str(reason or "").strip()
|
||||
if not normalized_reason:
|
||||
raise WalletAdjustmentError("Adjustment reason is required.")
|
||||
return normalized_reason
|
||||
|
||||
|
||||
def _format_operator(actor) -> str:
|
||||
if actor is None:
|
||||
return ""
|
||||
username_getter = getattr(actor, "get_username", None)
|
||||
username = (
|
||||
username_getter()
|
||||
if callable(username_getter)
|
||||
else getattr(actor, "username", "")
|
||||
)
|
||||
actor_pk = getattr(actor, "pk", None)
|
||||
if username and actor_pk:
|
||||
return f"{username}#{actor_pk}"
|
||||
if username:
|
||||
return str(username)
|
||||
if actor_pk:
|
||||
return f"id#{actor_pk}"
|
||||
return str(actor)
|
||||
|
||||
|
||||
def _locked_wallet_for_user(user) -> UserWallet:
|
||||
wallet, _created = UserWallet.objects.select_for_update().get_or_create(user=user)
|
||||
return wallet
|
||||
@@ -234,6 +291,48 @@ def get_balance_snapshot(user) -> BalanceSnapshot:
|
||||
)
|
||||
|
||||
|
||||
def adjust_wallet_points(
|
||||
*,
|
||||
user,
|
||||
points_delta: int,
|
||||
reason: str,
|
||||
actor=None,
|
||||
) -> WalletAdjustment:
|
||||
points_delta = _validate_adjustment_delta(points_delta)
|
||||
normalized_reason = _normalize_adjustment_reason(reason)
|
||||
operator = _format_operator(actor)
|
||||
ledger_reason = (
|
||||
f"{normalized_reason}\n\noperator={operator}" if operator else normalized_reason
|
||||
)
|
||||
|
||||
with transaction.atomic():
|
||||
wallet = _locked_wallet_for_user(user)
|
||||
balance_after = wallet.points_balance + points_delta
|
||||
if balance_after < 0:
|
||||
raise WalletAdjustmentWouldOverdrawError(
|
||||
points_delta=points_delta,
|
||||
available_points=wallet.points_balance,
|
||||
)
|
||||
|
||||
wallet.points_balance = balance_after
|
||||
wallet.save(update_fields=("points_balance", "updated_at"))
|
||||
|
||||
ledger_entry = PointsLedger.objects.create(
|
||||
user=user,
|
||||
change_type=PointsLedger.ChangeType.ADJUST,
|
||||
points_delta=points_delta,
|
||||
balance_after=wallet.points_balance,
|
||||
reason=ledger_reason,
|
||||
)
|
||||
|
||||
return WalletAdjustment(
|
||||
wallet=wallet,
|
||||
ledger_entry=ledger_entry,
|
||||
points_delta=points_delta,
|
||||
balance_after=ledger_entry.balance_after,
|
||||
)
|
||||
|
||||
|
||||
def apply_recharge_payment(payment: RechargePayment) -> RechargeResult:
|
||||
order_no = str(payment.order_no or "").strip()
|
||||
pay_method = _normalize_pay_method(payment.pay_method)
|
||||
|
||||
@@ -34,6 +34,9 @@ from apps.billing.services import (
|
||||
RechargeAmountMismatchError,
|
||||
RechargeOrderCreateError,
|
||||
RechargePayment,
|
||||
WalletAdjustmentError,
|
||||
WalletAdjustmentWouldOverdrawError,
|
||||
adjust_wallet_points,
|
||||
apply_recharge_payment,
|
||||
create_recharge_order,
|
||||
mark_call_success,
|
||||
@@ -683,6 +686,72 @@ class BillingServiceTests(TestCase):
|
||||
self.assertEqual(order.status, RechargeOrder.Status.PAID)
|
||||
self.assertEqual(order.payment_txn_no, "queried-txn-001")
|
||||
|
||||
def test_adjust_wallet_points_credits_wallet_and_writes_adjust_ledger(self):
|
||||
result = adjust_wallet_points(
|
||||
user=self.user,
|
||||
points_delta=25,
|
||||
reason="运营补偿",
|
||||
)
|
||||
|
||||
self.wallet.refresh_from_db()
|
||||
self.assertEqual(result.points_delta, 25)
|
||||
self.assertEqual(result.balance_after, 125)
|
||||
self.assertEqual(self.wallet.points_balance, 125)
|
||||
|
||||
ledger = PointsLedger.objects.get(
|
||||
user=self.user,
|
||||
change_type=PointsLedger.ChangeType.ADJUST,
|
||||
)
|
||||
self.assertEqual(ledger.points_delta, 25)
|
||||
self.assertEqual(ledger.balance_after, 125)
|
||||
self.assertEqual(ledger.reason, "运营补偿")
|
||||
|
||||
def test_adjust_wallet_points_debits_wallet_without_overdraft(self):
|
||||
result = adjust_wallet_points(
|
||||
user=self.user,
|
||||
points_delta=-40,
|
||||
reason="纠正误充值",
|
||||
)
|
||||
|
||||
self.wallet.refresh_from_db()
|
||||
self.assertEqual(result.balance_after, 60)
|
||||
self.assertEqual(self.wallet.points_balance, 60)
|
||||
self.assertEqual(result.ledger_entry.points_delta, -40)
|
||||
self.assertEqual(result.ledger_entry.reason, "纠正误充值")
|
||||
|
||||
def test_adjust_wallet_points_requires_non_zero_delta_and_reason(self):
|
||||
with self.assertRaises(WalletAdjustmentError):
|
||||
adjust_wallet_points(user=self.user, points_delta=0, reason="无变化")
|
||||
with self.assertRaises(WalletAdjustmentError):
|
||||
adjust_wallet_points(user=self.user, points_delta=10, reason=" ")
|
||||
|
||||
self.wallet.refresh_from_db()
|
||||
self.assertEqual(self.wallet.points_balance, 100)
|
||||
self.assertFalse(
|
||||
PointsLedger.objects.filter(
|
||||
user=self.user,
|
||||
change_type=PointsLedger.ChangeType.ADJUST,
|
||||
).exists()
|
||||
)
|
||||
|
||||
def test_adjust_wallet_points_rejects_overdraft_without_writing_ledger(self):
|
||||
with self.assertRaises(WalletAdjustmentWouldOverdrawError) as context:
|
||||
adjust_wallet_points(
|
||||
user=self.user,
|
||||
points_delta=-101,
|
||||
reason="扣回异常点数",
|
||||
)
|
||||
|
||||
self.assertEqual(context.exception.available_points, 100)
|
||||
self.wallet.refresh_from_db()
|
||||
self.assertEqual(self.wallet.points_balance, 100)
|
||||
self.assertFalse(
|
||||
PointsLedger.objects.filter(
|
||||
user=self.user,
|
||||
change_type=PointsLedger.ChangeType.ADJUST,
|
||||
).exists()
|
||||
)
|
||||
|
||||
|
||||
class ConcurrentDebitTests(TransactionTestCase):
|
||||
def setUp(self):
|
||||
|
||||
+134
-4
@@ -1,9 +1,42 @@
|
||||
from django.contrib import admin
|
||||
from django import forms
|
||||
from django.contrib import admin, messages
|
||||
from django.contrib.admin.utils import quote, unquote
|
||||
from django.contrib.auth.admin import UserAdmin as DjangoUserAdmin
|
||||
from django.core.exceptions import PermissionDenied
|
||||
from django.http import Http404, HttpResponseRedirect
|
||||
from django.template.response import TemplateResponse
|
||||
from django.urls import path, reverse
|
||||
from django.utils.html import format_html
|
||||
|
||||
from apps.billing.services import WalletAdjustmentError, adjust_wallet_points
|
||||
|
||||
from .models import ApiKey, User, UserWallet
|
||||
|
||||
|
||||
class WalletAdjustmentForm(forms.Form):
|
||||
points_delta = forms.IntegerField(
|
||||
label="点数变动",
|
||||
help_text="正数加点,负数扣点;不可为 0。",
|
||||
)
|
||||
reason = forms.CharField(
|
||||
label="调整原因",
|
||||
widget=forms.Textarea(attrs={"rows": 4}),
|
||||
help_text="必填,会写入点数流水用于审计。",
|
||||
)
|
||||
|
||||
def clean_points_delta(self):
|
||||
points_delta = self.cleaned_data["points_delta"]
|
||||
if points_delta == 0:
|
||||
raise forms.ValidationError("点数变动不可为 0。")
|
||||
return points_delta
|
||||
|
||||
def clean_reason(self):
|
||||
reason = self.cleaned_data["reason"].strip()
|
||||
if not reason:
|
||||
raise forms.ValidationError("必须填写调整原因。")
|
||||
return reason
|
||||
|
||||
|
||||
@admin.register(User)
|
||||
class UserAdmin(DjangoUserAdmin):
|
||||
fieldsets = DjangoUserAdmin.fieldsets + (
|
||||
@@ -14,6 +47,7 @@ class UserAdmin(DjangoUserAdmin):
|
||||
"username",
|
||||
"email",
|
||||
"status",
|
||||
"wallet_points_balance",
|
||||
"is_staff",
|
||||
"is_active",
|
||||
"created_at",
|
||||
@@ -21,13 +55,35 @@ class UserAdmin(DjangoUserAdmin):
|
||||
list_filter = DjangoUserAdmin.list_filter + ("status",)
|
||||
search_fields = ("username", "email", "payment_user_id")
|
||||
|
||||
def get_queryset(self, request):
|
||||
return super().get_queryset(request).select_related("wallet")
|
||||
|
||||
@admin.display(description="points")
|
||||
def wallet_points_balance(self, obj):
|
||||
wallet = getattr(obj, "wallet", None)
|
||||
return wallet.points_balance if wallet else 0
|
||||
|
||||
|
||||
@admin.register(UserWallet)
|
||||
class UserWalletAdmin(admin.ModelAdmin):
|
||||
list_display = ("user", "points_balance", "updated_at")
|
||||
search_fields = ("user__username", "user__email")
|
||||
readonly_fields = ("user", "points_balance", "created_at", "updated_at")
|
||||
list_display = ("user", "points_balance", "updated_at", "adjust_points_link")
|
||||
search_fields = ("user__username", "user__email", "user__payment_user_id")
|
||||
readonly_fields = (
|
||||
"user",
|
||||
"points_balance",
|
||||
"created_at",
|
||||
"updated_at",
|
||||
"adjust_points_link",
|
||||
)
|
||||
fields = (
|
||||
"user",
|
||||
"points_balance",
|
||||
"created_at",
|
||||
"updated_at",
|
||||
"adjust_points_link",
|
||||
)
|
||||
ordering = ("user__username",)
|
||||
list_select_related = ("user",)
|
||||
|
||||
def has_add_permission(self, request):
|
||||
return False
|
||||
@@ -35,12 +91,86 @@ class UserWalletAdmin(admin.ModelAdmin):
|
||||
def has_delete_permission(self, request, obj=None):
|
||||
return False
|
||||
|
||||
def get_urls(self):
|
||||
urls = super().get_urls()
|
||||
custom_urls = [
|
||||
path(
|
||||
"<path:object_id>/adjust-points/",
|
||||
self.admin_site.admin_view(self.adjust_points_view),
|
||||
name="users_userwallet_adjust_points",
|
||||
),
|
||||
]
|
||||
return custom_urls + urls
|
||||
|
||||
@admin.display(description="手工调点")
|
||||
def adjust_points_link(self, obj):
|
||||
if obj is None:
|
||||
return ""
|
||||
url = reverse(
|
||||
"admin:users_userwallet_adjust_points",
|
||||
args=(quote(obj.pk),),
|
||||
current_app=self.admin_site.name,
|
||||
)
|
||||
return format_html('<a class="button" href="{}">调点</a>', url)
|
||||
|
||||
def adjust_points_view(self, request, object_id):
|
||||
wallet = self.get_object(request, unquote(object_id))
|
||||
if wallet is None:
|
||||
raise Http404("UserWallet does not exist.")
|
||||
if not self.has_change_permission(request, wallet):
|
||||
raise PermissionDenied
|
||||
|
||||
if request.method == "POST":
|
||||
form = WalletAdjustmentForm(request.POST)
|
||||
if form.is_valid():
|
||||
try:
|
||||
result = adjust_wallet_points(
|
||||
user=wallet.user,
|
||||
points_delta=form.cleaned_data["points_delta"],
|
||||
reason=form.cleaned_data["reason"],
|
||||
actor=request.user,
|
||||
)
|
||||
except WalletAdjustmentError as exc:
|
||||
form.add_error(None, str(exc))
|
||||
else:
|
||||
messages.success(
|
||||
request,
|
||||
(
|
||||
f"已调整 {wallet.user} 点数 {result.points_delta:+d},"
|
||||
f"当前余额 {result.balance_after}。"
|
||||
),
|
||||
)
|
||||
change_url = reverse(
|
||||
"admin:users_userwallet_change",
|
||||
args=(quote(wallet.pk),),
|
||||
current_app=self.admin_site.name,
|
||||
)
|
||||
return HttpResponseRedirect(change_url)
|
||||
else:
|
||||
form = WalletAdjustmentForm()
|
||||
|
||||
context = {
|
||||
**self.admin_site.each_context(request),
|
||||
"title": "手工调整点数",
|
||||
"opts": self.model._meta,
|
||||
"original": wallet,
|
||||
"form": form,
|
||||
"media": self.media + form.media,
|
||||
"has_change_permission": self.has_change_permission(request, wallet),
|
||||
}
|
||||
return TemplateResponse(
|
||||
request,
|
||||
"admin/users/userwallet/adjust_points.html",
|
||||
context,
|
||||
)
|
||||
|
||||
|
||||
@admin.register(ApiKey)
|
||||
class ApiKeyAdmin(admin.ModelAdmin):
|
||||
list_display = ("key_prefix", "user", "name", "status", "last_used_at", "created_at")
|
||||
list_filter = ("status", "created_at")
|
||||
search_fields = ("key_prefix", "user__username", "user__email", "name")
|
||||
list_select_related = ("user",)
|
||||
readonly_fields = (
|
||||
"user",
|
||||
"key_prefix",
|
||||
|
||||
@@ -0,0 +1,58 @@
|
||||
{% extends "admin/base_site.html" %}
|
||||
{% load admin_urls i18n %}
|
||||
|
||||
{% block breadcrumbs %}
|
||||
<div class="breadcrumbs">
|
||||
<a href="{% url 'admin:index' %}">首页</a>
|
||||
› <a href="{% url 'admin:app_list' app_label=opts.app_label %}">{{ opts.app_config.verbose_name }}</a>
|
||||
› <a href="{% url opts|admin_urlname:'changelist' %}">{{ opts.verbose_name_plural|capfirst }}</a>
|
||||
› <a href="{% url opts|admin_urlname:'change' original.pk|admin_urlquote %}">{{ original }}</a>
|
||||
› 手工调整点数
|
||||
</div>
|
||||
{% endblock %}
|
||||
|
||||
{% block content %}
|
||||
<div id="content-main">
|
||||
<form method="post" novalidate>
|
||||
{% csrf_token %}
|
||||
<div>
|
||||
{% if form.non_field_errors %}
|
||||
<p class="errornote">{{ form.non_field_errors }}</p>
|
||||
{% endif %}
|
||||
|
||||
<fieldset class="module aligned">
|
||||
<div class="form-row">
|
||||
<div>
|
||||
<label>用户:</label>
|
||||
<div class="readonly">{{ original.user }}</div>
|
||||
</div>
|
||||
</div>
|
||||
<div class="form-row">
|
||||
<div>
|
||||
<label>当前余额:</label>
|
||||
<div class="readonly">{{ original.points_balance }}</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{% for field in form %}
|
||||
<div class="form-row{% if field.errors %} errors{% endif %}">
|
||||
{{ field.errors }}
|
||||
<div>
|
||||
{{ field.label_tag }}
|
||||
{{ field }}
|
||||
{% if field.help_text %}
|
||||
<div class="help">{{ field.help_text }}</div>
|
||||
{% endif %}
|
||||
</div>
|
||||
</div>
|
||||
{% endfor %}
|
||||
</fieldset>
|
||||
|
||||
<div class="submit-row">
|
||||
<input type="submit" value="保存调整" class="default">
|
||||
<a class="closelink" href="{% url opts|admin_urlname:'change' original.pk|admin_urlquote %}">取消</a>
|
||||
</div>
|
||||
</div>
|
||||
</form>
|
||||
</div>
|
||||
{% endblock %}
|
||||
@@ -3,6 +3,9 @@ from django.contrib.auth import get_user_model
|
||||
from django.test import TestCase
|
||||
from django.urls import reverse
|
||||
|
||||
from apps.billing.models import PointsLedger
|
||||
from apps.users.models import ApiKey, UserWallet
|
||||
|
||||
|
||||
class AdminSmokeTests(TestCase):
|
||||
def test_custom_user_is_registered_in_admin(self):
|
||||
@@ -21,3 +24,96 @@ class AdminSmokeTests(TestCase):
|
||||
|
||||
response = self.client.get(reverse("admin:index"))
|
||||
self.assertEqual(response.status_code, 200)
|
||||
|
||||
|
||||
class UserAdminOperationTests(TestCase):
|
||||
def setUp(self):
|
||||
user_model = get_user_model()
|
||||
self.admin_user = user_model.objects.create_superuser(
|
||||
username="ops-admin",
|
||||
email="ops-admin@example.com",
|
||||
password="test-password",
|
||||
)
|
||||
self.user = user_model.objects.create_user(
|
||||
username="client-user",
|
||||
email="client-user@example.com",
|
||||
password="test-password",
|
||||
)
|
||||
self.wallet = UserWallet.objects.create(user=self.user, points_balance=100)
|
||||
self.client.force_login(self.admin_user)
|
||||
|
||||
def test_wallet_adjust_admin_view_calls_billing_service_and_writes_ledger(self):
|
||||
url = reverse("admin:users_userwallet_adjust_points", args=(self.wallet.pk,))
|
||||
|
||||
get_response = self.client.get(url)
|
||||
self.assertEqual(get_response.status_code, 200)
|
||||
self.assertContains(get_response, "手工调整点数")
|
||||
self.assertContains(get_response, "当前余额")
|
||||
|
||||
response = self.client.post(
|
||||
url,
|
||||
{
|
||||
"points_delta": "30",
|
||||
"reason": "运营补偿",
|
||||
},
|
||||
)
|
||||
|
||||
self.assertEqual(response.status_code, 302)
|
||||
self.wallet.refresh_from_db()
|
||||
self.assertEqual(self.wallet.points_balance, 130)
|
||||
|
||||
ledger = PointsLedger.objects.get(
|
||||
user=self.user,
|
||||
change_type=PointsLedger.ChangeType.ADJUST,
|
||||
)
|
||||
self.assertEqual(ledger.points_delta, 30)
|
||||
self.assertEqual(ledger.balance_after, 130)
|
||||
self.assertIn("运营补偿", ledger.reason)
|
||||
self.assertIn("operator=ops-admin#", ledger.reason)
|
||||
|
||||
def test_wallet_adjust_admin_requires_reason_and_keeps_balance_unchanged(self):
|
||||
url = reverse("admin:users_userwallet_adjust_points", args=(self.wallet.pk,))
|
||||
|
||||
response = self.client.post(
|
||||
url,
|
||||
{
|
||||
"points_delta": "30",
|
||||
"reason": "",
|
||||
},
|
||||
)
|
||||
|
||||
self.assertEqual(response.status_code, 200)
|
||||
self.wallet.refresh_from_db()
|
||||
self.assertEqual(self.wallet.points_balance, 100)
|
||||
self.assertFalse(
|
||||
PointsLedger.objects.filter(
|
||||
user=self.user,
|
||||
change_type=PointsLedger.ChangeType.ADJUST,
|
||||
).exists()
|
||||
)
|
||||
|
||||
def test_wallet_admin_change_form_does_not_directly_edit_balance(self):
|
||||
url = reverse("admin:users_userwallet_change", args=(self.wallet.pk,))
|
||||
|
||||
response = self.client.post(url, {"points_balance": "999"}, follow=True)
|
||||
|
||||
self.assertEqual(response.status_code, 200)
|
||||
self.wallet.refresh_from_db()
|
||||
self.assertEqual(self.wallet.points_balance, 100)
|
||||
self.assertFalse(
|
||||
PointsLedger.objects.filter(
|
||||
user=self.user,
|
||||
change_type=PointsLedger.ChangeType.ADJUST,
|
||||
).exists()
|
||||
)
|
||||
|
||||
def test_api_key_admin_hides_plaintext_and_full_hash(self):
|
||||
api_key, raw_key = ApiKey.create_for_user(self.user, name="desktop")
|
||||
url = reverse("admin:users_apikey_change", args=(api_key.pk,))
|
||||
|
||||
response = self.client.get(url)
|
||||
|
||||
self.assertEqual(response.status_code, 200)
|
||||
self.assertContains(response, api_key.key_prefix)
|
||||
self.assertNotContains(response, raw_key)
|
||||
self.assertNotContains(response, api_key.key_hash)
|
||||
|
||||
Reference in New Issue
Block a user