Feature/sc 45836/allow admins to delete bad links created by - #3545
Feature/sc 45836/allow admins to delete bad links created by#3545nsantacruz wants to merge 15 commits into
Conversation
The linker admin parse flow re-derived ref parts by parsing a crrd(...) test string in two places (a JS regex in ConnectionsPanel and a Python ast parser in linker_admin). The linkerOutput debug span already carries the ref parts as inputRefParts/inputRefPartTypes, so both parsers were redundant. - Always derive parts from the selected debug span; remove linkerPartsFromCrrd (JS) and the crrd/ast machinery + ENCODED_PART_TYPE_MAP (Python). - Flatten ranged parts (RANGE) on the frontend into NUMBERED + RANGE_SYMBOL + NUMBERED so the server's RawRef._group_ranged_parts reconstructs the range; no server range logic needed. - Drop dead part_type-is-None branch in _raw_ref_from_part_dicts. - Update tests: ranged-ref parse + missing-parts error (replacing crrd test). Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
- Add NON_CTS to the sole remaining partTypeSymbolMap (sefaria.js) so the test string generator no longer emits "?" for it. The two duplicate symbol maps were already removed with the crrd parsers. - Extract the "enter Linker Admin sidebar" URL-param dance into Sefaria.util.setLinkerAdminUrlParams and use it from the three call sites (toggleLinkerDebugMode, openLinkerAdminTools, handleLinkerAdminCitationClick). - Give LinkerAdminAPIView._handle an optional status arg and reuse it in LinkerAdminRerunSegmentView instead of re-inlining the try/except. Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
The same deleted citation can appear in more than one source passed to _merge_deleted_spans (e.g. the existing MUTC spans and the LinkerOutput deleted spans in _replace_existing_chunk). Deleted spans were collected without dedup, so each linker rerun appended duplicate deleted entries that compounded over time. Dedupe by _span_identity before concatenating. Adds a regression test. Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
…ox props - _delete_generated_link no longer re-normalizes its refs in a try/except: pass; the sole caller (set_linker_citation_deleted) already passes normalized refs, and the bare except would have masked a genuinely bad ref. - Drop the unused contentLang and setConnectionsMode props (and the required propType) from LinkerAdminBox; neither was referenced in the component. Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
- LinkerAdminAPIView now uses StaffRequiredMixin (JSON 403) instead of staff_member_required, which redirected non-staff XHR callers to the admin login HTML page. Moved StaffRequiredMixin above its first use so both the linker-admin and linker-editor views share it; dropped the now-unused import. - Exclude the /_api/ prefix from LanguageSettingsMiddleware and ModuleMiddleware (as /api/ already is), so internal JSON endpoints skip language/module resolution. No existing /_api/ view reads interfaceLang/contentLang/module. - Move the linker-admin routes and their frontend callers from /api/ to /_api/, consistent with the sibling _api/linker-editor endpoints. Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
|
I'll analyze this PR systematically, reviewing the code quality, architecture, implementation, and test coverage. PR SummaryThis PR implements a Linker Editor — a staff-only tool for editing linker metadata (MatchTemplates, AddressTypes, NonUniqueTerms) on schema nodes, plus a LinkerAdmin sidebar panel for debugging and managing linker citations in the reader. Key components:
Issues Found🔴 Potential Bugs1. # sefaria/helper/linker_admin.py:130
else:
rerun_linker_for_segment(normalized, user_id)
2. # sefaria/helper/linker_admin.py:207
ref_resolver._ibid_history.last_refs = Ref(prev_tref)This directly mutates a private attribute ( 3. # sefaria/helper/linker/tasks.py:797-800
linker_output_deleted_spans = _mutc_deleted_spans_from_linker_output(chunk.ref, chunk.versionTitle, chunk.language)
if existing:
chunk.spans = _merge_deleted_spans(chunk.spans, existing.spans + linker_output_deleted_spans)
4. useEffect(() => {
if (initialTestString) {
setTestString(initialTestString);
setSelectedSpan(selectedCitationData?.linkerAdminSpan || getSelectedLinkerAdminSpan(initialTestString));
parseCitation(initialTestString);
}
}, [initialTestString]);
5. def add_usage_entry(entry: dict) -> None:
for slug in entry["term_slugs"]:
entries = get_term_usages(slug)
if _entry_identity(entry) not in {_entry_identity(e) for e in entries}:
entries.append(entry)
set_term_usages(slug, entries)This is a read-modify-write pattern on Redis without any locking. In a multi-server environment, concurrent calls could result in lost updates. For a staff-only tool this is low risk, but worth noting. 6. def remove_incorrect_matches(resolved_refs: List[ResolvedRef]) -> List[ResolvedRef]:
ResolvedRefPruner.annotate_disqualified_matches(resolved_refs)
temp_resolved_refs = list(filter(ResolvedRefPruner.is_match_correct, resolved_refs))
if len(temp_resolved_refs) == 0:
temp_resolved_refs = ResolvedRefPruner._merge_subset_matches(resolved_refs)
ResolvedRefPruner.annotate_disqualified_matches(temp_resolved_refs) # second callThe second annotation call is on 7. rebuildLinker: () => Sefaria.apiRequestWithBody('/admin/reset/linker', {}, {}, 'POST'),The 🟡 Code Quality Issues8. else:
rerun_linker_for_segment(normalized, user_id)The return value (async result with task_id) is discarded. The caller gets no indication that a rerun was queued. This is intentional but undocumented. 9. if (!confirm(Sefaria._('Delete this MatchTemplate?'))) { return; }
// ...
alert(Sefaria._('Linker rebuilt.'));
alert(`Completed linker rerun: ${rerunRef}`);Using 10. {shouldDisplaySuggestions(isOpen) && renderItems(suggestions, highlightedIndex, getItemProps, wrappedGetInputProps)}
11. @staff_member_required
def rebuild_linker(request):Compare with 12. except Exception as e: # noqa - one bad index shouldn't abort the whole rebuildWhile the comment explains the intent, swallowing all exceptions means corrupted indexes could go unnoticed. Consider at minimum tracking a failure count and returning it alongside the success count. 13. def recurse(cls):
for sub in cls.__subclasses__():
names.append(sub.__name__[len("Address"):])
recurse(sub)
recurse(AddressType)This relies on 🟢 Positive Observations
Test Coverage AssessmentNew production code: ~700 lines (Python helpers + views) Coverage is approximately 40-45% of new Python code, below the 70% threshold. Key gaps:
Suggestions:
Code Quality ScoreScope (S): 18/20
Architecture (A): 16/20
Implementation (I): 16/20
Risk (R): 12/20
Quality (Q): 10/15
Performance/Security (P): 3/5
Base Score: 18 + 16 + 16 + 12 + 10 + 3 = 75 Effort Scale: 2463 effective lines → Extra Large tier (ESF: 1.0x), 31 files → Extra Large tier. No bump needed. Final Score: 75 × 1.0 = 75 Code Quality Data (JSON){
"_schema": "code_quality_v5",
"total_score": 75,
"total_factors": "75 × 1.0 (Extra Large ESF) = 75",
"scope_score": 18,
"scope_factors": "31 files across frontend/backend/model/routing/CSS/templates; new /linker-editor page, LinkerAdmin sidebar mode, _api/ URL namespace, management command, Redis-backed index module; cross-cutting reader, connections panel, text segment, schema model, linker pipeline",
"architecture_score": 16,
"architecture_factors": "New _api/ URL namespace for staff-only endpoints; nonuniqueterm_index.py introduces Redis-backed reverse index pattern; StaffRequiredMixin reusable CBV; linker_admin/linker_editor cleanly separate from views; debug pruning path cleanly separated from production; minor coupling via global Sefaria._linkerOutputMap",
"implementation_score": 16,
"implementation_factors": "Redis-backed reverse index with rebuild/surgical-update duality; is_match_correct refactored to get_disqualification_reason with reason propagation; _merge_deleted_spans with identity-based deduplication; 710-line recursive tree component with expand/collapse and alt-struct support; parse_linker_citation reconstructs RawRef from parts with span alignment; linkerPartsFromSpan range flattening mirrors server-side reconstruction",
"risk_score": 12,
"risk_factors": "Staff-only tool limits blast radius; _ibid_history.last_refs direct private API mutation; annotate_disqualified_matches side-effects on ResolvedRef in production path; extra DB query per linker run (_mutc_deleted_spans_from_linker_output); Redis race condition in add_usage_entry; no feature flag for tasks.py changes affecting all linker runs",
"quality_score": 10,
"quality_factors": "Good test separation (pure logic/DB/Redis); covers key edge cases (deduplication, deleted spans, range refs); missing coverage for rebuild(), view auth enforcement, _update_deleted_marker; linker_ |
This pull request introduces a comprehensive set of features and improvements to support advanced Linker admin and editor workflows, including new API endpoints, a Django management command, and backend logic for handling deleted citations and schema editing. The most important changes are grouped below.
New Linker Admin & Editor API Endpoints
api/views.pyfor staff-only Linker admin actions (delete/recreate/parse citations, rerun segment, and editor endpoints for match templates, address types, and non-unique terms), with robust JSON body handling and error responses. [1] [2]StaffRequiredMixinto enforce staff-only access for these endpoints.Backend Logic for Deleted Citations
sefaria/helper/linker/tasks.pyby tracking and merging deleted citation spans across reruns, ensuring deduplication and correct propagation of deleted state. [1] [2] [3] [4]Management Command for NonUniqueTerm Index
build_nonuniqueterm_indexto rebuild the NonUniqueTerm-to-MatchTemplate usage index, ensuring editor dropdowns and search features are accurate after deploys.Reader UI Integration
"LinkerAdmin"and added a staff-only view for the Linker Editor in the reader UI, enabling access to the new editor interface. [1] [2]Supporting Imports
linker_adminandlinker_editorhelpers inapi/views.pyto support the new API endpoints.