Commit ac93efdb authored by David Read's avatar David Read
Browse files

Organization page search now works

Also added to README.
Tested only against ckan 2.7 at this stage.
parent 84dcfc1b
# ckanext-hierarchy - Organization hierarchy for CKAN # ckanext-hierarchy - Organization hierarchy for CKAN
Organizations can be arranged into a tree hierarchy.
This new hierarchical arrangement of organizations is displayed
using templates in this extension, instead of the usual list:
![Screenshot of organizations page](screenshots/orgs_page.png)
Provides a new field on the organization edit form to select a parent Provides a new field on the organization edit form to select a parent
organization. This new hierarchical arrangement of organizations is displayed organization:
using templates in this extension, instead of the usual list. An organization ![Screenshot of organization edit page](screenshots/org_edit.png)
page also displays the section of the tree that it is part of, under the
'About' tab. When viewing an organization you see its context within the tree in the side bar. In addition you can widen search of the organization's datasets to include datasets in sub-organizations too:
![Screenshot of organization page](screenshots/org_page.png)
## Detail
Forms (hierachy_form plugin): Forms (hierachy_form plugin):
* /organization/new * /organization/new
...@@ -51,13 +60,12 @@ the short name or acronym (more convenient for display). ...@@ -51,13 +60,12 @@ the short name or acronym (more convenient for display).
} }
``` ```
TODO: TODO:
* make the trees prettier with JSTree * make the trees prettier with JSTree
## Compatibility ## Compatibility
This extension requires CKAN v2.2 or later. This extension requires CKAN v2.7 or later.
## Installation ## Installation
......
import re
import ckan.plugins as p import ckan.plugins as p
import ckan.model as model import ckan.model as model
from ckan.common import request from ckan.common import request
...@@ -14,7 +16,7 @@ def group_tree(organizations=[], type_='organization'): ...@@ -14,7 +16,7 @@ def group_tree(organizations=[], type_='organization'):
def group_tree_filter(organizations, group_tree_list, highlight=False): def group_tree_filter(organizations, group_tree_list, highlight=False):
# this method leaves only the sections of the tree corresponding to the list # this method leaves only the sections of the tree corresponding to the list
# since it was developed for the users, all children organizations from the # since it was developed for the users, all children organizations from the
# organizations in the list are included # organizations in the list are included
def traverse_select_highlighted(group_tree, selection=[], highlight=False): def traverse_select_highlighted(group_tree, selection=[], highlight=False):
# add highlighted branches to the filtered tree # add highlighted branches to the filtered tree
...@@ -89,4 +91,3 @@ def is_include_children_selected(fields): ...@@ -89,4 +91,3 @@ def is_include_children_selected(fields):
if request.params.get('include_children'): if request.params.get('include_children'):
include_children_selected = True include_children_selected = True
return include_children_selected return include_children_selected
import logging
import re
import ckan.plugins as p import ckan.plugins as p
from ckanext.hierarchy.logic import action from ckan import model
from ckanext.hierarchy import helpers
from ckan.lib.plugins import DefaultOrganizationForm from ckan.lib.plugins import DefaultOrganizationForm
from ckan.lib.plugins import DefaultGroupForm from ckan.lib.plugins import DefaultGroupForm
import ckan.logic.schema as s import ckan.logic.schema as s
from ckan.common import c, request from ckan.common import c, request
import logging
import re from ckanext.hierarchy.logic import action
from ckanext.hierarchy import helpers
log = logging.getLogger(__name__) log = logging.getLogger(__name__)
...@@ -69,25 +72,28 @@ class HierarchyDisplay(p.SingletonPlugin): ...@@ -69,25 +72,28 @@ class HierarchyDisplay(p.SingletonPlugin):
# IPackageController # IPackageController
# Modify the search query to include the datasets from
# the children organizations in the result list
def before_search(self, search_params):
if not hasattr('c', 'fields'): def before_search(self, search_params):
'''When searching an organization, optionally extend the search any
sub-organizations too. This is achieved by modifying the search options
before they go to SOLR.
'''
# Check if we're called from the organization controller, as detected
# by c being registered for this thread, and the existence of c.fields
# values
try:
if not isinstance(c.fields, list):
return search_params
except TypeError:
return search_params return search_params
def _children_name_list(children): # e.g. search_params['q'] = u' owner_org:"id" include_children: "True"'
name_list = []
for child in children:
name = child.get('name', "")
name_list += [name] + _children_name_list(child.get('children', []))
return name_list
query = search_params.get('q', None) query = search_params.get('q', None)
c.include_children_selected = False c.include_children_selected = False
# fix the issues with multiple times repeated fields # Fix the issues with multiple times repeated fields
# remove the param from the fields # Remove the param from the fields - NB no longer works
# e.g. [('include_children', 'True')]
new_fields = set() new_fields = set()
for field,value in c.fields: for field,value in c.fields:
if (field != 'include_children'): if (field != 'include_children'):
...@@ -95,37 +101,40 @@ class HierarchyDisplay(p.SingletonPlugin): ...@@ -95,37 +101,40 @@ class HierarchyDisplay(p.SingletonPlugin):
c.fields = list(new_fields) c.fields = list(new_fields)
# parse the query string to check if children are requested # parse the query string to check if children are requested
if query: c.include_children_selected = query and \
base_query = [] 'include_children: "True"' in query
# remove whitespaces between fields and values
query = re.sub(': +', ':', query)
for item in query.split(' '):
field = item.split(':')[0]
value = item.split(':')[-1]
# skip organization
if (field == 'owner_org'):
org_id = value
continue
# skip include children andset option value
if (field == 'include_children'):
if (value.upper() != "FALSE"):
c.include_children_selected = True
continue
base_query += [item]
if c.include_children_selected: if c.include_children_selected:
# add all the children organizations in an 'or' join
children = _children_name_list(helpers.group_tree_section(c.group_dict.get('id'), include_parents=False, include_siblings=False).get('children',[])) # get a list of all the children organizations and include them in
if(children): # the search params
search_params['q'] = " ".join(base_query) children_org_hierarchy = model.Group.get(c.group_dict.get('id')).\
if (len(search_params['q'].strip())>0): get_children_group_hierarchy(type='organization')
search_params['q'] += ' AND ' children_names = [org[1] for org in children_org_hierarchy]
search_params['q'] += '(organization:%s' % c.group_dict.get('name')
for name in children: if children_names:
if name: # remove existing owner_org:"<parent>" clause - we'll replace
search_params['q'] += ' OR organization:%s' % name # it with the tree of orgs in a moment
search_params['q'] += ")" query = query.replace(
'owner_org:"{}"'.format(c.group_dict.get('id')), '')
# remove include_children clause
query = query.replace('include_children: "True"', '')
# add the org clause
query = query.strip()
if query:
query += ' AND '
query += '({})'.format(
' OR '.join(
'organization:{}'.format(org_name)
for org_name in [c.group_dict.get('name')] +
children_names))
search_params['q'] = query
# add it back to fields # add it back to fields
c.fields += [('include_children','True')] # c.fields += [('include_children', 'True')]
# remove include_children from the filter-list - we have a checkbox
del c.fields_grouped['include_children']
return search_params return search_params
......
{% ckan_extends %} {% ckan_extends %}
{% block groups_search_form %} {% block groups_search_form %}
{# This is the same as the original BUT we also pass the 'include_children_option' parameter to the snippet #}
{% set facets = { {% set facets = {
'fields': c.fields_grouped, 'fields': c.fields_grouped,
'search': c.search_facets, 'search': c.search_facets,
......
{% ckan_extends %}
{# This is to fix a bad ckan template that caused this error when you view an organization in the Web UI:
"Exception: menu item `organization_activity` need parameter `offset`" when you view an organization.
This affected CKAN 2.4.0-2.4.2 only.
See:
* https://github.com/datagovuk/ckanext-hierarchy/issues/9
* https://github.com/ckan/ckan/pull/2640
#}
{% block breadcrumb_content %}
<li>{% link_for _('Organizations'), controller='organization', action='index' %}</li>
{% set parent_list = h.group_tree_parents(c.group_dict.name) %}
{% for parent_node in parent_list %}
<li>{% link_for parent_node.title|truncate(35), controller='organization', action='read', id=parent_node.name, suppress_active_class=true %}</li>
{% endfor %}
<li class="active">{% link_for c.group_dict.title|truncate(35), controller='organization', action='read', id=c.group_dict.name %}</li>
{% endblock %}
{% block content_primary_nav %}
{{ h.build_nav_icon('organization_read', _('Datasets'), id=c.group_dict.name) }}
{{ h.build_nav_icon('organization_activity', _('Activity Stream'), id=c.group_dict.name, offset=0) }}
{{ h.build_nav_icon('organization_about', _('About'), id=c.group_dict.name) }}
{% endblock %}
import nose.tools
from bs4 import BeautifulSoup
from ckan.tests import helpers, factories
from ckan.lib import search
from ckan.common import c
from ckan import model
eq = nose.tools.assert_equals
class TestOrgPage(helpers.FunctionalTestBase):
def test_search_parent_including_children(self):
parent_org, child_org, parent_dataset, child_dataset = \
create_fixtures()
app = self._get_test_app()
response = app.get(
url='/organization/parent_org?include_children=True')
search_results = scrape_search_results(response)
eq(search_results, set(('parent', 'child')))
def scrape_search_results(response):
soup = BeautifulSoup(response.body)
dataset_names = set()
for dataset_li in soup.find_all('li', class_='dataset-item'):
dataset_names.add(dataset_li.find('a')['href'].split('/')[-1])
return dataset_names
class TestSearchApi(object):
def setup(self):
helpers.reset_db()
def test_package_search_is_unaffected(self):
parent_org, child_org, parent_dataset, child_dataset = \
create_fixtures()
# package_search API is unaffected by ckanext-hierarchy (only searches
# via the front-end are affected)
package_search_result = helpers.call_action(
'package_search',
fq='owner_org:{}'.format(parent_org['id']))
search_results = \
[result['name'] for result in package_search_result['results']]
eq(set(search_results), set(('parent',)))
def create_fixtures():
parent_org = factories.Organization(name='parent_org', title='Parent')
child_org = factories.Organization(name='child_org', title='child')
member = model.Member(group=model.Group.get(child_org['id']), table_id=parent_org['id'], table_name='group', capacity='parent')
model.Session.add(member)
model.Session.commit()
parent_dataset = factories.Dataset(name='parent', title='Parent',
owner_org=parent_org['id'])
child_dataset = factories.Dataset(name='child', title='Child',
owner_org=child_org['id'])
decoy_dataset = factories.Dataset(name='decoy', title='Decoy')
return parent_org, child_org, parent_dataset, child_dataset
class LoadPluginsBase(object):
@classmethod
def setup_class(cls):
'''Load plugins'''
import ckan.plugins as p
try:
config['ckan.plugins'] = ' '.join(cls._load_plugins)
except AttributeError:
pass
cls._get_test_app()
@classmethod
def teardown_class(cls):
import ckan.plugins as p
for plugin in reversed(getattr(cls, '_load_plugins', [])):
p.unload(plugin)
[DEFAULT]
debug = false
smtp_server = localhost
error_email_from = paste@localhost
[server:main]
use = egg:Paste#http
host = 0.0.0.0
port = 5000
[app:main]
use = config:../ckan/test-core.ini
solr_url = http://127.0.0.1:8983/solr
# Insert any custom config settings to be used when running your extension's
# tests here.
ckan.plugins = hierarchy_display hierarchy_form
# Logging configuration
[loggers]
keys = root, ckan, ckanext_hierarchy, sqlalchemy
[handlers]
keys = console
[formatters]
keys = generic
[logger_root]
level = WARN
handlers = console
[logger_ckan]
qualname = ckan
handlers =
level = INFO
[logger_ckanext_hierarchy]
qualname = ckanext.hierarchy
handlers =
level = DEBUG
[logger_sqlalchemy]
handlers =
qualname = sqlalchemy.engine
level = WARN
[handler_console]
class = StreamHandler
args = (sys.stdout,)
level = NOTSET
formatter = generic
[formatter_generic]
format = %(asctime)s %(levelname)-5.5s [%(name)s] %(message)s
Supports Markdown
0% or .
You are about to add 0 people to the discussion. Proceed with caution.
Finish editing this message first!
Please register or to comment