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
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
organization. This new hierarchical arrangement of organizations is displayed
using templates in this extension, instead of the usual list. An organization
page also displays the section of the tree that it is part of, under the
'About' tab.
![Screenshot of organization edit page](screenshots/org_edit.png)
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):
* /organization/new
......@@ -51,13 +60,12 @@ the short name or acronym (more convenient for display).
* make the trees prettier with JSTree
## Compatibility
This extension requires CKAN v2.2 or later.
This extension requires CKAN v2.7 or later.
## Installation
import re
import ckan.plugins as p
import ckan.model as model
from ckan.common import request
......@@ -14,7 +16,7 @@ def group_tree(organizations=[], type_='organization'):
def group_tree_filter(organizations, group_tree_list, highlight=False):
# 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
def traverse_select_highlighted(group_tree, selection=[], highlight=False):
# add highlighted branches to the filtered tree
......@@ -89,4 +91,3 @@ def is_include_children_selected(fields):
if request.params.get('include_children'):
include_children_selected = True
return include_children_selected
import logging
import re
import ckan.plugins as p
from ckanext.hierarchy.logic import action
from ckanext.hierarchy import helpers
from ckan import model
from ckan.lib.plugins import DefaultOrganizationForm
from ckan.lib.plugins import DefaultGroupForm
import ckan.logic.schema as s
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__)
......@@ -69,25 +72,28 @@ class HierarchyDisplay(p.SingletonPlugin):
# 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
if not isinstance(c.fields, list):
return search_params
except TypeError:
return search_params
def _children_name_list(children):
name_list = []
for child in children:
name = child.get('name', "")
name_list += [name] + _children_name_list(child.get('children', []))
return name_list
# e.g. search_params['q'] = u' owner_org:"id" include_children: "True"'
query = search_params.get('q', None)
c.include_children_selected = False
# fix the issues with multiple times repeated fields
# remove the param from the fields
# Fix the issues with multiple times repeated fields
# Remove the param from the fields - NB no longer works
# e.g. [('include_children', 'True')]
new_fields = set()
for field,value in c.fields:
if (field != 'include_children'):
......@@ -95,37 +101,40 @@ class HierarchyDisplay(p.SingletonPlugin):
c.fields = list(new_fields)
# parse the query string to check if children are requested
if query:
base_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
# skip include children andset option value
if (field == 'include_children'):
if (value.upper() != "FALSE"):
c.include_children_selected = True
base_query += [item]
c.include_children_selected = query and \
'include_children: "True"' in query
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',[]))
search_params['q'] = " ".join(base_query)
if (len(search_params['q'].strip())>0):
search_params['q'] += ' AND '
search_params['q'] += '(organization:%s' % c.group_dict.get('name')
for name in children:
if name:
search_params['q'] += ' OR organization:%s' % name
search_params['q'] += ")"
# get a list of all the children organizations and include them in
# the search params
children_org_hierarchy = model.Group.get(c.group_dict.get('id')).\
children_names = [org[1] for org in children_org_hierarchy]
if children_names:
# remove existing owner_org:"<parent>" clause - we'll replace
# it with the tree of orgs in a moment
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(
for org_name in [c.group_dict.get('name')] +
search_params['q'] = query
# 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
{% ckan_extends %}
{% 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 = {
'fields': c.fields_grouped,
'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.
{% block breadcrumb_content %}
<li>{% link_for _('Organizations'), controller='organization', action='index' %}</li>
{% set parent_list = h.group_tree_parents( %}
{% for parent_node in parent_list %}
<li>{% link_for parent_node.title|truncate(35), controller='organization', action='read',, suppress_active_class=true %}</li>
{% endfor %}
<li class="active">{% link_for c.group_dict.title|truncate(35), controller='organization', action='read', %}</li>
{% endblock %}
{% block content_primary_nav %}
{{ h.build_nav_icon('organization_read', _('Datasets'), }}
{{ h.build_nav_icon('organization_activity', _('Activity Stream'),, offset=0) }}
{{ h.build_nav_icon('organization_about', _('About'), }}
{% endblock %}
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 =
class TestOrgPage(helpers.FunctionalTestBase):
def test_search_parent_including_children(self):
parent_org, child_org, parent_dataset, child_dataset = \
app = self._get_test_app()
response = app.get(
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'):
return dataset_names
class TestSearchApi(object):
def setup(self):
def test_package_search_is_unaffected(self):
parent_org, child_org, parent_dataset, child_dataset = \
# package_search API is unaffected by ckanext-hierarchy (only searches
# via the front-end are affected)
package_search_result = helpers.call_action(
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')
parent_dataset = factories.Dataset(name='parent', title='Parent',
child_dataset = factories.Dataset(name='child', title='Child',
decoy_dataset = factories.Dataset(name='decoy', title='Decoy')
return parent_org, child_org, parent_dataset, child_dataset
class LoadPluginsBase(object):
def setup_class(cls):
'''Load plugins'''
import ckan.plugins as p
config['ckan.plugins'] = ' '.join(cls._load_plugins)
except AttributeError:
def teardown_class(cls):
import ckan.plugins as p
for plugin in reversed(getattr(cls, '_load_plugins', [])):
debug = false
smtp_server = localhost
error_email_from = paste@localhost
use = egg:Paste#http
host =
port = 5000
use = config:../ckan/test-core.ini
solr_url =
# Insert any custom config settings to be used when running your extension's
# tests here.
ckan.plugins = hierarchy_display hierarchy_form
# Logging configuration
keys = root, ckan, ckanext_hierarchy, sqlalchemy
keys = console
keys = generic
level = WARN
handlers = console
qualname = ckan
handlers =
level = INFO
qualname = ckanext.hierarchy
handlers =
level = DEBUG
handlers =
qualname = sqlalchemy.engine
level = WARN
class = StreamHandler
args = (sys.stdout,)
level = NOTSET
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