Files
cmhub/apps/licensing/admin.py
T

549 lines
17 KiB
Python

from django import forms
from django.contrib import admin, messages
from django.contrib.auth import get_user_model
from django.http import HttpResponseRedirect
from django.shortcuts import get_object_or_404
from django.template.response import TemplateResponse
from django.urls import path, reverse
from .models import (
ClientSubscriptionPolicy,
ClientSubscriptionPolicyAudit,
ClientDevice,
DeviceCredential,
DeviceBindingAudit,
DeviceSession,
LicenseEvent,
LicenseSeat,
LegacyMigrationGrant,
MigrationRequest,
SoftwareEntitlement,
SoftwareOrder,
SoftwarePlan,
)
from .services import (
LicensingError,
grant_software_entitlement,
create_legacy_migration_grant,
get_subscription_mode,
renew_software_entitlement,
revoke_software_entitlement,
set_client_subscription_policy,
)
class HiddenFromAdminIndexMixin:
"""Keep registered models addressable without adding menu noise."""
def get_model_perms(self, request):
return {}
class ClientSubscriptionPolicyAdminForm(forms.ModelForm):
reason = forms.CharField(label="变更原因", widget=forms.Textarea(attrs={"rows": 3}))
class Meta:
model = ClientSubscriptionPolicy
fields = ("mode",)
def clean_mode(self):
mode = self.cleaned_data["mode"]
if not self.instance._state.adding and self.instance.mode == mode:
raise forms.ValidationError("请选择不同的客户端订阅策略。")
return mode
def clean_reason(self):
reason = self.cleaned_data["reason"].strip()
if not reason:
raise forms.ValidationError("必须填写操作原因。")
return reason
class ClientSubscriptionPolicyAuditInline(admin.TabularInline):
model = ClientSubscriptionPolicyAudit
extra = 0
can_delete = False
max_num = 0
ordering = ("-changed_at", "-id")
fields = ("previous_mode", "mode", "reason", "changed_by", "changed_at")
readonly_fields = fields
verbose_name = "策略变更审计"
verbose_name_plural = "策略变更审计"
def has_add_permission(self, request, obj=None):
return False
def has_change_permission(self, request, obj=None):
return False
@admin.register(ClientSubscriptionPolicy)
class ClientSubscriptionPolicyAdmin(admin.ModelAdmin):
form = ClientSubscriptionPolicyAdminForm
inlines = (ClientSubscriptionPolicyAuditInline,)
fields = (
"mode",
"reason",
"server_subscription_mode",
"updated_by",
"created_at",
"updated_at",
)
readonly_fields = (
"server_subscription_mode",
"updated_by",
"created_at",
"updated_at",
)
def has_module_permission(self, request):
return request.user.is_superuser
def has_view_permission(self, request, obj=None):
return request.user.is_superuser
def has_add_permission(self, request):
return request.user.is_superuser and not ClientSubscriptionPolicy.objects.exists()
def has_change_permission(self, request, obj=None):
return request.user.is_superuser
def has_delete_permission(self, request, obj=None):
return False
@admin.display(description="服务端订阅授权模式")
def server_subscription_mode(self, obj):
return get_subscription_mode()
def save_model(self, request, obj, form, change):
policy = set_client_subscription_policy(
mode=form.cleaned_data["mode"],
reason=form.cleaned_data["reason"],
actor=request.user,
)
obj.singleton_id = policy.singleton_id
obj.mode = policy.mode
obj.updated_by = policy.updated_by
obj.created_at = policy.created_at
obj.updated_at = policy.updated_at
def _masked_fingerprint(value: str) -> str:
if not value:
return ""
return f"{value[:8]}...{value[-6:]}"
@admin.register(ClientDevice)
class ClientDeviceAdmin(HiddenFromAdminIndexMixin, admin.ModelAdmin):
list_display = (
"user",
"product_code",
"platform",
"client_version",
"status",
"last_seen_at",
"first_seen_at",
)
list_filter = ("product_code", "platform", "status", "last_seen_at")
search_fields = ("user__username", "user__email")
list_select_related = ("user",)
ordering = ("-last_seen_at", "-id")
readonly_fields = (
"user",
"product_code",
"device_id_version",
"device_fingerprint_masked",
"public_key_fingerprint_masked",
"platform",
"client_version",
"status",
"first_seen_at",
"last_seen_at",
"created_at",
"updated_at",
)
fields = readonly_fields
def has_add_permission(self, request):
return False
def has_delete_permission(self, request, obj=None):
return False
@admin.display(description="设备摘要")
def device_fingerprint_masked(self, obj):
return _masked_fingerprint(obj.device_fingerprint)
@admin.display(description="安装公钥摘要")
def public_key_fingerprint_masked(self, obj):
return _masked_fingerprint(obj.public_key_fingerprint)
@admin.register(DeviceSession)
class DeviceSessionAdmin(HiddenFromAdminIndexMixin, admin.ModelAdmin):
list_display = ("device", "expires_at", "last_used_at", "revoked_at", "created_at")
list_filter = ("revoked_at", "expires_at")
search_fields = ("device__user__username", "device__user__email")
list_select_related = ("device", "device__user")
readonly_fields = (
"device",
"expires_at",
"last_used_at",
"revoked_at",
"created_at",
)
fields = readonly_fields
def has_add_permission(self, request):
return False
def has_change_permission(self, request, obj=None):
return False
def has_delete_permission(self, request, obj=None):
return False
@admin.register(DeviceBindingAudit)
class DeviceBindingAuditAdmin(HiddenFromAdminIndexMixin, admin.ModelAdmin):
list_display = ("user", "device", "api_key", "action", "reason", "created_at")
list_filter = ("action", "created_at")
search_fields = ("user__username", "user__email", "api_key__key_prefix")
list_select_related = ("user", "device", "api_key")
readonly_fields = (
"user",
"device",
"api_key",
"action",
"reason",
"client_version",
"created_at",
)
fields = readonly_fields
def has_add_permission(self, request):
return False
def has_change_permission(self, request, obj=None):
return False
def has_delete_permission(self, request, obj=None):
return False
@admin.register(SoftwarePlan)
class SoftwarePlanAdmin(admin.ModelAdmin):
list_display = (
"name",
"product_code",
"duration_days",
"price",
"device_limit",
"grace_days",
"status",
"updated_at",
)
list_filter = ("product_code", "status")
search_fields = ("name",)
list_editable = ("status",)
readonly_fields = ("created_at", "updated_at")
class EntitlementGrantForm(forms.Form):
user = forms.ModelChoiceField(queryset=get_user_model().objects.all(), label="用户")
plan = forms.ModelChoiceField(
queryset=SoftwarePlan.objects.filter(status=SoftwarePlan.Status.ACTIVE),
label="套餐",
)
reason = forms.CharField(label="授予原因", widget=forms.Textarea(attrs={"rows": 4}))
def clean_reason(self):
reason = self.cleaned_data["reason"].strip()
if not reason:
raise forms.ValidationError("必须填写操作原因。")
return reason
class EntitlementReasonForm(forms.Form):
reason = forms.CharField(label="操作原因", widget=forms.Textarea(attrs={"rows": 4}))
def clean_reason(self):
reason = self.cleaned_data["reason"].strip()
if not reason:
raise forms.ValidationError("必须填写操作原因。")
return reason
@admin.register(SoftwareEntitlement)
class SoftwareEntitlementAdmin(admin.ModelAdmin):
change_form_template = "admin/licensing/softwareentitlement/change_form.html"
list_display = (
"user",
"product_code",
"plan_name",
"plan_device_limit",
"status",
"starts_at",
"expires_at",
"grace_expires_at",
)
list_filter = ("product_code", "status", "expires_at")
search_fields = ("user__username", "user__email", "plan_name")
list_select_related = ("user", "source_plan")
readonly_fields = (
"user",
"product_code",
"source_plan",
"plan_name",
"plan_duration_days",
"plan_price",
"plan_device_limit",
"plan_grace_days",
"status",
"starts_at",
"expires_at",
"grace_expires_at",
"revoked_at",
"created_at",
"updated_at",
)
fields = readonly_fields
def has_add_permission(self, request):
return False
def has_delete_permission(self, request, obj=None):
return False
def get_urls(self):
urls = super().get_urls()
custom_urls = [
path(
"grant/",
self.admin_site.admin_view(self.grant_view),
name="licensing_softwareentitlement_grant",
),
path(
"<path:object_id>/renew/",
self.admin_site.admin_view(self.renew_view),
name="licensing_softwareentitlement_renew",
),
path(
"<path:object_id>/revoke/",
self.admin_site.admin_view(self.revoke_view),
name="licensing_softwareentitlement_revoke",
),
]
return custom_urls + urls
def grant_view(self, request):
form = EntitlementGrantForm(request.POST or None)
if request.method == "POST" and form.is_valid():
try:
entitlement = grant_software_entitlement(
user=form.cleaned_data["user"],
plan=form.cleaned_data["plan"],
reason=form.cleaned_data["reason"],
actor=request.user,
)
except LicensingError as exc:
form.add_error(None, exc.message)
else:
self.message_user(request, "软件权益已授予。", messages.SUCCESS)
return HttpResponseRedirect(self._change_url(entitlement))
return self._operation_response(
request,
title="手工授予软件权益",
form=form,
)
def renew_view(self, request, object_id):
entitlement = get_object_or_404(SoftwareEntitlement, pk=object_id)
return self._entitlement_operation_view(
request,
entitlement=entitlement,
action="renew",
title="续期软件权益",
)
def revoke_view(self, request, object_id):
entitlement = get_object_or_404(SoftwareEntitlement, pk=object_id)
return self._entitlement_operation_view(
request,
entitlement=entitlement,
action="revoke",
title="撤销软件权益",
)
def _entitlement_operation_view(self, request, *, entitlement, action, title):
form = EntitlementReasonForm(request.POST or None)
if request.method == "POST" and form.is_valid():
try:
operation = (
renew_software_entitlement
if action == "renew"
else revoke_software_entitlement
)
operation(
entitlement=entitlement,
reason=form.cleaned_data["reason"],
actor=request.user,
)
except LicensingError as exc:
form.add_error(None, exc.message)
else:
self.message_user(request, f"软件权益已{'续期' if action == 'renew' else '撤销'}。", messages.SUCCESS)
return HttpResponseRedirect(self._change_url(entitlement))
return self._operation_response(
request,
title=title,
form=form,
entitlement=entitlement,
)
def _operation_response(self, request, *, title, form, entitlement=None):
context = {
**self.admin_site.each_context(request),
"title": title,
"opts": self.model._meta,
"form": form,
"entitlement": entitlement,
}
return TemplateResponse(
request,
"admin/licensing/entitlement_operation.html",
context,
)
def _change_url(self, entitlement):
return reverse("admin:licensing_softwareentitlement_change", args=(entitlement.pk,))
class ReadOnlyLicenseAdmin(admin.ModelAdmin):
def get_readonly_fields(self, request, obj=None):
return tuple(field.name for field in self.model._meta.fields)
def has_add_permission(self, request):
return False
def has_change_permission(self, request, obj=None):
return False
def has_delete_permission(self, request, obj=None):
return False
class HiddenLicenseAdmin(HiddenFromAdminIndexMixin, ReadOnlyLicenseAdmin):
pass
@admin.register(LicenseSeat)
class LicenseSeatAdmin(HiddenLicenseAdmin):
list_display = ("entitlement", "seat_number", "device", "bound_at", "released_at")
list_filter = ("entitlement__product_code", "bound_at", "released_at")
search_fields = (
"entitlement__user__username",
"entitlement__user__email",
"device__user__username",
)
list_select_related = ("entitlement", "entitlement__user", "device")
@admin.register(LicenseEvent)
class LicenseEventAdmin(HiddenLicenseAdmin):
list_display = ("entitlement", "action", "seat", "device", "actor", "reason", "created_at")
list_filter = ("action", "entitlement__product_code", "created_at")
search_fields = (
"entitlement__user__username",
"entitlement__user__email",
"reason",
)
list_select_related = ("entitlement", "seat", "device", "actor")
class LegacyMigrationGrantForm(EntitlementGrantForm):
reason = forms.CharField(label="迁移原因", widget=forms.Textarea(attrs={"rows": 4}))
@admin.register(LegacyMigrationGrant)
class LegacyMigrationGrantAdmin(HiddenLicenseAdmin):
list_display = ("user", "product_code", "entitlement", "status", "actor", "created_at")
list_filter = ("product_code", "status", "created_at")
search_fields = ("user__username", "user__email", "reason")
list_select_related = ("user", "entitlement", "actor")
def get_urls(self):
urls = super().get_urls()
return [
path(
"grant/",
self.admin_site.admin_view(self.grant_view),
name="licensing_legacymigrationgrant_grant",
),
] + urls
def grant_view(self, request):
form = LegacyMigrationGrantForm(request.POST or None)
if request.method == "POST" and form.is_valid():
try:
grant = create_legacy_migration_grant(
user=form.cleaned_data["user"],
plan=form.cleaned_data["plan"],
reason=form.cleaned_data["reason"],
actor=request.user,
)
except LicensingError as exc:
form.add_error(None, exc.message)
else:
self.message_user(request, "存量迁移资格已授予。", messages.SUCCESS)
return HttpResponseRedirect(
reverse("admin:licensing_legacymigrationgrant_change", args=(grant.pk,))
)
context = {
**self.admin_site.each_context(request),
"title": "授予存量迁移资格",
"opts": self.model._meta,
"form": form,
}
return TemplateResponse(
request,
"admin/licensing/entitlement_operation.html",
context,
)
@admin.register(MigrationRequest)
class MigrationRequestAdmin(HiddenLicenseAdmin):
list_display = ("request_id", "user", "device", "migration_grant", "status", "expires_at", "confirmed_at")
list_filter = ("status", "device__product_code", "expires_at")
search_fields = ("=request_id", "user__username", "user__email")
list_select_related = ("user", "device", "migration_grant")
@admin.register(DeviceCredential)
class DeviceCredentialAdmin(HiddenLicenseAdmin):
list_display = ("token_prefix", "user", "product_code", "device", "entitlement", "expires_at", "revoked_at")
list_filter = ("product_code", "revoked_at", "expires_at")
search_fields = ("token_prefix", "user__username", "user__email")
list_select_related = ("user", "device", "entitlement", "seat")
@admin.register(SoftwareOrder)
class SoftwareOrderAdmin(HiddenLicenseAdmin):
list_display = (
"created_at",
"order_no",
"user",
"plan_name",
"amount_money",
"pay_method",
"status",
"payment_txn_no",
"entitlement",
"fulfilled_at",
)
list_filter = ("product_code", "pay_method", "status", "created_at")
search_fields = ("=order_no", "=payment_txn_no", "user__username", "user__email")
list_select_related = ("user", "source_plan", "entitlement", "fulfillment_event")