Skip to content

Commit 99e95d3

Browse files
authored
Merge pull request #71 from kitconcept/63-vocab-support
Add vocabulary support for facet conditions (#63)
2 parents 08f2420 + 4db7c10 commit 99e95d3

32 files changed

Lines changed: 1621 additions & 138 deletions

CLAUDE.md

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,3 @@
1+
# Instructions for using kitconcept.solr
2+
3+
The file `./markdown-docs/INSTRUCTIONS.md` contains instructions for using kitconcept.solr in selected use cases. Refer to this file for more details.

backend/news/63.bugfix

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1 @@
1+
Fix test_services_navigation.py which used the wrong layer and corrupted ZODB state @reebalazs

backend/news/63.feature

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1 @@
1+
Add vocabulary support for facet conditions. @reebalazs

backend/src/kitconcept/solr/interfaces.py

Lines changed: 20 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -17,7 +17,26 @@ class IKitconceptSolrLayer(IDefaultBrowserLayer):
1717
"properties": {
1818
"fieldList": {
1919
"type": "array",
20-
"items": {"type": "string"},
20+
"items": {
21+
"oneOf": [
22+
{"type": "string"},
23+
{
24+
"type": "object",
25+
"properties": {
26+
"name": {"type": "string"},
27+
"vocabulary": {
28+
"type": "object",
29+
"properties": {
30+
"name": {"type": "string"},
31+
"isMultilingual": {"type": "boolean"},
32+
},
33+
"required": ["name"],
34+
},
35+
},
36+
"required": ["name"],
37+
},
38+
],
39+
},
2140
},
2241
"searchTabs": {
2342
"type": "array",

backend/src/kitconcept/solr/profiles/default/metadata.xml

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,6 @@
11
<?xml version="1.0" encoding="utf-8"?>
22
<metadata>
3-
<version>1002</version>
3+
<version>1003</version>
44
<dependencies>
55
<dependency>profile-collective.solr:default</dependency>
66
</dependencies>

backend/src/kitconcept/solr/services/solr.py

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -253,6 +253,7 @@ def enhance_result(
253253
result.get("response", {}).get("docs", []),
254254
result.get("highlighting", {}),
255255
)
256+
result["vocabularies"] = solr_config.vocabularies
256257
# Solr response is pruned of the unnecessary parts, unless explicitly requested.
257258
if not keep_full_solr_response:
258259
result.pop("facet_counts", None)

backend/src/kitconcept/solr/services/solr_utils.py

Lines changed: 19 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -90,16 +90,33 @@ def select_condition(self, group_select: int) -> str:
9090
condition = filters[group_select]
9191
return f"{base_query}{condition}"
9292

93+
@staticmethod
94+
def _field_name(item) -> str:
95+
"""Extract the field name from a fieldList item (string or dict)."""
96+
if isinstance(item, dict):
97+
return item["name"]
98+
return item
99+
93100
@property
94101
def field_list(self) -> str:
95102
raw_value = self.config.get("fieldList", [])
96-
invalid_fields = [item for item in raw_value if "," in item]
103+
names = [self._field_name(item) for item in raw_value]
104+
invalid_fields = [name for name in names if "," in name]
97105
if invalid_fields:
98106
raise SolrConfigError(
99107
"Error parsing solr config, fieldList item contains comma (,) "
100108
"which is prohibited"
101109
)
102-
return ",".join(raw_value)
110+
return ",".join(names)
111+
112+
@property
113+
def vocabularies(self) -> list:
114+
raw_value = self.config.get("fieldList", [])
115+
return [
116+
{"field": item["name"], **item["vocabulary"]}
117+
for item in raw_value
118+
if isinstance(item, dict) and "vocabulary" in item
119+
]
103120

104121
def select_layouts(self, group_select: int) -> list:
105122
return self.listOflayouts[group_select]

backend/src/kitconcept/solr/upgrades/__init__.py

Lines changed: 8 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,7 @@
1+
from kitconcept.solr.interfaces import IKitconceptSolrSettings
12
from plone import api
3+
from plone.registry.interfaces import IRegistry
4+
from zope.component import getUtility
25

36

47
def add_highlighting_config(context):
@@ -10,3 +13,8 @@ def add_highlighting_config(context):
1013
{"field": "Description", "prop": "highlighting_description"},
1114
]
1215
api.portal.set_registry_record("kitconcept.solr.config", value)
16+
17+
18+
def update_registry_schema(context):
19+
registry = getUtility(IRegistry)
20+
registry.registerInterface(IKitconceptSolrSettings, prefix="kitconcept.solr")

backend/src/kitconcept/solr/upgrades/configure.zcml

Lines changed: 11 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -26,4 +26,15 @@
2626
/>
2727
</genericsetup:upgradeSteps>
2828

29+
<genericsetup:upgradeSteps
30+
profile="kitconcept.solr:default"
31+
source="1002"
32+
destination="1003"
33+
>
34+
<genericsetup:upgradeStep
35+
title="Update registry schema"
36+
handler=".update_registry_schema"
37+
/>
38+
</genericsetup:upgradeSteps>
39+
2940
</configure>

backend/tests/services/navigation/test_services_navigation.py

Lines changed: 35 additions & 17 deletions
Original file line numberDiff line numberDiff line change
@@ -1,56 +1,74 @@
1+
from kitconcept.solr.testing import FUNCTIONAL_TESTING
12
from plone.app.testing import setRoles
23
from plone.app.testing import SITE_OWNER_NAME
34
from plone.app.testing import SITE_OWNER_PASSWORD
45
from plone.app.testing import TEST_USER_ID
56
from plone.dexterity.utils import createContentInContainer
67
from plone.registry.interfaces import IRegistry
78
from plone.restapi.bbb import INavigationSchema
8-
from plone.restapi.testing import PLONE_RESTAPI_DX_FUNCTIONAL_TESTING
99
from plone.restapi.testing import RelativeSession
1010
from zope.component import getUtility
1111

1212
import transaction
1313
import unittest
1414

1515

16+
LANG = "en"
17+
18+
19+
def create(container, portal_type, **kw):
20+
content = createContentInContainer(container, portal_type, **kw)
21+
content.language = LANG
22+
return content
23+
24+
1625
class TestServicesNavigation(unittest.TestCase):
17-
layer = PLONE_RESTAPI_DX_FUNCTIONAL_TESTING
26+
layer = FUNCTIONAL_TESTING
1827

1928
def setUp(self):
2029
self.app = self.layer["app"]
2130
self.portal = self.layer["portal"]
2231
self.portal_url = self.portal.absolute_url()
2332
setRoles(self.portal, TEST_USER_ID, ["Manager"])
2433

34+
# Re-enable Folder type (disabled by plone.volto profile)
35+
fti = self.portal.portal_types["Folder"]
36+
fti.global_allow = True
37+
38+
# Ensure Folder is in displayed_types for navigation
39+
registry = getUtility(IRegistry)
40+
settings = registry.forInterface(INavigationSchema, prefix="plone")
41+
displayed_types = settings.displayed_types
42+
if "Folder" not in displayed_types:
43+
settings.displayed_types = tuple(list(displayed_types) + ["Folder"])
44+
2545
self.api_session = RelativeSession(self.portal_url, test=self)
2646
self.api_session.headers.update({"Accept": "application/json"})
2747
self.api_session.auth = (SITE_OWNER_NAME, SITE_OWNER_PASSWORD)
2848

29-
self.folder = createContentInContainer(
30-
self.portal, "Folder", id="folder", title="Some Folder"
31-
)
32-
self.folder2 = createContentInContainer(
49+
self.folder = create(self.portal, "Folder", id="folder", title="Some Folder")
50+
self.folder2 = create(
3351
self.portal, "Folder", id="folder2", title="Some Folder 2"
3452
)
35-
self.subfolder1 = createContentInContainer(
53+
self.subfolder1 = create(
3654
self.folder, "Folder", id="subfolder1", title="SubFolder 1"
3755
)
38-
self.subfolder2 = createContentInContainer(
56+
self.subfolder2 = create(
3957
self.folder, "Folder", id="subfolder2", title="SubFolder 2"
4058
)
41-
self.thirdlevelfolder = createContentInContainer(
59+
self.thirdlevelfolder = create(
4260
self.subfolder1,
4361
"Folder",
4462
id="thirdlevelfolder",
4563
title="Third Level Folder",
4664
)
47-
self.fourthlevelfolder = createContentInContainer(
65+
self.fourthlevelfolder = create(
4866
self.thirdlevelfolder,
4967
"Folder",
5068
id="fourthlevelfolder",
5169
title="Fourth Level Folder",
5270
)
53-
createContentInContainer(self.folder, "Document", id="doc1", title="A document")
71+
create(self.folder, "Document", id="doc1", title="A document")
5472
transaction.commit()
5573

5674
def tearDown(self):
@@ -93,13 +111,13 @@ def test_dont_broke_with_contents_without_review_state(self):
93111
settings = registry.forInterface(INavigationSchema, prefix="plone")
94112
displayed_types = settings.displayed_types
95113
settings.displayed_types = tuple(list(displayed_types) + ["File"])
96-
createContentInContainer(
114+
create(
97115
self.portal,
98116
"File",
99117
id="example-file",
100118
title="Example file",
101119
)
102-
createContentInContainer(
120+
create(
103121
self.folder,
104122
"File",
105123
id="example-file-1",
@@ -123,7 +141,7 @@ def test_show_excluded_items(self):
123141
# False for Plone 6.0 and True for Plone 5.2
124142
# explicitly set the value to False to avoid test failures
125143
settings.show_excluded_items = False
126-
createContentInContainer(
144+
create(
127145
self.folder,
128146
"Folder",
129147
id="excluded-subfolder",
@@ -161,13 +179,13 @@ def test_navigation_sorting(self):
161179
"Collection",
162180
"File",
163181
)
164-
createContentInContainer(
182+
create(
165183
self.portal,
166184
"File",
167185
id="example-file",
168186
title="Example file",
169187
)
170-
createContentInContainer(
188+
create(
171189
self.folder,
172190
"File",
173191
id="example-file-1",
@@ -208,7 +226,7 @@ def test_use_nav_title_when_available_and_set(self):
208226
title = "Example Document"
209227
nav_title = "Fancy title"
210228

211-
createContentInContainer(
229+
create(
212230
self.folder,
213231
"DXTestDocument",
214232
id="example-dx-document",

0 commit comments

Comments
 (0)