diff --git a/CHANGES.rst b/CHANGES.rst index 48e819bb..a6c36a64 100644 --- a/CHANGES.rst +++ b/CHANGES.rst @@ -14,8 +14,26 @@ Breaking Change: needs to expect this parameter too! [MrTango] +Features: + +- Add checkbox to reverse output the filters. + [jensens] + +- Support and Test Plone 6. + [jensens] + +- Add a module global COLLECTIONISH_TARGETS to c.c.tiles to register other tiles with collections than plone.app.standardtiles.contentlisting. + [jensens] + Bug Fixes: +- Not every metadata entry has an index with same name. + Ignore if not a pair. + [jensens] + +- Fix/Workaround for #59 (Int fields in indexes are not working). + [jensens] + - Hide uninstall profiles from install view. [jensens] @@ -28,6 +46,7 @@ Bug Fixes: - Fixed searches for only non-alphanumeric characters causing an exception to be displayed. [JeffersonBledsoe] + Other: - Code-Style Black and Isort diff --git a/base.cfg b/base.cfg index 89d11bfb..334c38a5 100644 --- a/base.cfg +++ b/base.cfg @@ -17,18 +17,6 @@ plone.batching = >=1.1.7 # plone.app.standardtiles = 2.3.1 # fix https://github.com/plone/plone.app.standardtiles/issues/111 pycodestyle = -# Robot Testing (see buildout.coredev[5.2]/versions.cfg) -plone.app.robotframework = 1.5.4 -robotframework = 3.1.2 -robotframework-python3 = 2.9 -robotframework-debuglibrary = 1.2.1 -robotframework-ride = 1.7.4.1 -robotframework-seleniumlibrary = 3.3.1 -robotframework-selenium2library = 3.0.0 -robotframework-selenium2screenshots = 0.8.1 -robotsuite = 2.2.1 -selenium = 3.141.0 - [versions:python27] # to get codeanalysis working build = 0.1 diff --git a/requirements-5.2.x.txt b/requirements-5.2.x.txt index 9dd163b8..4ad8c835 100644 --- a/requirements-5.2.x.txt +++ b/requirements-5.2.x.txt @@ -1 +1 @@ - -r https://dist.plone.org/release/5.2-latest/requirements.txt +-r https://dist.plone.org/release/5.2-latest/requirements.txt diff --git a/requirements-6.0.x.txt b/requirements-6.0.x.txt index 8b0f7dc3..3ae6c36a 100644 --- a/requirements-6.0.x.txt +++ b/requirements-6.0.x.txt @@ -1 +1 @@ - -r https://dist.plone.org/release/6.0-latest/requirements.txt +-r https://dist.plone.org/release/6.0-latest/requirements.txt diff --git a/src/collective/collectionfilter/baseviews.py b/src/collective/collectionfilter/baseviews.py index 26e9053a..ff950ee8 100644 --- a/src/collective/collectionfilter/baseviews.py +++ b/src/collective/collectionfilter/baseviews.py @@ -130,13 +130,11 @@ class BaseFilterView(BaseView): def input_type(self): if self.settings.input_type == "links": return "link" - elif self.settings.filter_type == "single": + if self.settings.filter_type == "single": if self.settings.input_type == "checkboxes_radiobuttons": return "radio" - else: - return "dropdown" - else: - return "checkbox" + return "dropdown" + return "checkbox" # results is called twice inside the template in view/available and view/results. But its expensive so we cache it # but just the the lifetime of the view @@ -152,6 +150,7 @@ def results(self): cache_enabled=self.settings.cache_enabled, request_params=self.top_request.form or {}, content_selector=self.settings.content_selector, + reverse=self.settings.reverse, ) return results diff --git a/src/collective/collectionfilter/filteritems.py b/src/collective/collectionfilter/filteritems.py index 78a8bb8a..432c08e7 100644 --- a/src/collective/collectionfilter/filteritems.py +++ b/src/collective/collectionfilter/filteritems.py @@ -42,6 +42,7 @@ def _build_url( ): # Build filter url query _urlquery = urlquery.copy() + # Allow deselection if filter_value in current_idx_value: _urlquery[idx] = [it for it in current_idx_value if it != filter_value] @@ -105,6 +106,7 @@ def _results_cachekey( cache_enabled=True, request_params=None, content_selector="", + reverse=False, ): if not cache_enabled: raise DontCache @@ -117,6 +119,7 @@ def _results_cachekey( view_name, request_params, content_selector, + reverse, " ".join(plone.api.user.get_roles()), plone.api.portal.get_current_language(), str(plone.api.portal.get_tool("portal_catalog").getCounter()), @@ -135,6 +138,7 @@ def get_filter_items( cache_enabled=True, request_params=None, content_selector="", + reverse=False, ): request_params = request_params or {} custom_query = {} # Additional query to filter the collection @@ -189,7 +193,7 @@ def get_filter_items( # Allow value_blacklist to be callables for runtime-evaluation value_blacklist = ( value_blacklist() if callable(value_blacklist) else value_blacklist - ) # noqa + ) # fallback to title sorted values sort_key_function = groupby_criteria[group_by].get( "sort_key_function", lambda it: it["title"].lower() @@ -213,6 +217,9 @@ def get_filter_items( or filter_value in value_blacklist ): continue + if type(filter_value) == int: + # if indexed value is an integer, convert to string + filter_value = str(filter_value) if filter_value in grouped_results: # Add counter, if filter value is already present grouped_results[filter_value]["count"] += 1 @@ -262,6 +269,9 @@ def get_filter_items( if callable(sort_key_function): grouped_results = sorted(grouped_results, key=sort_key_function) + if reverse: + grouped_results = reversed(grouped_results) + ret += grouped_results return ret diff --git a/src/collective/collectionfilter/interfaces.py b/src/collective/collectionfilter/interfaces.py index 0d3215d5..f33bb343 100644 --- a/src/collective/collectionfilter/interfaces.py +++ b/src/collective/collectionfilter/interfaces.py @@ -132,8 +132,18 @@ class ICollectionFilterSchema(ICollectionFilterBaseSchema): vocabulary="collective.collectionfilter.InputType", ) + reverse = schema.Bool( + title=_(u"label_reverse", default=u"Reverse sort filter"), + description=_( + u"help_reverse", + default=u"Reverse the sorting of th list of filter options.", + ), + default=False, + required=False, + ) + narrow_down = schema.Bool( - title=_(u"label_narrow_down", default=u"Narrow down filter options"), # noqa + title=_(u"label_narrow_down", default=u"Narrow down filter options"), description=_( u"help_narrow_down", default=u"Narrow down the filter options when a filter of this group is applied." # noqa @@ -145,7 +155,7 @@ class ICollectionFilterSchema(ICollectionFilterBaseSchema): ) hide_if_empty = schema.Bool( - title=_(u"label_hide_if_empty", default=u"Hide if empty"), # noqa + title=_(u"label_hide_if_empty", default=u"Hide if empty"), description=_( u"help_hide_if_empty", default=u"Don't display if there is 1 or no options without selecting a filter yet.", diff --git a/src/collective/collectionfilter/portlets/collectionfilter.py b/src/collective/collectionfilter/portlets/collectionfilter.py index 9301d4d9..38337d08 100644 --- a/src/collective/collectionfilter/portlets/collectionfilter.py +++ b/src/collective/collectionfilter/portlets/collectionfilter.py @@ -28,6 +28,7 @@ class Assignment(base.Assignment): view_name = None content_selector = "#content-core" hide_if_empty = False + reverse = False # list_scaling = None def __init__( @@ -43,6 +44,7 @@ def __init__( view_name=None, content_selector="#content-core", hide_if_empty=False, + reverse=False, # list_scaling=None ): self.header = header @@ -56,6 +58,7 @@ def __init__( self.view_name = view_name self.content_selector = content_selector self.hide_if_empty = hide_if_empty + self.reverse = reverse # self.list_scaling = list_scaling @property diff --git a/src/collective/collectionfilter/testing.py b/src/collective/collectionfilter/testing.py index dc041935..37ff5f17 100644 --- a/src/collective/collectionfilter/testing.py +++ b/src/collective/collectionfilter/testing.py @@ -14,6 +14,8 @@ import json import os +import pytz +import six try: @@ -77,12 +79,17 @@ def setUpPloneSite(self, portal): } ], ) + if six.PY2: + now = datetime.now() + else: + now = datetime.now(pytz.UTC) + portal.invokeFactory( "Event", id="testevent", title=u"Test Event", - start=datetime.now() + timedelta(days=1), - end=datetime.now() + timedelta(days=2), + start=now + timedelta(days=1), + end=now + timedelta(days=2), subject=[u"Süper", u"Evänt"], exclude_from_nav=False, ) diff --git a/src/collective/collectionfilter/tests/robot/test_filterportlets.robot b/src/collective/collectionfilter/tests/robot/test_filterportlets.robot index 431a6e7b..0605083f 100644 --- a/src/collective/collectionfilter/tests/robot/test_filterportlets.robot +++ b/src/collective/collectionfilter/tests/robot/test_filterportlets.robot @@ -45,7 +45,7 @@ Scenario: Test Batching Scenario: Hide when no options Given I've got a site with a collection - and my collection has a collection filter author_name or checkboxes_dropdowns Hide if empty + and my collection has a collection filter Creator or checkboxes_dropdowns Hide if empty When I'm viewing the collection then Should be 3 collection results then Should be 0 filter options diff --git a/src/collective/collectionfilter/tiles/__init__.py b/src/collective/collectionfilter/tiles/__init__.py index e6e42637..305775d7 100644 --- a/src/collective/collectionfilter/tiles/__init__.py +++ b/src/collective/collectionfilter/tiles/__init__.py @@ -15,6 +15,12 @@ import re +# tile names actijg collectionish +COLLECTIONISH_TARGETS = [ + "plone.app.standardtiles.contentlisting", +] + + class DictDataWrapper(object): def __init__(self, data): self.data = data @@ -51,6 +57,8 @@ def reload_url(self): def findall_tiles(context, spec): + if not isinstance(spec, list): + spec = [spec] request = context.REQUEST la = ILayoutAware(context) layout = ( @@ -62,8 +70,10 @@ def findall_tiles(context, spec): ) if layout is None: return [] - urls = re.findall(r"(@@[\w\.]+/\w+)", layout) - urls = [url for url in urls if url.startswith("@@{}".format(spec))] + possible_urls = re.findall(r"(@@[\w\.]+/\w+)", layout) + urls = [] + for name in spec: + urls += [url for url in possible_urls if url.startswith("@@{}".format(name))] # TODO: maybe better to get tile data? using ITileDataManager(id)? our_tile = request.response.headers.get("x-tile-url") tiles = [context.unrestrictedTraverse(str(url)) for url in urls] @@ -107,10 +117,10 @@ def selectContent(self, selector=None): if selector is None: selector = "" self.tile = None - tiles = findall_tiles(self.context, "plone.app.standardtiles.contentlisting") + tiles = findall_tiles(self.context, COLLECTIONISH_TARGETS) for tile in tiles: tile.update() - tile_classes = tile.tile_class.split() + [""] + tile_classes = getattr(tile, "tile_class", "").split() + [""] # First tile that matches all the selector classes if all([_class in tile_classes for _class in selector.split(".")]): self.tile = tile @@ -121,25 +131,22 @@ def selectContent(self, selector=None): self.tile = tile if self.tile is not None or self.collection is not None: return self - else: - return None @property def sort_reversed(self): if self.tile is not None: return self.sort_order == "reverse" - else: - return self.collection.sort_reversed + return self.collection.sort_reversed @property def content_selector(self): - """will return None if no tile or colleciton found""" + """will return None if no tile or collection found""" if self.collection is None: - return None - elif self.tile is None: + return + if self.tile is None: return super(CollectionishLayout, self).content_selector classes = ["contentlisting-tile"] - if self.tile.tile_class: + if getattr(self.tile, "tile_class", ""): classes += self.tile.tile_class.split() return "." + ".".join(classes) diff --git a/src/collective/collectionfilter/vocabularies.py b/src/collective/collectionfilter/vocabularies.py index 84fcce34..964e97cd 100644 --- a/src/collective/collectionfilter/vocabularies.py +++ b/src/collective/collectionfilter/vocabularies.py @@ -26,6 +26,7 @@ # Use this EMPTY_MARKER for your custom indexer to index empty criterions. EMPTY_MARKER = "__EMPTY__" TEXT_IDX = "SearchableText" +INTEGER_IDXS = [] GEOLOC_IDX = [ "latitude", "longitude", @@ -56,9 +57,16 @@ "start", "sync_uid", "total_comments", + "TranslationGroup", ] + GEOLOC_IDX # latitude/longitude is handled as a range filter ... see query.py # noqa DEFAULT_FILTER_TYPE = "single" LIST_SCALING = ["No Scaling", "Linear", "Logarithmic"] +TRUTHY = [ + safe_encode("true"), + safe_encode("1"), + safe_encode("t"), + safe_encode("yes"), +] def translate_value(value, *args, **kwargs): @@ -71,30 +79,26 @@ def translate_messagefactory(value, *args, **kwargs): def make_bool(value): """Transform into a boolean value.""" - truthy = [ - safe_encode("true"), - safe_encode("1"), - safe_encode("t"), - safe_encode("yes"), - ] + if value is None: return if isinstance(value, bool): return value - value = safe_encode(value) - value = value.lower() - if value in truthy: - return True - else: - return False + value = safe_encode(value).lower() + return value in TRUTHY + + +def make_int(value): + if isinstance(value, (list, tuple)): + return [int(val) for val in value] + return int(value) def yes_no(value): """Return i18n message for a value.""" if value: return _(u"Yes") - else: - return _(u"No") + return _(u"No") def get_yes_no_title(item, *args, **kwargs): @@ -143,11 +147,13 @@ def groupby(self): # get catalog metadata schema, but filter out items which cannot be # used for grouping metadata = [it for it in cat.schema() if it not in GROUPBY_BLACKLIST] - for it in metadata: + if it not in cat.indexes(): + # collectionfilter needs both, metadata and index entries. + continue + idx = cat._catalog.indexes.get(it) index_modifier = None display_modifier = translate_value # Allow to translate in this package domain per default. # noqa - idx = cat._catalog.indexes.get(it) if six.PY2 and getattr(idx, "meta_type", None) == "KeywordIndex": # in Py2 KeywordIndex accepts only utf-8 encoded values. index_modifier = safe_encode @@ -156,6 +162,9 @@ def groupby(self): index_modifier = make_bool display_modifier = get_yes_no_title + if idx.getId() in INTEGER_IDXS: + index_modifier = make_int + # for portal_type or Type we have some special sauce as we need to translate via fti.i18n_domain. # noqa if it == "portal_type": display_modifier = translate_portal_type @@ -187,7 +196,7 @@ def groupby(self, value): def GroupByCriteriaVocabulary(context): """Collection filter group by criteria.""" groupby = getUtility(IGroupByCriteria).groupby - items = [SimpleTerm(title=_(it), value=it) for it in groupby.keys()] + items = [SimpleTerm(value=it, token=str(it), title=_(it)) for it in groupby.keys()] return SimpleVocabulary(items) @@ -209,10 +218,10 @@ def InputTypeVocabulary(context): SimpleTerm( title=_("inputtype_checkboxes_radiobuttons"), value="checkboxes_radiobuttons", - ), # noqa + ), SimpleTerm( title=_("inputtype_checkboxes_dropdowns"), value="checkboxes_dropdowns" - ), # noqa + ), ] return SimpleVocabulary(items) @@ -231,5 +240,5 @@ def SortOnIndexesVocabulary(context): sortable_indexes = reader().get("sortable_indexes") items = [ SimpleTerm(title=_(v["title"]), value=k) for k, v in sortable_indexes.items() - ] # noqa + ] return SimpleVocabulary(items) diff --git a/test-5.2.x.cfg b/test-5.2.x.cfg index dd888675..dd44bfa1 100644 --- a/test-5.2.x.cfg +++ b/test-5.2.x.cfg @@ -18,5 +18,17 @@ test-eggs = extensions += flake8-black +[versions] +plone.app.robotframework = 1.5.4 +robotframework = 3.1.2 +robotframework-python3 = 2.9 +robotframework-debuglibrary = 1.2.1 +robotframework-ride = 1.7.4.1 +robotframework-seleniumlibrary = 3.3.1 +robotframework-selenium2library = 3.0.0 +robotframework-selenium2screenshots = 0.8.1 +robotsuite = 2.2.1 +selenium = 3.141.0 + [versions:python3] coverage = >=3.7