from django.contrib import admin, messages from django.forms import ModelForm, ValidationError as FormValidationError from django.utils.html import format_html from links.models import Click, ShortLink from links.services import ValidationError, generate_code, validate_target_url admin.site.site_header = "URL shortener" admin.site.site_title = "Shortener admin" admin.site.index_title = "Links and clicks" class ShortLinkAdminForm(ModelForm): class Meta: model = ShortLink fields = "__all__" def __init__(self, *args, **kwargs): super().__init__(*args, **kwargs) if "code" in self.fields: self.fields["code"].required = False if "created_by_token" in self.fields: self.fields["created_by_token"].required = False def clean_target_url(self): raw = self.cleaned_data.get("target_url") or "" try: return validate_target_url(raw) except ValidationError as exc: raise FormValidationError(str(exc)) from exc def clean(self): cleaned = super().clean() if not self.instance.pk and not cleaned.get("code"): cleaned["code"] = generate_code() if not cleaned.get("created_by_token"): cleaned["created_by_token"] = "admin" return cleaned class ClickInline(admin.TabularInline): model = Click extra = 0 can_delete = False show_change_link = True max_num = 0 readonly_fields = ("occurred_at", "ip_hash", "user_agent", "referrer") fields = ("occurred_at", "ip_hash", "user_agent", "referrer") def has_add_permission(self, request, obj=None): return False @admin.register(ShortLink) class ShortLinkAdmin(admin.ModelAdmin): form = ShortLinkAdminForm list_display = ( "code", "short_url_display", "target_url", "title", "is_active", "click_count", "created_by_token", "expires_at", "created_at", ) list_filter = ("is_active", "created_by_token", "created_at") search_fields = ("code", "target_url", "external_ref", "title") date_hierarchy = "created_at" ordering = ("-created_at",) list_per_page = 50 actions = ("disable_links",) inlines = (ClickInline,) readonly_fields = ( "id", "code", "created_by_token", "click_count", "created_at", "updated_at", "short_url_display", ) fieldsets = ( ( None, { "fields": ( "code", "short_url_display", "target_url", "title", "external_ref", "is_active", "expires_at", ) }, ), ( "Attribution", {"fields": ("created_by_token", "click_count", "created_at", "updated_at", "id")}, ), ) def get_readonly_fields(self, request, obj=None): if obj: return self.readonly_fields + ("target_url",) return self.readonly_fields @admin.display(description="Short URL") def short_url_display(self, obj: ShortLink) -> str: if not obj.code: return "—" return format_html('{0}', obj.public_short_url) def save_model(self, request, obj, form, change): if not change: if not obj.code: obj.code = generate_code() if not obj.created_by_token: obj.created_by_token = ( request.user.get_username() if request.user.is_authenticated else "admin" ) super().save_model(request, obj, form, change) @admin.action(description="Disable selected links") def disable_links(self, request, queryset): updated = queryset.filter(is_active=True).update(is_active=False) self.message_user( request, f"Disabled {updated} link(s).", messages.SUCCESS if updated else messages.INFO, ) @admin.register(Click) class ClickAdmin(admin.ModelAdmin): list_display = ("link", "occurred_at", "ip_hash_short", "user_agent_short") list_filter = ("occurred_at",) search_fields = ("link__code", "ip_hash", "user_agent", "referrer") date_hierarchy = "occurred_at" readonly_fields = ("id", "link", "occurred_at", "ip_hash", "user_agent", "referrer") ordering = ("-occurred_at",) def has_add_permission(self, request): return False def has_change_permission(self, request, obj=None): return False @admin.display(description="IP hash") def ip_hash_short(self, obj: Click) -> str: return (obj.ip_hash[:12] + "…") if obj.ip_hash else "—" @admin.display(description="User agent") def user_agent_short(self, obj: Click) -> str: ua = obj.user_agent or "" return (ua[:48] + "…") if len(ua) > 48 else (ua or "—")