Skip to content
Draft
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
4 changes: 4 additions & 0 deletions CHANGES.md
Original file line number Diff line number Diff line change
Expand Up @@ -2,6 +2,10 @@

## 1.0.0b49

- Show metadata columns in the object information page.

## 1.0.0b49

### Fixed

- Gracefully handle missing `meta` column in `_load_idx_batch()` (#105).
Expand Down
18 changes: 17 additions & 1 deletion src/plone/pgcatalog/catalog.py
Original file line number Diff line number Diff line change
Expand Up @@ -1323,6 +1323,7 @@ def manage_get_object_detail(self, zoid):

Returns idx_items as a pre-sorted list of {"key", "value"} dicts
so the DTML template doesn't need isinstance/sorted (restricted).
Also separates metadata columns from index columns for display.
"""
zoid = int(zoid)
conn = self._get_pg_read_connection()
Expand All @@ -1337,8 +1338,14 @@ def manage_get_object_detail(self, zoid):
row = cur.fetchone()
if row is None:
return None

idx = row["idx"] or {}
registry = get_registry()

# Separate metadata from index data
metadata_items = []
idx_items = []

for k in sorted(idx):
v = idx[k]
if isinstance(v, list):
Expand All @@ -1349,10 +1356,19 @@ def manage_get_object_detail(self, zoid):
display = "True" if v else "False"
else:
display = str(v)
idx_items.append({"key": k, "value": display, "is_none": v is None})

item = {"key": k, "value": display, "is_none": v is None}

# Check if this key is a metadata column
if k in registry.metadata:
metadata_items.append(item)
else:
idx_items.append(item)

return {
"path": row["path"],
"idx_items": idx_items,
"metadata_items": metadata_items,
"has_searchable_text": row["has_searchable_text"],
"searchable_text_preview": row["searchable_text_preview"] or "",
}
Expand Down
44 changes: 44 additions & 0 deletions src/plone/pgcatalog/www/catalogObjectInformation.dtml
Original file line number Diff line number Diff line change
Expand Up @@ -3,6 +3,8 @@

<main class="container-fluid">

<dtml-if "REQUEST.get('zoid')">

<dtml-let detail="manage_get_object_detail(REQUEST['zoid'])">

<dtml-if "detail is None">
Expand Down Expand Up @@ -47,6 +49,39 @@
</div>
</dtml-if>

<dtml-if "detail['metadata_items']">
<div class="card mb-3">
<div class="card-header d-flex justify-content-between align-items-center">
<strong>Metadata Columns</strong> <small class="text-muted ml-1">(lightweight brain data)</small>
<span class="badge badge-secondary"><dtml-var "len(detail['metadata_items'])"> columns</span>
</div>
<table class="table table-striped table-hover table-sm mb-0">
<thead class="thead-light">
<tr>
<th scope="col" style="width:220px">Column</th>
<th scope="col">Value</th>
</tr>
</thead>
<tbody>
<dtml-in "detail['metadata_items']">
<dtml-let item="_['sequence-item']">
<tr>
<td><strong><dtml-var "item['key']" html_quote></strong></td>
<td style="word-break: break-all;">
<dtml-if "item['is_none']">
<span class="text-muted"><em>None</em></span>
<dtml-else>
<dtml-var "item['value']" html_quote>
</dtml-if>
</td>
</tr>
</dtml-let>
</dtml-in>
</tbody>
</table>
</div>
</dtml-if>

<div class="card mb-3">
<div class="card-header d-flex justify-content-between align-items-center">
<strong>Index Data</strong> <small class="text-muted ml-1">(idx JSONB)</small>
Expand Down Expand Up @@ -82,6 +117,15 @@

</dtml-let>

<dtml-else>

<div class="alert alert-danger mt-3">
<strong>Error:</strong> No object ID (zoid) specified.
Please access this page from the <a href="manage_catalogView" class="alert-link">catalog object list</a>.
</div>

</dtml-if>

<p>
<a href="manage_catalogView" class="btn btn-sm btn-outline-secondary">&laquo; Back to Catalog</a>
</p>
Expand Down
54 changes: 47 additions & 7 deletions tests/test_zmi_helpers.py
Original file line number Diff line number Diff line change
Expand Up @@ -223,7 +223,7 @@ def test_batch_start_passed_as_offset(self, catalog_tool):
class TestManageGetObjectDetail:
"""Tests for manage_get_object_detail()."""

def test_returns_sorted_idx_items(self, catalog_tool):
def test_returns_sorted_idx_items(self, catalog_tool, _mock_registry):
row = {
"path": "/Plone/doc",
"idx": {"Title": "Hello", "portal_type": "Document", "UID": "abc-123"},
Expand All @@ -238,9 +238,11 @@ def test_returns_sorted_idx_items(self, catalog_tool):
result = catalog_tool.manage_get_object_detail(zoid=42)
assert result["path"] == "/Plone/doc"
keys = [item["key"] for item in result["idx_items"]]
assert keys == ["Title", "UID", "portal_type"]
assert keys == ["UID"]
metadata_keys = [item["key"] for item in result["metadata_items"]]
assert metadata_keys == ["Title", "portal_type"]

def test_none_value_marked(self, catalog_tool):
def test_none_value_marked(self, catalog_tool, _mock_registry):
row = {
"path": "/x",
"idx": {"review_state": None},
Expand All @@ -257,7 +259,7 @@ def test_none_value_marked(self, catalog_tool):
assert item["is_none"] is True
assert item["value"] == ""

def test_list_value_joined(self, catalog_tool):
def test_list_value_joined(self, catalog_tool, _mock_registry):
row = {
"path": "/x",
"idx": {"Subject": ["python", "zope"]},
Expand All @@ -273,7 +275,8 @@ def test_list_value_joined(self, catalog_tool):
item = result["idx_items"][0]
assert item["value"] == "python, zope"

def test_bool_value_display(self, catalog_tool):
def test_bool_value_display(self, catalog_tool, _mock_registry):
# Test True value displays as "true"
row = {
"path": "/x",
"idx": {"is_folderish": True},
Expand All @@ -288,7 +291,7 @@ def test_bool_value_display(self, catalog_tool):
result = catalog_tool.manage_get_object_detail(zoid=1)
assert result["idx_items"][0]["value"] == "True"

def test_not_found_returns_none(self, catalog_tool):
def test_not_found_returns_none(self, catalog_tool, _mock_registry):
mock_conn = mock.MagicMock()
mock_conn.cursor().__enter__().fetchone.return_value = None
with mock.patch.object(
Expand All @@ -297,7 +300,7 @@ def test_not_found_returns_none(self, catalog_tool):
result = catalog_tool.manage_get_object_detail(zoid=999)
assert result is None

def test_empty_idx_returns_empty_items(self, catalog_tool):
def test_empty_idx_returns_empty_items(self, catalog_tool, _mock_registry):
row = {
"path": "/x",
"idx": None,
Expand All @@ -311,3 +314,40 @@ def test_empty_idx_returns_empty_items(self, catalog_tool):
):
result = catalog_tool.manage_get_object_detail(zoid=1)
assert result["idx_items"] == []
assert result["metadata_items"] == []

def test_separates_metadata_from_index_data(self, catalog_tool, _mock_registry):
"""Test that metadata columns are separated from index data."""
row = {
"path": "/Plone/doc",
"idx": {
"Title": "My Document", # metadata + index
"Description": "A test doc", # metadata only
"portal_type": "Document", # metadata + index
"UID": "abc-123", # index only
"allowedRolesAndUsers": ["Anonymous"], # index only
},
"has_searchable_text": True,
"searchable_text_preview": "My Document",
}
mock_conn = mock.MagicMock()
mock_conn.cursor().__enter__().fetchone.return_value = row
with mock.patch.object(
catalog_tool, "_get_pg_read_connection", return_value=mock_conn
):
result = catalog_tool.manage_get_object_detail(zoid=42)

# Check metadata items are separated correctly
metadata_keys = [item["key"] for item in result["metadata_items"]]
assert set(metadata_keys) == {"Title", "Description", "portal_type"}

# Check index-only items
idx_keys = [item["key"] for item in result["idx_items"]]
assert set(idx_keys) == {"UID", "allowedRolesAndUsers"}

# Verify values are preserved correctly
title_item = next(
item for item in result["metadata_items"] if item["key"] == "Title"
)
assert title_item["value"] == "My Document"
assert title_item["is_none"] is False
Loading