"""Base admin classes for models using UUIDv7.""" __package__ = 'archivebox.base_models' import json from django import forms from django.contrib import admin from django.utils.html import format_html, mark_safe from django_object_actions import DjangoObjectActions 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 = None # We render manually class Media: css = { 'all': [] } js = [] def _get_config_options(self): """Get available config options from plugins.""" try: from archivebox.hooks import discover_plugin_configs plugin_configs = discover_plugin_configs() options = {} for plugin_name, schema in plugin_configs.items(): for key, prop in schema.get('properties', {}).items(): options[key] = { 'plugin': plugin_name, 'type': prop.get('type', 'string'), 'default': prop.get('default', ''), 'description': prop.get('description', ''), } return options except Exception: return {} def render(self, name, value, attrs=None, renderer=None): # Parse JSON value to dict if value is None: data = {} elif isinstance(value, str): try: data = json.loads(value) if value else {} except json.JSONDecodeError: data = {} elif isinstance(value, dict): data = value else: data = {} 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'''
{datalist_options}
''' # Render existing key-value pairs row_idx = 0 for key, val in data.items(): val_str = json.dumps(val) if not isinstance(val, str) else val html += self._render_row(widget_id, row_idx, key, val_str) row_idx += 1 # Always add one empty row for new entries html += self._render_row(widget_id, row_idx, '', '') html += f'''
''' return mark_safe(html) def _render_row(self, widget_id, idx, key, value): return f'''
''' def _escape(self, s): """Escape HTML special chars in attribute values.""" if not s: return '' return str(s).replace('&', '&').replace('<', '<').replace('>', '>').replace('"', '"') def value_from_datadict(self, data, files, name): value = data.get(name, '{}') return value class ConfigEditorMixin: """ Mixin for admin classes with a config JSON field. Provides a key-value editor widget with autocomplete for available config keys. """ def formfield_for_dbfield(self, db_field, request, **kwargs): """Use KeyValueWidget for the config JSON field.""" if db_field.name == 'config': kwargs['widget'] = KeyValueWidget() return super().formfield_for_dbfield(db_field, request, **kwargs) class BaseModelAdmin(DjangoObjectActions, admin.ModelAdmin): list_display = ('id', 'created_at', 'created_by') readonly_fields = ('id', 'created_at', 'modified_at') def get_form(self, request, obj=None, **kwargs): form = super().get_form(request, obj, **kwargs) if 'created_by' in form.base_fields: form.base_fields['created_by'].initial = request.user return form