Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

[WIP] info portlet to show summary of query and results #120

Draft
wants to merge 80 commits into
base: main
Choose a base branch
from
Draft
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
80 commits
Select commit Hold shift + click to select a range
8c7b613
initial version of info portlet. not reloading with ajax yet
djay Mar 19, 2021
3ea76b1
make the ajax loading work
djay Mar 23, 2021
b7bf1b0
working first test
djay Mar 24, 2021
8e79e15
Merge branch 'master' into djay/info_portlet
djay Mar 25, 2021
17a793e
fix bad merge on robot tests
djay Mar 25, 2021
00c8c73
make cache keys hit more often. I think.
djay Mar 25, 2021
bdef4d0
fix showing multiple values in a filter
djay Mar 25, 2021
a642b74
add another test
djay Mar 26, 2021
73b5eb3
result and search tests
djay Mar 26, 2021
e280147
test combining templates
djay Mar 26, 2021
14603a7
changes that make test pass
djay Mar 26, 2021
0f6c78e
remove unused code
djay Mar 26, 2021
db36d99
bug in robot test setup so they failed to run in 5.0
djay Mar 26, 2021
48bb584
test search regardless of ajax
djay Mar 26, 2021
16e53bf
hide_when to make teh info portlet hidden sometimes
djay Mar 30, 2021
94c9262
fix bug in hiding portlet
djay Mar 31, 2021
2114d7f
fix big with run keywords if
djay Mar 31, 2021
6568144
fixed bug in search portlet that prevented mutliple filter working wh…
djay Apr 1, 2021
a1cc22c
unicode fix
djay Apr 1, 2021
ff31aea
Merge branch 'master' into djay/info_portlet
djay Apr 9, 2021
9eccfe8
Merge branch 'djay/info_portlet' of github.com:collective/collective.…
djay Apr 9, 2021
0474234
fix merge error
djay Apr 9, 2021
4858014
Merge branch 'master' into djay/info_portlet
djay May 12, 2021
99b0fc8
Merge branch 'master' into djay/info_portlet
djay May 28, 2021
fbf0283
tile test seems to be working locally now. might be plone version dep…
djay Jun 1, 2021
336f888
fix default target_collection being None
djay Jun 2, 2021
a1f6a1a
tiles test only works in 5.0. i18n error otherwise
djay Jun 2, 2021
4c2c67f
initial version of info tile
djay Jun 2, 2021
eac2c4f
fix missing info tile registrations
djay Jun 4, 2021
dca1b03
fix leftover code in info tile
djay Jun 15, 2021
dace969
Add layer for to run all tests using tiles with ajax)
djay Jun 15, 2021
0c7443e
fix info tests to work with tiles
djay Jun 15, 2021
c01e0b2
make layers into matrix so tests run parallel
djay Jun 15, 2021
8a935de
use new standardtiles
djay Jun 15, 2021
dd4149b
remove pause
djay Jun 15, 2021
291a00b
fix tiles not hiding if empty
djay Jun 15, 2021
3fa358e
fix wait test issue
djay Jun 15, 2021
9f22f76
fix Get Get Element Count
djay Jun 15, 2021
be784af
do coverage only for python 3.
djay Jun 16, 2021
e526a8d
make test pass even though aside still visible
djay Jun 16, 2021
9ecd7d1
Filters can pick listing tiles or collections now.
djay Jun 17, 2021
866de2b
make coverage jobs clearer
djay Jun 17, 2021
5ea710a
use select2 on info and sort (for 5.0)
djay Jun 18, 2021
c23636b
skip 5.0 for tiles. can't work
djay Jun 18, 2021
2b70794
fix bugs preventing porlets working with collectionish
djay Jun 18, 2021
9c5804e
exclude ajax disabled from plone 5.0 testing
djay Jun 18, 2021
71dfc03
bug in main.yml
djay Jun 18, 2021
3e30dcf
bug in info tests
djay Jun 18, 2021
e6cc7d6
Merge branch 'collectionish' into djay/info_portlet
djay Jun 22, 2021
66976cc
display as info tile as a title
djay Jun 22, 2021
a5d2322
compile changed js
djay Jun 22, 2021
7a3ddd5
bug hiding info tile
djay Jun 23, 2021
9961fcb
info portlets can have links to select reset filter to single value
djay Jun 25, 2021
b7ec8f6
update readme for info portlet
djay Jun 25, 2021
1e349c4
Merge branch 'master' into djay/info_portlet
JeffersonBledsoe Sep 22, 2021
89a8857
Remove unused import
JeffersonBledsoe Sep 22, 2021
c51121c
Format with black
JeffersonBledsoe Sep 22, 2021
ab30e79
Pass index to display_modifier
JeffersonBledsoe Sep 22, 2021
036a3ec
Initial context-aware implementation
JeffersonBledsoe Sep 29, 2021
49e6dd3
Fix functions and fields with iterable data
JeffersonBledsoe Sep 29, 2021
a8fb172
Fix missing portlet title
JeffersonBledsoe Sep 29, 2021
91f28e1
Cherry pick "refactor code to simplify a4b42ae Dylan Jay <software@pr…
djay Jul 24, 2021
636a118
Flake8 fixes
JeffersonBledsoe Oct 14, 2021
7b1cff4
Fix missing argument error
JeffersonBledsoe Oct 15, 2021
6827955
Fix support for getting target collection from context
JeffersonBledsoe Oct 15, 2021
d068064
Merge branch 'master' into djay/info_portlet
JeffersonBledsoe Oct 15, 2021
56db04f
WIP: Styles
JeffersonBledsoe Oct 19, 2021
9043b54
Merge branch 'master' into djay/info_portlet
JeffersonBledsoe Oct 22, 2021
b6ab863
Fixes from bad merge
JeffersonBledsoe Oct 22, 2021
459e344
Remove unrelated PR changes
JeffersonBledsoe Oct 22, 2021
48e296d
Fix argument name
JeffersonBledsoe Oct 22, 2021
5e146be
Display friendly field names where possible
JeffersonBledsoe Nov 8, 2021
0799d10
Cache the friendly name lookup
JeffersonBledsoe Nov 8, 2021
3ee06c0
Allow displaying a single value
JeffersonBledsoe Dec 8, 2021
e210dcf
Fix for when multiple values are present in the request
JeffersonBledsoe Dec 9, 2021
b6c1081
Fix portlet displaying token instead of value
JeffersonBledsoe Dec 9, 2021
9a214ba
don't use ram cache in case it's causing memory leak
djay Dec 12, 2023
0e5db34
Merge branch 'djay/info_portlet' of github.com:collective/collective.…
djay Dec 12, 2023
a45edf6
merge
djay Dec 12, 2023
ec4805a
prevent circular reference
djay Dec 26, 2023
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
11 changes: 11 additions & 0 deletions README.rst
Original file line number Diff line number Diff line change
Expand Up @@ -33,6 +33,8 @@ There are three portlets/tiles available for filtering:
is activated on a contenttype. See installation notes below)
``Collection Result Listing Sort``
a list of indexes where the user can sort the filtered result listing
``Filter Info``
Displays customisable information about the current search or current context


Filter Results of Collections
Expand Down Expand Up @@ -90,6 +92,15 @@ Simply do this somewhere in your buildout::
collective.collectionfilter[geolocation]
...

Filter Info support
-------------------

Info display (tile or portlet) can be used to
- display the current search or filters.
- as a title replacement when using mosaic with information about the current search
- an alternative form of navigation with links to reset the search to a single option
- a form of navigation on content using values of the currently viewed object to search
for other similar content

Overloading GroupByCriteria
---------------------------
Expand Down
10 changes: 10 additions & 0 deletions base.cfg
Original file line number Diff line number Diff line change
@@ -1,6 +1,16 @@
[buildout]
# extensions = mr.developer
versions = versions
# parts += vscode


[vscode]
recipe = collective.recipe.vscode
eggs = ${test:eggs} ${instance:eggs}
enable-flake8 = true
enable-black = true
generate-envfile = true


[code-analysis]
directory = ${buildout:directory}/src/collective/collectionfilter
Expand Down
247 changes: 238 additions & 9 deletions src/collective/collectionfilter/baseviews.py
Original file line number Diff line number Diff line change
@@ -1,30 +1,39 @@
# -*- coding: utf-8 -*-
import weakref
from Acquisition import aq_inner
from collective.collectionfilter import PLONE_VERSION
from collective.collectionfilter.filteritems import get_filter_items
from collective.collectionfilter.filteritems import ICollectionish
from collective.collectionfilter.filteritems import (
get_filter_items,
ICollectionish,
_build_url,
_build_option,
)
from collective.collectionfilter.interfaces import IGroupByCriteria
from collective.collectionfilter.query import make_query
from collective.collectionfilter.utils import base_query
from collective.collectionfilter.utils import safe_decode
from collective.collectionfilter.utils import safe_encode
from collective.collectionfilter.utils import safe_iterable
from collective.collectionfilter.vocabularies import TEXT_IDX
from plone import api
from collective.collectionfilter.vocabularies import get_conditions
from collective.collectionfilter.vocabularies import EMPTY_MARKER
from plone.api.portal import get_registry_record as getrec
from plone.app.uuid.utils import uuidToCatalogBrain
from plone.app.uuid.utils import uuidToObject
from plone.dexterity.utils import iterSchemata
from plone.i18n.normalizer.interfaces import IIDNormalizer
from plone.memoize import instance
from plone.memoize import instance, ram
from plone.uuid.interfaces import IUUID
from Products.CMFPlone.utils import get_top_request
from Products.CMFPlone.utils import safe_unicode
from six.moves.urllib.parse import urlencode
from zope.component import getUtility
from zope.component import queryUtility
from zope.i18n import translate
from zope.schema import getFieldsInOrder
from zope.schema.interfaces import IVocabularyFactory

from Products.CMFCore.Expression import Expression, getExprContext
from plone import api
import json


Expand All @@ -37,11 +46,15 @@
HAS_GEOLOCATION = False


def empty_ref(self):
return None


class BaseView(object):
"""Abstract base filter view class."""

_collection = None
_top_request = None
_top_request = empty_ref # prevent loops

@property
def settings(self):
Expand Down Expand Up @@ -74,9 +87,9 @@ def reload_url(self):

@property
def top_request(self):
if not self._top_request:
self._top_request = get_top_request(self.request)
return self._top_request
if self._top_request() is None:
self._top_request = weakref.ref(get_top_request(self.request))
return self._top_request()

@property
def collection_uuid(self):
Expand Down Expand Up @@ -278,6 +291,222 @@ def ajax_url(self):
return ajax_url


def _exp_cachekey(method, self, target_collection, request):
return (
target_collection,
json.dumps(request),
self.settings.view_name,
self.settings.as_links,
)


def _field_title_cache_key(method, self, field_id):
return (
field_id[0],
self.context.getTypeInfo().id
)


class BaseInfoView(BaseView):

# TODO: should just cache on request?
# @instance.memoize
# @ram.cache(_exp_cachekey)
def get_expression_context(self, collection, request_params):
count_query = {}
query = base_query(request_params)
collection_url = collection.absolute_url()
# TODO: take out the search
# TODO: match them to indexes and get proper names
# TODO: format values properly
# TODO: do we want to read out sort too?
count_query.update(query)
# TODO: delay evaluating this unless its needed
# TODO: This could be cached as same result total appears in other filter counts
catalog_results_fullcount = ICollectionish(collection).results(
make_query(count_query), request_params
)
results = len(catalog_results_fullcount)

# Clean up filters and values
if "collectionfilter" in query:
del query["collectionfilter"]
groupby_criteria = getUtility(IGroupByCriteria).groupby
q = []
for group_by, value in query.items():
if group_by not in groupby_criteria:
continue
# TODO: we actually have idx not group_by
idx = groupby_criteria[group_by]['index']
value = safe_decode(value)
current_idx_value = safe_iterable(value)
# Set title from filter value with modifications,
# e.g. uuid to title
display_modifier = groupby_criteria[group_by].get("display_modifier", None)
titles = []
for filter_value in current_idx_value:
title = filter_value
if filter_value is not EMPTY_MARKER and callable(display_modifier):
title = safe_decode(display_modifier(filter_value, idx))
# TODO: still no nice title for filter indexes? Should be able to get from query builder
# TODO: we don't know if filter is AND/OR to display that detail
# TODO: do we want no follow always?
# TODO: should support clearing filter? e.g. if single value, click to remove?
# Build filter url query
query_param = urlencode(
safe_encode({group_by: filter_value}), doseq=True
)
url = "/".join(
[
it
for it in [
collection_url,
self.settings.view_name,
"?" + query_param if query_param else None,
]
if it
]
)
# TODO: should have option for nofollow?
if self.settings.as_links:
titles.append(u'<a href="{}">{}</a>'.format(url, title))
else:
titles.append(title)
q.append((group_by, titles))

# Set up context for running templates
expression_context = getExprContext(
collection,
)
expression_context.setLocal("results", results)
expression_context.setLocal("query", q)
expression_context.setLocal("search", query.get("SearchableText", ""))
return expression_context

def info_contents(self):
request_params = self.top_request.form or {}
expression_context = self.get_expression_context(
self.collection.getObject(), request_params
)

parts_vocabulary_factory = getUtility(
IVocabularyFactory, "collective.collectionfilter.TemplateParts"
)
parts_vocabulary = parts_vocabulary_factory(self.context)

parts = []
for template in self.settings.template_type:
text = None
try:
# This vocab term lookup will throw a LookupError if the term doesn't exist in the built-ins
template_definition_term = parts_vocabulary.getTerm(template)
exp = template_definition_term.value
# TODO: precompile templates
text = Expression(exp)(expression_context)

if isinstance(text, list):
text = u", ".join(text)

except LookupError:
text = template

if text:
parts.append(text)

line = u" ".join(parts)
# TODO: should be more generic i18n way to do this?
line = line.replace(u" ,", u",").replace(" :", ":")
return line

def is_content_context(self):
if not self.settings.context_aware:
return False

if self.collection.getObject() == self.context:
return False

return True

def get_fields_to_display(self):
fields = self.settings.context_aware_fields
# TODO: Get the friendly name for the group_by instead of the id
return [
(field, getattr(self.context, field))
for field in fields
if hasattr(self.context, field)
]

# Cache this function based on the index id and the dexterity type
@ram.cache(_field_title_cache_key)
def get_field_title(self, field):
"""
Field is a tuple, where the first index is the name of the field and the second the values for that field
Returns the friendly version of the title when possible, falling
back to the ID if a firiendly name can't be found.
"""
index_id = field[0]

for schema in iterSchemata(context=self.context):
for field_id, field_object in getFieldsInOrder(schema=schema):
if field_id == index_id:
return field_object.title

return index_id

def get_field_values(self, field):
content_value = field[1]
index = field[0]
groupby_criteria = getUtility(IGroupByCriteria).groupby
request_params = self.top_request.form
request_params = safe_decode(request_params)
extra_ignores = [index, index + "_op"]
urlquery = base_query(request_params, extra_ignores)
collection = self.collection.getObject()

# TODO: Refactor the following copied lines from collective.collectionfitler.filteritems into a function
field_value = content_value() if callable(content_value) else content_value
# decode it to unicode
field_value = safe_decode(field_value)
# Make sure it's iterable, as it's the case for e.g. the subject index.
field_values = safe_iterable(field_value)
# allow excluding or extending the field_valueue per index
groupby_modifier = groupby_criteria[index].get("groupby_modifier", None)

if not groupby_modifier:
groupby_modifier = lambda values, cur, narrow: values # noqa: E731

field_values = groupby_modifier(field_values, field_value, False)

field_data = []
for value in field_values:
url = _build_url(collection_url=collection.absolute_url(), urlquery=urlquery, filter_value=value, current_idx_value=[], idx=index, filter_type="single")
data = _build_option(filter_value=value, url=url, current_idx_value=[value], groupby_options=groupby_criteria[index])
field_data.append(data)

return field_data

@property
def is_available(self):
target_collection = self.collection
if target_collection is None:
return False
request_params = self.top_request.form or {}
expression_context = self.get_expression_context(
target_collection.getObject(), request_params
)

conditions = dict((k, (t, e)) for k, t, e in get_conditions())
if not self.settings.hide_when:
return True
for cond in self.settings.hide_when:
_, exp = conditions.get(cond)
# TODO: precompile templates
res = Expression(exp)(expression_context)
if not res:
return True
return False


if HAS_GEOLOCATION:

class BaseMapsView(BaseView):
Expand Down
8 changes: 8 additions & 0 deletions src/collective/collectionfilter/configure.zcml
Original file line number Diff line number Diff line change
Expand Up @@ -47,6 +47,14 @@
component=".vocabularies.SortOnIndexesVocabulary"
name="collective.collectionfilter.SortOnIndexes"
/>
<utility
component=".vocabularies.TemplatePartsVocabulary"
name="collective.collectionfilter.TemplateParts"
/>
<utility
component=".vocabularies.InfoConditionsVocabulary"
name="collective.collectionfilter.InfoConditions"
/>

<adapter factory=".filteritems.CollectionishCollection"
provides="collective.collectionfilter.interfaces.ICollectionish"
Expand Down
5 changes: 5 additions & 0 deletions src/collective/collectionfilter/filteritems.py
Original file line number Diff line number Diff line change
Expand Up @@ -11,6 +11,11 @@
from collective.collectionfilter.vocabularies import EMPTY_MARKER
from Missing import Missing
from plone.app.contenttypes.behaviors.collection import ICollection

try:
from plone.app.blocks.layoutbehavior import ILayoutAware
except ImportError:
ILayoutAware = None
from plone.app.event.base import _prepare_range
from plone.app.event.base import guess_date_from
from plone.app.event.base import start_end_from_mode
Expand Down
Loading
Loading