From 50dffe688b4011d83b2125929201ed6f92c502d0 Mon Sep 17 00:00:00 2001 From: Robert Helewka Date: Fri, 22 May 2026 23:54:10 -0400 Subject: [PATCH] feat(library): register IngestJob admin and link Neo4j views - Add read-only ModelAdmin for IngestJob with filters, search, and date hierarchy for operational visibility - Inject proxy entries into the admin index for Neo4j-backed entities (Libraries, Concepts, Search, Embedding pipeline) that link to existing CRUD views in library/views.py - Makes library content discoverable from /admin/ without pretending neomodel StructuredNodes are Django ORM models --- mnemosyne/library/admin.py | 128 +++++++++++++++++++++++++++++++++++-- 1 file changed, 123 insertions(+), 5 deletions(-) diff --git a/mnemosyne/library/admin.py b/mnemosyne/library/admin.py index f6d882c..aa7b564 100644 --- a/mnemosyne/library/admin.py +++ b/mnemosyne/library/admin.py @@ -1,5 +1,123 @@ -# Library app does not use standard Django admin (neomodel StructuredNodes -# are not Django ORM models). Custom admin views are provided as regular -# app views in library/views.py, rendered within Themis's template structure. -# -# The embedding pipeline dashboard is at /library/embedding/ +"""Admin registrations for the library app. + +Most library content (Library, Collection, Item, Chunk, Concept, Image, +ImageEmbedding) lives in Neo4j via neomodel and cannot use Django's +standard ModelAdmin. Those entities have full CRUD pages in +``library/views.py``; this module wires proxy rows into the admin index +so they are discoverable from ``/admin/``. + +``IngestJob`` is a regular Django ORM model and gets a normal, +read-only ModelAdmin. +""" + +from django.contrib import admin +from django.urls import NoReverseMatch, reverse + +from .models import IngestJob + + +@admin.register(IngestJob) +class IngestJobAdmin(admin.ModelAdmin): + list_display = ( + "id", + "status", + "library_uid", + "item_uid", + "source", + "source_ref", + "progress", + "retry_count", + "created_at", + "completed_at", + ) + list_filter = ("status", "source", "created_at") + search_fields = ( + "id", + "library_uid", + "item_uid", + "source_ref", + "content_hash", + "title", + "celery_task_id", + ) + date_hierarchy = "created_at" + ordering = ("-created_at",) + readonly_fields = ( + "id", + "item_uid", + "library_uid", + "celery_task_id", + "status", + "progress", + "error", + "retry_count", + "chunks_created", + "concepts_extracted", + "embedding_model", + "content_hash", + "source", + "source_ref", + "s3_key", + "title", + "file_type", + "file_size", + "collection_uid", + "created_at", + "started_at", + "completed_at", + ) + + def has_add_permission(self, request): + return False + + def has_change_permission(self, request, obj=None): + # View-only; superusers can still open the detail page. + return request.method in ("GET", "HEAD") + + +# --------------------------------------------------------------------------- +# Neo4j-resident entities: proxy entries on the admin index that link to the +# existing CRUD views in library/views.py. No shim models — just an honest +# signpost so /admin/ doesn't pretend the library is empty. +# --------------------------------------------------------------------------- + +_NEO4J_ADMIN_LINKS = [ + ("Libraries", "library:library-list"), + ("Concepts", "library:concept-list"), + ("Search", "library:search"), + ("Embedding pipeline", "library:embedding-dashboard"), +] + + +_original_get_app_list = admin.site.get_app_list + + +def _get_app_list_with_library_links(request, app_label=None): + app_list = _original_get_app_list(request, app_label) + for app in app_list: + if app["app_label"] != "library": + continue + for name, url_name in _NEO4J_ADMIN_LINKS: + try: + url = reverse(url_name) + except NoReverseMatch: + continue + app["models"].append( + { + "name": name, + "object_name": name, + "perms": { + "add": False, + "change": True, + "delete": False, + "view": True, + }, + "admin_url": url, + "add_url": None, + "view_only": True, + } + ) + return app_list + + +admin.site.get_app_list = _get_app_list_with_library_links