"""Base admin classes for models using UUIDv7.""" __package__ = "archivebox.base_models" import json from collections.abc import Mapping from typing import NotRequired, TypedDict from django import forms from django.contrib import admin from django.db import models from django.forms.renderers import BaseRenderer from django.http import HttpRequest, QueryDict from django.utils.safestring import SafeString, mark_safe from django_object_actions import DjangoObjectActions class ConfigOption(TypedDict): plugin: str type: str | list[str] default: object description: str enum: NotRequired[list[object]] pattern: NotRequired[str] minimum: NotRequired[int | float] maximum: NotRequired[int | float] class KeyValueWidget(forms.Widget): """ A widget that renders JSON dict as editable key-value input fields with + and - buttons to add/remove rows. Includes autocomplete for available config keys from the plugin system. """ template_name = "" # We render manually class Media: css = { "all": [], } js = [] def _get_config_options(self) -> dict[str, ConfigOption]: """Get available config options from plugins.""" try: from archivebox.hooks import discover_plugin_configs plugin_configs = discover_plugin_configs() options: dict[str, ConfigOption] = {} for plugin_name, schema in plugin_configs.items(): for key, prop in schema.get("properties", {}).items(): option: ConfigOption = { "plugin": plugin_name, "type": prop.get("type", "string"), "default": prop.get("default", ""), "description": prop.get("description", ""), } for schema_key in ("enum", "pattern", "minimum", "maximum"): if schema_key in prop: option[schema_key] = prop[schema_key] options[key] = option return options except Exception: return {} def _parse_value(self, value: object) -> dict[str, object]: # Parse JSON value to dict if value is None: return {} if isinstance(value, str): try: parsed = json.loads(value) if value else {} except json.JSONDecodeError: return {} return parsed if isinstance(parsed, dict) else {} if isinstance(value, Mapping): return {str(key): item for key, item in value.items()} return {} def render( self, name: str, value: object, attrs: Mapping[str, str] | None = None, renderer: BaseRenderer | None = None, ) -> SafeString: data = self._parse_value(value) widget_id = attrs.get("id", name) if attrs else name config_options = self._get_config_options() # Build datalist options datalist_options = "\n".join( f'' for key, opt in sorted(config_options.items()) ) # Build config metadata as JSON for JS config_meta_json = json.dumps(config_options) html = f'''