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

Geolocation filtering #37

Merged
merged 30 commits into from
Apr 20, 2019
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
30 commits
Select commit Hold shift + click to select a range
8302a2d
WIP: Geolocation filtering
petschki Mar 27, 2019
b3ef9c1
generalize geojson data fetching
petschki Mar 27, 2019
3681684
remove unused imports
petschki Mar 27, 2019
08e5450
define map configuration inside of tile
petschki Mar 27, 2019
5b3c0c6
flake8: ignore multiple spaces
petschki Mar 27, 2019
48fe33c
update README and fix empty searchable text field submission
petschki Mar 27, 2019
7819715
first draft of adaptable GeoJSON properties
petschki Mar 27, 2019
648be75
maps filter testing [ci skip]
petschki Mar 27, 2019
4d1673d
initialize filtering on map zoomend/moveend [ci skip]
petschki Mar 28, 2019
b240f92
zcml conditions when optional dependency to plone.tiles is missing [c…
petschki Mar 28, 2019
174e816
Add thet to geolocation feature.
thet Mar 29, 2019
8988f3e
map filtering feature: narrow down result after zooming/moving
petschki Mar 29, 2019
df2979d
refactor features to different packages
petschki Apr 1, 2019
3a9aedb
cleaup locales
petschki Apr 1, 2019
e0bcd72
fix buildout
petschki Apr 1, 2019
0401f3b
checkout refactored branch
petschki Apr 1, 2019
ddfab54
note on lng/lat index columns
thet Apr 4, 2019
c396f31
info if collection is not accessible
petschki Apr 4, 2019
94a20e3
map filtering - getting closer
petschki Apr 5, 2019
949494b
do not reload map while zooming/moving ... only results and filter ti…
petschki Apr 5, 2019
4c3f937
add maps portlet
petschki Apr 5, 2019
220f35b
flake8
petschki Apr 5, 2019
c4d5914
revert brower history fix
petschki Apr 5, 2019
0da12dd
add a buildout-extends file to declare needed versions for geolocatio…
petschki Apr 6, 2019
dbfcc3c
use extra_require for geolocation dependencies in setup.py
petschki Apr 6, 2019
9985436
new ``pat-leaflet`` property
petschki Apr 8, 2019
4fe351d
update just released patternslib version
petschki Apr 11, 2019
1075781
minor cleanups
thet Apr 19, 2019
8c3d65b
Use getattr for tolerant accessing IGeoJSONProperties attributes. Als…
thet Apr 20, 2019
e6a4e23
get default map layer/layers values from registry setting and then fr…
thet Apr 20, 2019
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
12 changes: 9 additions & 3 deletions CHANGES.rst
Original file line number Diff line number Diff line change
Expand Up @@ -4,13 +4,19 @@ Changelog
3.1 (unreleased)
----------------

New features:

- Geolocation filter.
[petschki, thet]


Bug fixes:

- constrain ``target collection`` to a configurable registry value.
the default is ``['Collection', ]``
- Constrain ``target collection`` to a configurable registry value.
The default is ``['Collection', ]``.
[petschki]

- fix non-interable catalog metadata values for Python 3
- Fix non-interable catalog metadata values for Python 3.
[petschki]


Expand Down
49 changes: 48 additions & 1 deletion README.rst
Original file line number Diff line number Diff line change
Expand Up @@ -9,7 +9,54 @@ This can also be used to build tag clouds.

The filter types can be extended (see: ``collective.collectionfilter.vocabularies``).

Besides the "Collection Filter" portlet/tile there is also a "Collection Search" portlet/tile for doing a fulltext search on the collection results.
There are three portlets/tiles available for filtering:

- "Collection Filter" - a list with values (select, radio, checkbox, link) you can filter on
- "Collection Search" - a SearchableText input field to do a fulltextsearch on the collection results
- "Collection Maps" - a LeafletJS map which shows and filters ``IGeolocatable`` items on it
(this feature is available if ``collective.geolocationbehavior`` is installed and the behavior
is activated on a contenttype. See installation notes below)


Filter Results with portlets
----------------------------

Add as many of the filter portlets above to any context you want (most likely the source collection)
and assign a collection with results to it.

When you select values from the filter the results are loaded asynchronously inside the container
with the selector defined in the field ``Content Selector``. Make sure the selector exists on the
source collection template and on the target page which shows the filtered results.


Mosaic Integration
------------------

The three tiles can be added within the Mosaic editor multiple times. Just select them in the ``Insert`` menu
and assign a collection to it. To show the results of the collection simply add a
``Existing Content`` tile which links to the same collection your filter tiles are assigned with.

TODO: right now the collection needs a default_view template, which wraps the result list with a unique selector
inside the ``#content-core`` container. so the collectionfilter can load the filtered result correctly from
the collection into the container inside the existing content tile.


Geolocation filter support
--------------------------

If ``collective.geolocationbehavior`` is installed, this package provides a LeafletJS Maps tile/portlet
which shows each item of a collection result if the ``IGeolocatable`` information is available.
In addition you can activate the ``Narrow down results`` checkbox to narrow down the collection result and
other available filter tiles/portlets if the user moves or zooms the map.

We provide a package extra to install all required dependencies with their according versions.
Simply do this somewhere in your buildout::

[buildout]
...
eggs +=
collective.collectionfilter[geolocation]
...


Overloading GroupByCriteria
Expand Down
31 changes: 31 additions & 0 deletions base.cfg
Original file line number Diff line number Diff line change
@@ -0,0 +1,31 @@
[buildout]
extensions = mr.developer
eggs +=
collective.geolocationbehavior
sources = sources
versions = versions
sources-dir = extras
auto-checkout =
collective.geolocationbehavior
plone.formwidget.geolocation
plone.patternslib

[sources]
collective.geolocationbehavior = git ${remotes:collective}/collective.geolocationbehavior.git pushurl=${remotes:collective_push}/collective.geolocationbehavior.git branch=petschki-indexer-adapter
plone.formwidget.geolocation = git ${remotes:collective}/plone.formwidget.geolocation.git pushurl=${remotes:collective_push}/plone.formwidget.geolocation.git branch=map-settings
plone.patternslib = git ${remotes:plone}/plone.patternslib.git pushurl=${remotes:plone_push}/plone.patternslib.git

[versions]
collective.collectionfilter =
collective.geolocationbehavior =
plone.formwidget.geolocation =
plone.patternslib =

[remotes]
# Collective
collective = https://github.com/collective
collective_push = git@github.com:collective

# Plone
plone = https://github.com/plone
plone_push = git@github.com:plone
6 changes: 4 additions & 2 deletions buildout.cfg
Original file line number Diff line number Diff line change
@@ -1,11 +1,13 @@
[buildout]
extends = test-5.1.x.cfg
extends =
test-5.1.x.cfg

parts +=
releaser
i18ndude
omelette

versions = versions

[omelette]
recipe = collective.recipe.omelette
Expand All @@ -31,5 +33,5 @@ eggs =
sphinxcontrib-httpdomain

[versions]
# Don't use a released version of collective.easyforms
# Don't use a released version of collective.collectionfilter
collective.collectionfilter =
23 changes: 20 additions & 3 deletions setup.py
Original file line number Diff line number Diff line change
Expand Up @@ -2,15 +2,23 @@
from setuptools import find_packages
from setuptools import setup

import os

version = '3.1.dev0'


def read(*rnames):
with open(os.path.join(os.path.dirname(__file__), *rnames)) as f:
return f.read()


setup(
name='collective.collectionfilter',
version=version,
description="Plone addon for filtering collection results.",
long_description='{0}\n\n{1}'.format(
open("README.rst").read(),
open("CHANGES.rst").read()
read("README.rst"),
read("CHANGES.rst"),
),
classifiers=[
"Framework :: Plone",
Expand Down Expand Up @@ -39,13 +47,22 @@
'plone.app.contenttypes',
],
extras_require={
'geolocation': [
# support for latitude/longitude catalog index
'collective.geolocationbehavior >= 1.6.0',
# refactored map configuration
'plone.formwidget.geolocation >= 2.2.0',
# leaflet JS events for map filter
'plone.patternslib >= 1.1.0',
],
'test': [
'collective.geolocationbehavior',
'plone.app.testing[robot]',
'plone.app.robotframework',
'plone.app.contenttypes',
'robotframework-selenium2library',
'robotframework-selenium2screenshots',
]
],
},
entry_points="""
# -*- Entry points: -*-
Expand Down
108 changes: 107 additions & 1 deletion src/collective/collectionfilter/baseviews.py
Original file line number Diff line number Diff line change
@@ -1,17 +1,29 @@
# -*- coding: utf-8 -*-
import json

from Acquisition import aq_inner
from Products.CMFPlone.utils import get_top_request
from Products.CMFPlone.utils import safe_unicode
from collective.collectionfilter import _
from collective.collectionfilter.filteritems import get_filter_items
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.vocabularies import TEXT_IDX
from plone.app.contenttypes.behaviors.collection import ICollection
from plone.app.uuid.utils import uuidToCatalogBrain
from plone.app.uuid.utils import uuidToObject
from plone.i18n.normalizer.interfaces import IIDNormalizer
from six.moves.urllib.parse import urlencode
from zope.component import queryUtility

try:
from collective.geolocationbehavior.interfaces import IGeoJSONProperties
HAS_GEOLOCATION = True
except ImportError:
HAS_GEOLOCATION = False


class BaseView(object):
"""Abstract base filter view class.
Expand Down Expand Up @@ -122,7 +134,6 @@ def urlquery(self):
def ajax_url(self):
# Recursively transform all to unicode
request_params = safe_decode(self.top_request.form)
request_params.update({'x': 'y'}) # ensure at least one val is set
urlquery = base_query(request_params, extra_ignores=['SearchableText'])
query_param = urlencode(safe_encode(urlquery), doseq=True)
ajax_url = u'/'.join([it for it in [
Expand All @@ -131,3 +142,98 @@ def ajax_url(self):
'?' + query_param if query_param else None
] if it])
return ajax_url


if HAS_GEOLOCATION:

class BaseMapsView(BaseView):

@property
def ajax_url(self):
# Recursively transform all to unicode
request_params = safe_decode(self.top_request.form)
urlquery = base_query(
request_params, extra_ignores=['latitude', 'longitude'])
query_param = urlencode(safe_encode(urlquery), doseq=True)
ajax_url = u'/'.join([it for it in [
self.collection.getURL(),
self.settings.view_name,
'?' + query_param if query_param else None
] if it])
return ajax_url

@property
def locations(self):
custom_query = {} # Additional query to filter the collection

collection = uuidToObject(self.settings.target_collection)
if not collection:
return None

# Recursively transform all to unicode
request_params = safe_decode(self.top_request.form or {})

# Get all collection results with additional filter
# defined by urlquery
custom_query = base_query(request_params)
custom_query = make_query(custom_query)
return ICollection(collection).results(
batch=False,
brains=True,
custom_query=custom_query
)

@property
def data_geojson(self):
"""Return the geo location as GeoJSON string.
"""
features = []

for it in self.locations:
if not it.longitude or not it.latitude:
# these ``it`` are brains, so anything which got lat/lng
# indexed can be used.
continue

props = IGeoJSONProperties(it.getObject())

feature = {
'type': 'Feature',
'id': it.UID,
'properties': {},
'geometry': {
'type': 'Point',
'coordinates': [
it.longitude,
it.latitude,
]
}
}
if getattr(props, 'popup', None):
feature['properties']['popup'] = props.popup
if getattr(props, 'color', None):
feature['properties']['color'] = props.color
if getattr(props, 'extraClasses', None):
feature['properties']['extraClasses'] = props.extraClasses

features.append(feature)

if not features:
return

geo_json = json.dumps({
'type': 'FeatureCollection',
'features': features
})
return geo_json

@property
def map_configuration(self):
config = {
"default_map_layer": self.settings.default_map_layer,
"map_layers": [
{"title": _(it), "id": it}
for it in self.settings.map_layers
],
}
return json.dumps(config)
60 changes: 60 additions & 0 deletions src/collective/collectionfilter/interfaces.py
Original file line number Diff line number Diff line change
@@ -1,13 +1,22 @@
# -*- coding: utf-8 -*-
from collective.collectionfilter import _
from collective.collectionfilter import utils
from plone.api.portal import get_registry_record as getrec
from plone.app.z3cform.widget import RelatedItemsFieldWidget
from plone.autoform.directives import widget
from zope import schema
from zope.interface import Interface
from zope.publisher.interfaces.browser import IDefaultBrowserLayer


try:
from plone.formwidget.geolocation.vocabularies import default_map_layer
from plone.formwidget.geolocation.vocabularies import default_map_layers
HAS_GEOLOCATION = True
except ImportError:
HAS_GEOLOCATION = False


class ICollectionFilterBaseSchema(Interface):

header = schema.TextLine(
Expand Down Expand Up @@ -175,3 +184,54 @@ class IGroupByModifier(Interface):

class ICollectionFilterBrowserLayer(IDefaultBrowserLayer):
"""Marker interface that defines a browser layer."""


if HAS_GEOLOCATION:

def map_layer_default():
return getrec(
name='geolocation.default_map_layer',
default=default_map_layer
)

def map_layers_default():
return getrec(
name='geolocation.map_layers',
default=default_map_layers
)

class ICollectionMapsSchema(ICollectionFilterBaseSchema):
""" schema for maps filtering
"""
narrow_down = schema.Bool(
title=_(u'label_narrow_down_results', default=u'Narrow down result'),
description=_(
u'help_narrow_down_results',
default=u'Narrow down the result after zooming/moving the map.'),
default=False,
required=False
)

default_map_layer = schema.Choice(
title=_(
u'default_map_layer',
u'Default map layer'
),
description=_(
u'help_default_map_layer',
default=u'Set the default map layer'
),
required=False,
defaultFactory=map_layer_default,
vocabulary='plone.formwidget.geolocation.vocabularies.map_layers'
)

map_layers = schema.List(
title=_(u'label_map_layers', u'Map Layers'),
description=_(
u'help_map_layers',
default=u'Set the available map layers'),
required=False,
defaultFactory=map_layers_default,
missing_value=[],
value_type=schema.Choice(vocabulary='plone.formwidget.geolocation.vocabularies.map_layers')) # noqa: E501
Loading