[ckan-changes] [okfn/ckan] f4aa0e: pep8 fixes for plugins

GitHub noreply at github.com
Mon Apr 30 11:56:31 UTC 2012


  Branch: refs/heads/master
  Home:   https://github.com/okfn/ckan
  Commit: f4aa0e8df4adcd38b32730317c935cfcb1121f4b
      https://github.com/okfn/ckan/commit/f4aa0e8df4adcd38b32730317c935cfcb1121f4b
  Author: Toby <toby.junk at gmail.com>
  Date:   2012-04-26 (Thu, 26 Apr 2012)

  Changed paths:
    M ckan/plugins/core.py
    M ckan/plugins/interfaces.py
    M ckan/plugins/toolkit.py

  Log Message:
  -----------
  pep8 fixes for plugins


diff --git a/ckan/plugins/core.py b/ckan/plugins/core.py
index 5260164..1fee98a 100644
--- a/ckan/plugins/core.py
+++ b/ckan/plugins/core.py
@@ -6,11 +6,10 @@
 from inspect import isclass
 from itertools import chain
 from pkg_resources import iter_entry_points
-from pyutilib.component.core import PluginGlobals, ExtensionPoint as PluginImplementations, implements
+from pyutilib.component.core import PluginGlobals, implements
+from pyutilib.component.core import ExtensionPoint as PluginImplementations
 from pyutilib.component.core import SingletonPlugin as _pca_SingletonPlugin
 from pyutilib.component.core import Plugin as _pca_Plugin
-from pyutilib.component.core import PluginEnvironment
-from sqlalchemy.orm.interfaces import MapperExtension
 
 from ckan.plugins.interfaces import IPluginObserver
 
@@ -23,18 +22,20 @@
 
 log = logging.getLogger(__name__)
 
-# Entry point group. 
+# Entry point group.
 PLUGINS_ENTRY_POINT_GROUP = "ckan.plugins"
 
 # Entry point group for system plugins (those that are part of core ckan and do
 # not need to be explicitly enabled by the user)
 SYSTEM_PLUGINS_ENTRY_POINT_GROUP = "ckan.system_plugins"
 
+
 class PluginNotFoundException(Exception):
     """
     Raised when a requested plugin cannot be found.
     """
 
+
 class Plugin(_pca_Plugin):
     """
     Base class for plugins which require multiple instances.
@@ -43,6 +44,7 @@ class Plugin(_pca_Plugin):
     probably use SingletonPlugin.
     """
 
+
 class SingletonPlugin(_pca_SingletonPlugin):
     """
     Base class for plugins which are singletons (ie most of them)
@@ -52,6 +54,7 @@ class SingletonPlugin(_pca_SingletonPlugin):
     same singleton instance.
     """
 
+
 def _get_service(plugin):
     """
     Return a service (ie an instance of a plugin class).
@@ -100,6 +103,7 @@ def load_all(config):
     for plugin in plugins:
         load(plugin)
 
+
 def reset():
     """
     Clear and reload all configured plugins
@@ -107,6 +111,7 @@ def reset():
     from pylons import config
     load_all(config)
 
+
 def load(plugin):
     """
     Load a single plugin, given a plugin name, class or instance
@@ -120,6 +125,7 @@ def load(plugin):
         observer_plugin.after_load(service)
     return service
 
+
 def unload_all():
     """
     Unload (deactivate) all loaded plugins
@@ -128,6 +134,7 @@ def unload_all():
         for service in env.services.copy():
             unload(service)
 
+
 def unload(plugin):
     """
     Unload a single plugin, given a plugin name, class or instance
@@ -144,6 +151,7 @@ def unload(plugin):
 
     return service
 
+
 def find_user_plugins(config):
     """
     Return all plugins specified by the user in the 'ckan.plugins' config
@@ -159,10 +167,11 @@ def find_user_plugins(config):
         plugins.extend(ep.load() for ep in entry_points)
     return plugins
 
+
 def find_system_plugins():
     """
     Return all plugins in the ckan.system_plugins entry point group.
-    
+
     These are essential for operation and therefore cannot be enabled/disabled
     through the configuration file.
     """
@@ -170,4 +179,3 @@ def find_system_plugins():
         ep.load()
         for ep in iter_entry_points(group=SYSTEM_PLUGINS_ENTRY_POINT_GROUP)
     )
-
diff --git a/ckan/plugins/interfaces.py b/ckan/plugins/interfaces.py
index bc2eb88..4ca74f2 100644
--- a/ckan/plugins/interfaces.py
+++ b/ckan/plugins/interfaces.py
@@ -21,6 +21,7 @@
 from inspect import isclass
 from pyutilib.component.core import Interface as _pca_Interface
 
+
 class Interface(_pca_Interface):
 
     @classmethod
@@ -80,13 +81,15 @@ def before_map(self, map):
 
     def after_map(self, map):
         """
-        Called after routes map is set up. ``after_map`` can be used to add fall-back handlers.
+        Called after routes map is set up. ``after_map`` can be used to
+        add fall-back handlers.
 
         :param map: Routes map object
         :returns: Modified version of the map object
         """
         return map
 
+
 class IMapper(Interface):
     """
     A subset of the SQLAlchemy mapper extension hooks.
@@ -104,7 +107,8 @@ class IMapper(Interface):
 
     def before_insert(self, mapper, connection, instance):
         """
-        Receive an object instance before that instance is INSERTed into its table.
+        Receive an object instance before that instance is INSERTed into
+        its table.
         """
 
     def before_update(self, mapper, connection, instance):
@@ -132,6 +136,7 @@ def after_delete(self, mapper, connection, instance):
         Receive an object instance after that instance is DELETEed.
         """
 
+
 class ISession(Interface):
     """
     A subset of the SQLAlchemy session extension hooks.
@@ -167,6 +172,7 @@ def after_rollback(self, session):
         Execute after a rollback has occured.
         """
 
+
 class IDomainObjectModification(Interface):
     """
     Receives notification of new, changed and deleted datesets.
@@ -175,6 +181,7 @@ class IDomainObjectModification(Interface):
     def notify(self, entity, operation):
         pass
 
+
 class IResourceUrlChange(Interface):
     """
     Receives notification of changed urls.
@@ -183,6 +190,7 @@ class IResourceUrlChange(Interface):
     def notify(self, resource):
         pass
 
+
 class ITagController(Interface):
     '''
     Hook into the Tag controller. These will usually be called just before
@@ -198,6 +206,7 @@ def before_view(self, tag_dict):
         '''
         return tag_dict
 
+
 class IGroupController(Interface):
     """
     Hook into the Group controller. These will
@@ -226,11 +235,13 @@ def delete(self, entity):
 
     def before_view(self, pkg_dict):
         '''
-             Extensions will recieve this before the group gets displayed. The dictionary
-             passed will be the one that gets sent to the template.
+             Extensions will recieve this before the group gets
+             displayed. The dictionary passed will be the one that gets
+             sent to the template.
         '''
         return pkg_dict
 
+
 class IPackageController(Interface):
     """
     Hook into the package controller.
@@ -288,17 +299,19 @@ def after_search(self, search_results, search_params):
 
     def before_index(self, pkg_dict):
         '''
-             Extensions will receive what will be given to the solr for indexing.
-             This is essentially a flattened dict (except for multlivlaued fields such as tags
-             of all the terms sent to the indexer.  The extension can modify this by returning
-             an altered version.
+             Extensions will receive what will be given to the solr for
+             indexing. This is essentially a flattened dict (except for
+             multli-valued fields such as tags) of all the terms sent to
+             the indexer. The extension can modify this by returning an
+             altered version.
         '''
         return pkg_dict
 
     def before_view(self, pkg_dict):
         '''
-             Extensions will recieve this before the dataset gets displayed. The dictionary
-             passed will be the one that gets sent to the template.
+             Extensions will recieve this before the dataset gets
+             displayed. The dictionary passed will be the one that gets
+             sent to the template.
         '''
         return pkg_dict
 
@@ -332,6 +345,7 @@ def after_unload(self, service):
         This method is passed the instantiated service object.
         """
 
+
 class IConfigurable(Interface):
     """
     Pass configuration to plugins and extensions
@@ -342,6 +356,7 @@ def configure(self, config):
         Called by load_environment
         """
 
+
 class IConfigurer(Interface):
     """
     Configure CKAN (pylons) environment via the ``pylons.config`` object
@@ -382,6 +397,7 @@ def is_authorized(self, username, action, domain_obj):
         other Authorizers to run; True will shortcircuit and return.
         """
 
+
 class IActions(Interface):
     """
     Allow adding of actions to the logic layer.
@@ -392,6 +408,7 @@ def get_actions(self):
         function and the values being the functions themselves.
         """
 
+
 class IAuthFunctions(Interface):
     """
     Allow customisation of default Authorization implementation
@@ -402,6 +419,7 @@ def get_auth_functions(self):
         implementation overrides
         """
 
+
 class ITemplateHelpers(Interface):
     """
     Allow adding extra template functions available via h variable
@@ -412,6 +430,7 @@ def get_helpers(self):
         function and the values being the functions themselves.
         """
 
+
 class IDatasetForm(Interface):
     """
     Allows customisation of the package controller as a plugin.
@@ -499,7 +518,6 @@ def history_template(self):
         rendered for the history page
         """
 
-
     def package_form(self):
         """
         Returns a string representing the location of the template to be
@@ -616,8 +634,6 @@ def history_template(self):
         rendered for the history page
         """
 
-
-
     def package_form(self):
         """
         Returns a string representing the location of the template to be
@@ -649,4 +665,3 @@ def setup_template_variables(self, context, data_dict):
         """
 
     ##### End of hooks                                                   #####
-
diff --git a/ckan/plugins/toolkit.py b/ckan/plugins/toolkit.py
index fa1f2c3..120c0ac 100644
--- a/ckan/plugins/toolkit.py
+++ b/ckan/plugins/toolkit.py
@@ -8,10 +8,12 @@
 
 __all__ = ['toolkit']
 
+
 class CkanVersionException(Exception):
     ''' Exception raised if required ckan version is not available. '''
     pass
 
+
 class _Toolkit(object):
     '''This class is intended to make functions/objects consistently
     available to plugins, whilst giving developers the ability move
@@ -40,7 +42,8 @@ class _Toolkit(object):
         'literal',              # stop tags in a string being escaped
         'get_action',           # get logic action function
         'check_access',         # check logic function authorisation
-        'ObjectNotFound',       # action not found exception (ckan.logic.NotFound)
+        'ObjectNotFound',       # action not found exception
+                                # (ckan.logic.NotFound)
         'NotAuthorized',        # action not authorized exception
         'ValidationError',      # model update validation error
         'CkanCommand',          # class for providing cli interfaces
@@ -53,7 +56,6 @@ class _Toolkit(object):
         'CkanVersionException',
     ]
 
-
     def __init__(self):
         self._toolkit = {}
 
@@ -85,7 +87,7 @@ def _initialize(self):
 
         t['get_action'] = logic.get_action
         t['check_access'] = logic.check_access
-        t['ObjectNotFound'] = logic.NotFound  ## Name change intentional
+        t['ObjectNotFound'] = logic.NotFound  # Name change intentional
         t['NotAuthorized'] = logic.NotAuthorized
         t['ValidationError'] = logic.ValidationError
 
@@ -117,7 +119,8 @@ def _render_snippet(cls, template, data=None):
     def _add_template_directory(cls, config, relative_path):
         ''' Function to aid adding extra template paths to the config.
         The path is relative to the file calling this function. '''
-        cls._add_served_directory(config, relative_path, 'extra_template_paths')
+        cls._add_served_directory(config, relative_path,
+                                  'extra_template_paths')
 
     @classmethod
     def _add_public_directory(cls, config, relative_path):


================================================================
  Commit: 38f1b5610fe48be60da14b90a00622510314264f
      https://github.com/okfn/ckan/commit/38f1b5610fe48be60da14b90a00622510314264f
  Author: Toby <toby.junk at gmail.com>
  Date:   2012-04-26 (Thu, 26 Apr 2012)

  Changed paths:
    M ckan/__init__.py
    M ckan/config/solr/CHANGELOG.txt
    M ckan/config/solr/schema-1.4.xml
    M ckan/controllers/package.py
    M ckan/lib/alphabet_paginate.py
    M ckan/lib/search/__init__.py
    M ckan/lib/search/index.py
    M ckan/logic/auth/delete.py
    M ckan/logic/auth/publisher/delete.py
    M ckan/public/css/style.css
    M ckan/public/scripts/vendor/recline/css/graph.css
    M ckan/public/scripts/vendor/recline/recline.js
    M ckan/templates/_util.html
    M ckan/templates/package/related_list.html
    M ckan/templates/tag/index.html
    M ckan/templates/user/list.html
    M ckan/templates/user/login.html
    M ckan/templates/user/logout.html
    M ckan/tests/functional/test_search.py
    M ckan/tests/functional/test_user.py
    M ckan/tests/lib/test_alphabet_pagination.py
    M ckan/tests/lib/test_solr_package_search.py
    M ckanext/multilingual/solr/schema.xml
    M doc/index.rst
    M doc/solr-setup.rst
    M doc/using-data-api.rst
    M pip-requirements.txt

  Log Message:
  -----------
  Merge branch 'master' of github.com:okfn/ckan


diff --git a/ckan/__init__.py b/ckan/__init__.py
index b433319..b1b1544 100644
--- a/ckan/__init__.py
+++ b/ckan/__init__.py
@@ -1,4 +1,4 @@
-__version__ = '1.6.1b'
+__version__ = '1.8a'
 __description__ = 'Comprehensive Knowledge Archive Network (CKAN) Software'
 __long_description__ = \
 '''CKAN software provides a hub for datasets. The flagship site running CKAN 
diff --git a/ckan/config/solr/CHANGELOG.txt b/ckan/config/solr/CHANGELOG.txt
index 5fe664f..1e4e67f 100644
--- a/ckan/config/solr/CHANGELOG.txt
+++ b/ckan/config/solr/CHANGELOG.txt
@@ -1,6 +1,14 @@
 CKAN SOLR schemas changelog
 ===========================
 
+v1.4 - (ckan>=1.7)
+--------------------
+* Add Ascii folding filter to text fields.
+* Add capacity field for public, private access.
+* Add title_string so you can sort alphabetically on title.
+* Fields related to analytics, access and view counts.
+* Add data_dict field for the whole package_dict.
+
 v1.3 - (ckan>=1.5.1)
 --------------------
 * Use the index_id (hash of dataset id + site_id) as uniqueKey (#1430)
diff --git a/ckan/config/solr/schema-1.4.xml b/ckan/config/solr/schema-1.4.xml
index 29cb473..0409e71 100644
--- a/ckan/config/solr/schema-1.4.xml
+++ b/ckan/config/solr/schema-1.4.xml
@@ -51,6 +51,7 @@
             <filter class="solr.WordDelimiterFilterFactory" generateWordParts="1" generateNumberParts="1" catenateWords="1" catenateNumbers="1" catenateAll="0" splitOnCaseChange="1"/>
             <filter class="solr.LowerCaseFilterFactory"/>
             <filter class="solr.SnowballPorterFilterFactory" language="English" protected="protwords.txt"/>
+            <filter class="solr.ASCIIFoldingFilterFactory"/>
         </analyzer>
         <analyzer type="query">
             <tokenizer class="solr.WhitespaceTokenizerFactory"/>
@@ -63,6 +64,7 @@
             <filter class="solr.WordDelimiterFilterFactory" generateWordParts="1" generateNumberParts="1" catenateWords="0" catenateNumbers="0" catenateAll="0" splitOnCaseChange="1"/>
             <filter class="solr.LowerCaseFilterFactory"/>
             <filter class="solr.SnowballPorterFilterFactory" language="English" protected="protwords.txt"/>
+            <filter class="solr.ASCIIFoldingFilterFactory"/>
         </analyzer>
     </fieldType>
 
@@ -115,6 +117,8 @@
     <field name="tags" type="string" indexed="true" stored="true" multiValued="true"/>
     <field name="groups" type="string" indexed="true" stored="true" multiValued="true"/>
 
+    <field name="capacity" type="string" indexed="true" stored="true" multiValued="false"/>
+
     <field name="res_description" type="textgen" indexed="true" stored="true" multiValued="true"/>
     <field name="res_format" type="string" indexed="true" stored="true" multiValued="true"/>
     <field name="res_url" type="string" indexed="true" stored="true" multiValued="true"/>
@@ -134,8 +138,8 @@
     <field name="parent_of" type="text" indexed="true" stored="false" multiValued="true"/>
     <field name="views_total" type="int" indexed="true" stored="false"/>
     <field name="views_recent" type="int" indexed="true" stored="false"/>
-    <field name="recources_accessed_total" type="int" indexed="true" stored="false"/>
-    <field name="recources_accessed_recent" type="int" indexed="true" stored="false"/>
+    <field name="resources_accessed_total" type="int" indexed="true" stored="false"/>
+    <field name="resources_accessed_recent" type="int" indexed="true" stored="false"/>
 
     <field name="metadata_created" type="date" indexed="true" stored="true" multiValued="false"/>
     <field name="metadata_modified" type="date" indexed="true" stored="true" multiValued="false"/>
@@ -144,8 +148,9 @@
 
     <!-- Copy the title field into titleString, and treat as a string
          (rather than text type).  This allows us to sort on the titleString -->
-    <field name="titleString" type="string" indexed="true" stored="false" />
-    <copyField source="title" dest="titleString"/>
+    <field name="title_string" type="string" indexed="true" stored="false" />
+
+    <field name="data_dict" type="string" indexed="false" stored="true" />
 
     <dynamicField name="extras_*" type="text" indexed="true" stored="true" multiValued="false"/>
     <dynamicField name="*" type="string" indexed="true"  stored="false"/>
diff --git a/ckan/controllers/package.py b/ckan/controllers/package.py
index ba12b86..9e99e0f 100644
--- a/ckan/controllers/package.py
+++ b/ckan/controllers/package.py
@@ -480,6 +480,7 @@ def edit(self, id, data=None, errors=None, error_summary=None):
         c.errors_json = json.dumps(errors)
 
         self._setup_template_variables(context, {'id': id}, package_type=package_type)
+        c.related_count = len(c.pkg.related)
 
         # TODO: This check is to maintain backwards compatibility with the old way of creating
         # custom forms. This behaviour is now deprecated.
@@ -749,6 +750,8 @@ def resource_read(self, id, resource_id):
             c.package['isopen'] = False
         c.datastore_api = h.url_for('datastore_read', id=c.resource.get('id'),
                 qualified=True)
+
+        c.related_count = len(c.pkg.related)
         return render('package/resource_read.html')
 
     def resource_embedded_dataviewer(self, id, resource_id):
diff --git a/ckan/lib/alphabet_paginate.py b/ckan/lib/alphabet_paginate.py
index 0afa88f..b75bf27 100644
--- a/ckan/lib/alphabet_paginate.py
+++ b/ckan/lib/alphabet_paginate.py
@@ -1,6 +1,7 @@
 '''
-Based on webhelpers.paginator, but each page is for items beginning
- with a particular letter.
+Based on webhelpers.paginator, but:
+ * each page is for items beginning with a particular letter
+ * output is suitable for Bootstrap
 
  Example:
         c.page = h.Page(
@@ -43,7 +44,12 @@ def __init__(self, collection, alpha_attribute, page, other_text, paging_thresho
         self.other_text = other_text
         self.paging_threshold = paging_threshold
         self.controller_name = controller_name
-        self.available = dict( (c,0,) for c in "ABCDEFGHIJKLMNOPQRSTUVWXYZ" )
+
+        self.letters = [char for char in 'ABCDEFGHIJKLMNOPQRSTUVWXYZ'] + [self.other_text]
+        
+        # Work out which alphabet letters are 'available' i.e. have some results
+        # because we grey-out those which aren't.
+        self.available = dict( (c,0,) for c in self.letters )
         for c in self.collection:
             if isinstance(c, unicode):
                 x = c[0]
@@ -51,35 +57,42 @@ def __init__(self, collection, alpha_attribute, page, other_text, paging_thresho
                 x = c[self.alpha_attribute][0]
             else:
                 x = getattr(c, self.alpha_attribute)[0]
+            x = x.upper()
+            if x not in self.letters:
+                x = self.other_text
             self.available[x] = self.available.get(x, 0) + 1
 
     def pager(self, q=None):
         '''Returns pager html - for navigating between the pages.
            e.g. Something like this:
-             <div class='pager'>
-                 <span class="pager_curpage">A</span>
-                 <a class="pager_link" href="/package/list?page=B">B</a>
-                 <a class="pager_link" href="/package/list?page=C">C</a>
+             <ul class='pagination pagination-alphabet'>
+                 <li class="active"><a href="/package/list?page=A">A</a></li>
+                 <li><a href="/package/list?page=B">B</a></li>
+                 <li><a href="/package/list?page=C">C</a></li>
                     ...
-                 <a class="pager_link" href="/package/list?page=Z">Z</a
-                 <a class="pager_link" href="/package/list?page=Other">Other</a
-             </div>
+                 <li class="disabled"><a href="/package/list?page=Z">Z</a></li>
+                 <li><a href="/package/list?page=Other">Other</a></li>
+             </ul>
         '''
         if self.item_count < self.paging_threshold:
             return ''
         pages = []
         page = q or self.page
-        letters = [char for char in 'ABCDEFGHIJKLMNOPQRSTUVWXYZ'] + [self.other_text]
-        for letter in letters:
+        for letter in self.letters:
+            href = url_for(controller=self.controller_name, action='index', page=letter)
+            link = HTML.a(href=href, c=letter)
             if letter != page:
                 if self.available.get(letter, 0):
-                    page_element = HTML.a(class_='pager_link', href=url_for(controller=self.controller_name, action='index', page=letter),c=letter)
+                    li_class = ''
                 else:
-                    page_element = HTML.span(class_="pager_empty", c=letter)
+                    li_class = 'disabled'
             else:
-                page_element = HTML.span(class_='pager_curpage', c=letter)
+                li_class = 'active'
+            attributes = {'class_': li_class} if li_class else {}
+            page_element = HTML.li(link, **attributes)
             pages.append(page_element)
-        div = HTML.tag('div', class_='pager', *pages)
+        ul = HTML.tag('ul', *pages)
+        div = HTML.div(ul, class_='pagination pagination-alphabet')
         return div
 
 
diff --git a/ckan/lib/search/__init__.py b/ckan/lib/search/__init__.py
index fbb924a..b2774fc 100644
--- a/ckan/lib/search/__init__.py
+++ b/ckan/lib/search/__init__.py
@@ -26,7 +26,7 @@ def text_traceback():
 
 SIMPLE_SEARCH = config.get('ckan.simple_search', False)
 
-SUPPORTED_SCHEMA_VERSIONS = ['1.3']
+SUPPORTED_SCHEMA_VERSIONS = ['1.4']
 
 DEFAULT_OPTIONS = {
     'limit': 20,
diff --git a/ckan/lib/search/index.py b/ckan/lib/search/index.py
index 086a39e..992721f 100644
--- a/ckan/lib/search/index.py
+++ b/ckan/lib/search/index.py
@@ -99,6 +99,11 @@ def index_package(self, pkg_dict):
         if pkg_dict is None:
             return
 
+        # add to string field for sorting
+        title = pkg_dict.get('title')
+        if title:
+            pkg_dict['title_string'] = title
+
         if (not pkg_dict.get('state')) or ('active' not in pkg_dict.get('state')):
             return self.delete_package(pkg_dict)
 
@@ -163,7 +168,7 @@ def index_package(self, pkg_dict):
 
         pkg_dict = dict([(k.encode('ascii', 'ignore'), v) for (k, v) in pkg_dict.items()])
 
-        for k in ('title','notes'):
+        for k in ('title', 'notes', 'title_string'):
             if k in pkg_dict and pkg_dict[k]:
                 pkg_dict[k] = escape_xml_illegal_chars(pkg_dict[k])
 
diff --git a/ckan/logic/auth/delete.py b/ckan/logic/auth/delete.py
index c99bec3..c324049 100644
--- a/ckan/logic/auth/delete.py
+++ b/ckan/logic/auth/delete.py
@@ -27,6 +27,15 @@ def related_delete(context, data_dict):
 
     related = get_related_object(context, data_dict)
     userobj = model.User.get( user )
+
+    if related.datasets:
+        package = related.datasets[0]
+
+        pkg_dict = { 'id': package.id }
+        authorized = package_delete(context, pkg_dict).get('success')
+        if authorized:
+            return {'success': True}
+
     if not userobj or userobj.id != related.owner_id:
         return {'success': False, 'msg': _('Only the owner can delete a related item')}
 
diff --git a/ckan/logic/auth/publisher/delete.py b/ckan/logic/auth/publisher/delete.py
index 9d3388f..0aaa0d9 100644
--- a/ckan/logic/auth/publisher/delete.py
+++ b/ckan/logic/auth/publisher/delete.py
@@ -40,6 +40,12 @@ def related_delete(context, data_dict):
 
     related = get_related_object(context, data_dict)
     userobj = model.User.get( user )
+
+    if related.datasets:
+        package = related.datasets[0]
+        if _groups_intersect( userobj.get_groups('organization'), package.get_groups('organization') ):
+            return {'success': True}
+
     if not userobj or userobj.id != related.owner_id:
         return {'success': False, 'msg': _('Only the owner can delete a related item')}
 
diff --git a/ckan/public/css/style.css b/ckan/public/css/style.css
index 7e9e609..283f5d3 100644
--- a/ckan/public/css/style.css
+++ b/ckan/public/css/style.css
@@ -379,28 +379,8 @@ ul.no-break li {
 /* ============== */
 /* = Pagination = */
 /* ============== */
-.pager {
-	width: 100%;
-	text-align: center;
-	margin: 0 0 1.2em 0;
-	clear: both;
-}
-.pager span, .pager a {
-	text-decoration: none;
-	margin: 0em;
-	border: none;
-	padding: 0.3em 0.1em;
-}
-.pager a:hover, .pager a:active {
-	color: #fff;
-	background-color: #c22;
-}
-.pager span.pager_dotdot {
-	color: #aaa;
-}
-.pager span.pager_curpage {
-	font-weight: bold;
-	border: 1px solid #ddd;
+.pagination-alphabet a { 
+        padding: 0 6px;
 }
 
 /* ====== */
diff --git a/ckan/public/scripts/vendor/recline/css/graph.css b/ckan/public/scripts/vendor/recline/css/graph.css
index 88acf5f..413ac14 100644
--- a/ckan/public/scripts/vendor/recline/css/graph.css
+++ b/ckan/public/scripts/vendor/recline/css/graph.css
@@ -13,6 +13,11 @@
   line-height: 13px;
 }
 
+.recline-graph .graph .alert {
+  width: 450px;
+  margin: auto;
+}
+
 /**********************************************************
   * Editor
   *********************************************************/
diff --git a/ckan/public/scripts/vendor/recline/recline.js b/ckan/public/scripts/vendor/recline/recline.js
index a4c01ca..271e9c5 100644
--- a/ckan/public/scripts/vendor/recline/recline.js
+++ b/ckan/public/scripts/vendor/recline/recline.js
@@ -757,22 +757,13 @@ my.Graph = Backbone.View.extend({
         <label>Group Column (x-axis)</label> \
         <div class="input editor-group"> \
           <select> \
+          <option value="">Please choose ...</option> \
           {{#fields}} \
           <option value="{{id}}">{{label}}</option> \
           {{/fields}} \
           </select> \
         </div> \
         <div class="editor-series-group"> \
-          <div class="editor-series"> \
-            <label>Series <span>A (y-axis)</span></label> \
-            <div class="input"> \
-              <select> \
-              {{#fields}} \
-              <option value="{{id}}">{{label}}</option> \
-              {{/fields}} \
-              </select> \
-            </div> \
-          </div> \
         </div> \
       </div> \
       <div class="editor-buttons"> \
@@ -784,13 +775,34 @@ my.Graph = Backbone.View.extend({
       </div> \
     </form> \
   </div> \
-  <div class="panel graph"></div> \
+  <div class="panel graph"> \
+    <div class="js-temp-notice alert alert-block"> \
+      <h3 class="alert-heading">Hey there!</h3> \
+      <p>There\'s no graph here yet because we don\'t know what fields you\'d like to see plotted.</p> \
+      <p>Please tell us by <strong>using the menu on the right</strong> and a graph will automatically appear.</p> \
+    </div> \
+  </div> \
 </div> \
 ',
+  templateSeriesEditor: ' \
+    <div class="editor-series js-series-{{seriesIndex}}"> \
+      <label>Series <span>{{seriesName}} (y-axis)</span> \
+        [<a href="#remove" class="action-remove-series">Remove</a>] \
+      </label> \
+      <div class="input"> \
+        <select> \
+        <option value="">Please choose ...</option> \
+        {{#fields}} \
+        <option value="{{id}}">{{label}}</option> \
+        {{/fields}} \
+        </select> \
+      </div> \
+    </div> \
+  ',
 
   events: {
     'change form select': 'onEditorSubmit',
-    'click .editor-add': 'addSeries',
+    'click .editor-add': '_onAddSeries',
     'click .action-remove-series': 'removeSeries',
     'click .action-toggle-help': 'toggleHelp'
   },
@@ -807,7 +819,8 @@ my.Graph = Backbone.View.extend({
     this.model.currentDocuments.bind('reset', this.redraw);
     var stateData = _.extend({
         group: null,
-        series: [],
+        // so that at least one series chooser box shows up
+        series: [""],
         graphType: 'lines-and-points'
       },
       options.state
@@ -817,21 +830,45 @@ my.Graph = Backbone.View.extend({
   },
 
   render: function() {
-    htmls = $.mustache(this.template, this.model.toTemplateJSON());
+    var self = this;
+    var tmplData = this.model.toTemplateJSON();
+    var htmls = $.mustache(this.template, tmplData);
     $(this.el).html(htmls);
-    // now set a load of stuff up
     this.$graph = this.el.find('.panel.graph');
-    // for use later when adding additional series
-    // could be simpler just to have a common template!
-    this.$seriesClone = this.el.find('.editor-series').clone();
-    this._updateSeries();
+
+    // set up editor from state
+    if (this.state.get('graphType')) {
+      this._selectOption('.editor-type', this.state.get('graphType'));
+    }
+    if (this.state.get('group')) {
+      this._selectOption('.editor-group', this.state.get('group'));
+    }
+    _.each(this.state.get('series'), function(series, idx) {
+      self.addSeries(idx);
+      self._selectOption('.editor-series.js-series-' + idx, series);
+    });
     return this;
   },
 
+  // Private: Helper function to select an option from a select list
+  //
+  _selectOption: function(id,value){
+    var options = this.el.find(id + ' select > option');
+    if (options) {
+      options.each(function(opt){
+        if (this.value == value) {
+          $(this).attr('selected','selected');
+          return false;
+        }
+      });
+    }
+  },
+
   onEditorSubmit: function(e) {
     var select = this.el.find('.editor-group select');
-    $editor = this;
-    var series = this.$series.map(function () {
+    var $editor = this;
+    var $series  = this.el.find('.editor-series select');
+    var series = $series.map(function () {
       return $(this).val();
     });
     var updatedState = {
@@ -870,10 +907,20 @@ my.Graph = Backbone.View.extend({
 //    }
   },
 
+  // ### getGraphOptions
+  //
+  // Get options for Flot Graph
+  //
   // needs to be function as can depend on state
+  //
+  // @param typeId graphType id (lines, lines-and-points etc)
   getGraphOptions: function(typeId) { 
     var self = this;
     // special tickformatter to show labels rather than numbers
+    // TODO: we should really use tickFormatter and 1 interval ticks if (and
+    // only if) x-axis values are non-numeric
+    // However, that is non-trivial to work out from a dataset (datasets may
+    // have no field type info). Thus at present we only do this for bars.
     var tickFormatter = function (val) {
       if (self.model.currentDocuments.models[val]) {
         var out = self.model.currentDocuments.models[val].get(self.state.attributes.group);
@@ -886,20 +933,25 @@ my.Graph = Backbone.View.extend({
       }
       return val;
     };
-    // TODO: we should really use tickFormatter and 1 interval ticks if (and
-    // only if) x-axis values are non-numeric
-    // However, that is non-trivial to work out from a dataset (datasets may
-    // have no field type info). Thus at present we only do this for bars.
-    var options = { 
+
+    var xaxis = {};
+    // check for time series on x-axis
+    if (this.model.fields.get(this.state.get('group')).get('type') === 'date') {
+      xaxis.mode = 'time';
+      xaxis.timeformat = '%y-%b';
+    }
+    var optionsPerGraphType = { 
       lines: {
-         series: { 
-           lines: { show: true }
-         }
+        series: { 
+          lines: { show: true }
+        },
+        xaxis: xaxis
       },
       points: {
         series: {
           points: { show: true }
         },
+        xaxis: xaxis,
         grid: { hoverable: true, clickable: true }
       },
       'lines-and-points': {
@@ -907,6 +959,7 @@ my.Graph = Backbone.View.extend({
           points: { show: true },
           lines: { show: true }
         },
+        xaxis: xaxis,
         grid: { hoverable: true, clickable: true }
       },
       bars: {
@@ -930,7 +983,7 @@ my.Graph = Backbone.View.extend({
         }
       }
     };
-    return options[typeId];
+    return optionsPerGraphType[typeId];
   },
 
   setupTooltips: function() {
@@ -987,8 +1040,15 @@ my.Graph = Backbone.View.extend({
     _.each(this.state.attributes.series, function(field) {
       var points = [];
       _.each(self.model.currentDocuments.models, function(doc, index) {
-        var x = doc.get(self.state.attributes.group);
-        var y = doc.get(field);
+        var xfield = self.model.fields.get(self.state.attributes.group);
+        var x = doc.getFieldValue(xfield);
+        // time series
+        var isDateTime = xfield.get('type') === 'date';
+        if (isDateTime) {
+          x = new Date(x);
+        }
+        var yfield = self.model.fields.get(field);
+        var y = doc.getFieldValue(yfield);
         if (typeof x === 'string') {
           x = index;
         }
@@ -1006,23 +1066,25 @@ my.Graph = Backbone.View.extend({
 
   // Public: Adds a new empty series select box to the editor.
   //
-  // All but the first select box will have a remove button that allows them
-  // to be removed.
+  // @param [int] idx index of this series in the list of series
   //
   // Returns itself.
-  addSeries: function (e) {
-    e.preventDefault();
-    var element = this.$seriesClone.clone(),
-        label   = element.find('label'),
-        index   = this.$series.length;
-
-    this.el.find('.editor-series-group').append(element);
-    this._updateSeries();
-    label.append(' [<a href="#remove" class="action-remove-series">Remove</a>]');
-    label.find('span').text(String.fromCharCode(this.$series.length + 64));
+  addSeries: function (idx) {
+    var data = _.extend({
+      seriesIndex: idx,
+      seriesName: String.fromCharCode(idx + 64 + 1),
+    }, this.model.toTemplateJSON());
+
+    var htmls = $.mustache(this.templateSeriesEditor, data);
+    this.el.find('.editor-series-group').append(htmls);
     return this;
   },
 
+  _onAddSeries: function(e) {
+    e.preventDefault();
+    this.addSeries(this.state.get('series').length);
+  },
+
   // Public: Removes a series list item from the editor.
   //
   // Also updates the labels of the remaining series elements.
@@ -1030,26 +1092,12 @@ my.Graph = Backbone.View.extend({
     e.preventDefault();
     var $el = $(e.target);
     $el.parent().parent().remove();
-    this._updateSeries();
-    this.$series.each(function (index) {
-      if (index > 0) {
-        var labelSpan = $(this).prev().find('span');
-        labelSpan.text(String.fromCharCode(index + 65));
-      }
-    });
     this.onEditorSubmit();
   },
 
   toggleHelp: function() {
     this.el.find('.editor-info').toggleClass('editor-hide-info');
   },
-
-  // Private: Resets the series property to reference the select elements.
-  //
-  // Returns itself.
-  _updateSeries: function () {
-    this.$series  = this.el.find('.editor-series select');
-  }
 });
 
 })(jQuery, recline.View);
diff --git a/ckan/templates/_util.html b/ckan/templates/_util.html
index 8e320d8..5aa7c07 100644
--- a/ckan/templates/_util.html
+++ b/ckan/templates/_util.html
@@ -128,6 +128,7 @@
   <py:def function="related_summary(related)">
     <li class="span3">
       <div class="thumbnail">
+        <button py:if="c.user and (c.userobj.id == related.owner_id or h.check_access('package_update',{'id':c.pkg.id}))" class="close" onclick="related_delete('${related.id}');">×</button>
         <a href="${related.url}" class="image">
           <img src="${related.image_url}" width="210" py:if="related.image_url" />
           <img src="/images/photo-placeholder.png" width="210" py:if="not related.image_url" />
diff --git a/ckan/templates/package/related_list.html b/ckan/templates/package/related_list.html
index 49a75ea..baced80 100644
--- a/ckan/templates/package/related_list.html
+++ b/ckan/templates/package/related_list.html
@@ -40,6 +40,25 @@
   </div>
 
   <py:def function="optional_head">
+    <script type="text/javascript" py:if="c.user">
+        function related_delete(related_id) {
+          var data = { 'id' : related_id }
+          $.ajax({
+            type: "post",
+            url: CKAN.SITE_URL + '/api/3/action/related_delete',
+            data: JSON.stringify(data),
+            success: function (data) {
+              window.location.reload();
+            },
+            error: function(err, txt, w) {
+              // This needs to be far more informative.
+              var msg = '<strong>Error:</strong> Unable to delete related item';
+              $('<div class="alert alert-error" />').html(msg).hide().prependTo($('div#main')).fadeIn();
+            }
+          });
+
+        }
+    </script>
     <py:if test="config.get('rdf_packages')">
       <link rel="alternate" type="application/rdf+xml" title="RDF/XML" href="${config['rdf_packages'] + '/' + c.pkg.id + '.rdf' }" />
       <link rel="alternate" type="application/turtle" title="RDF/Turtle" href="${config['rdf_packages'] + '/' + c.pkg.id + '.ttl' }" />
diff --git a/ckan/templates/tag/index.html b/ckan/templates/tag/index.html
index 17292e9..8c383c8 100644
--- a/ckan/templates/tag/index.html
+++ b/ckan/templates/tag/index.html
@@ -8,9 +8,9 @@
   <div py:match="content">
     <h2>Tags</h2>
 
-    <form id="tag-search" action="" method="GET">
+    <form class="form-inline" id="tag-search" action="" method="GET">
       <input type="text" id="q" name="q" value="${c.q}" />
-      <input type="submit" name="search" value="${_('Search')} »" />
+      <input class="btn btn-primary" type="submit" name="search" value="${_('Search')} »" />
     </form>
     
     <hr />
diff --git a/ckan/templates/user/list.html b/ckan/templates/user/list.html
index 3c736b4..1ef2655 100644
--- a/ckan/templates/user/list.html
+++ b/ckan/templates/user/list.html
@@ -8,9 +8,10 @@
   
   <py:match path="primarysidebar">
     <li class="widget-container widget_text">
-      <form id="user-search" class="user-search" action="" method="GET">
-        <input type="text" id="q" name="q" value="${c.q}" />
-        <input type="submit" class="btn btn-small" name="" value="${_('Search')} »" />
+      <h3>Search Users</h3>
+      <form id="user-search" class="form-inline user-search" action="" method="GET">
+        <input type="text" class="input-medium" id="q" name="q" value="${c.q}" />
+        <input type="submit" class="btn btn-small btn-primary" name="" value="${_('Search')} »" />
       </form>
       <p py:if="c.q" i18n:msg="item_count">
         <strong>${c.page.item_count}</strong> users found.
diff --git a/ckan/templates/user/login.html b/ckan/templates/user/login.html
index 6cabb81..9fdf3df 100644
--- a/ckan/templates/user/login.html
+++ b/ckan/templates/user/login.html
@@ -18,25 +18,39 @@
   
   <py:def function="page_title">Login - User</py:def>
   <py:def function="page_heading">Login to ${g.site_title}</py:def>
+  <py:def function="body_class">no-sidebar</py:def>
 
   <div py:match="content">
-    
-    <form action="${h.url_for('/login_generic')}" method="post" class="simple-form" id="login">  
+
+    <form action="${h.url_for('/login_generic')}" method="post" class="form-horizontal" id="login">  
       <fieldset>
         <!--legend i18n:msg="site_title">Login</legend-->
+        <div class="control-group">
+          <label class="control-label" for="login">Login:</label>
+          <div class="controls">
+            <input type="text" class="input-xlarge" name="login" id="login" value="" />
+          </div>
+        </div>
+        <div class="control-group">
+          <label class="control-label" for="password">Password:</label>
+          <div class="controls">
+            <input type="password" name="password" id="password" value="" />
+          </div>
+        </div>
+        <div class="control-group">
+          <label class="control-label" for="remember">Remember me:</label>
+          <!-- optional 2 year cookie expiry -->
+          <div class="controls">
+            <input type="checkbox" name="remember" id="remember" value="63072000" checked="checked"/>
+          </div>
+        </div>        
 
-        <label for="login">Login:</label>
-        <input type="text" name="login" value="" />
-        <br/>
-        <label for="password">Password:</label>
-        <input type="password" name="password" value="" />
-        <!-- 50 year timeout -->
-        <input type="hidden" name="remember" value="1576800000" />
-        <br/>
+        <div class="form-actions">
+          <button name="s" id="s" type="submit" class="btn btn-primary">${_('Sign In')}</button>
+          — 
+          <a href="${h.url_for('reset')}">Forgot your password?</a>
+        </div>
       </fieldset>
-      <input name="s" id="s" type="submit" class="btn primary" value="${_('Sign In')}"/>
-      — 
-      <a href="${h.url_for(controller='user', action='request_reset')}">Forgot your password?</a>
     </form>
     <br/> 
     <!-- Simple OpenID Selector -->
diff --git a/ckan/templates/user/logout.html b/ckan/templates/user/logout.html
index e40ec5c..0f37272 100644
--- a/ckan/templates/user/logout.html
+++ b/ckan/templates/user/logout.html
@@ -6,6 +6,7 @@
 
   <py:def function="page_title">Logout</py:def>
   <py:def function="page_heading">Logout from ${g.site_title}</py:def>
+  <py:def function="body_class">no-sidebar</py:def>
 
   <div py:match="content">
     <p>You have logged out successfully.</p>
diff --git a/ckan/tests/functional/test_search.py b/ckan/tests/functional/test_search.py
index fe1802c..a9a9339 100644
--- a/ckan/tests/functional/test_search.py
+++ b/ckan/tests/functional/test_search.py
@@ -108,7 +108,7 @@ def test_search_foreign_chars(self):
         res = self.app.get(offset)
         assert 'Search - ' in res
         self._check_search_results(res, u'th\xfcmb', ['<strong>1</strong>'])
-        self._check_search_results(res, 'thumb', ['<strong>0</strong>'])
+        self._check_search_results(res, 'thumb', ['<strong>1</strong>'])
 
     @search_related
     def test_search_escape_chars(self):
diff --git a/ckan/tests/functional/test_user.py b/ckan/tests/functional/test_user.py
index 5ab1efc..eb44bd9 100644
--- a/ckan/tests/functional/test_user.py
+++ b/ckan/tests/functional/test_user.py
@@ -168,11 +168,14 @@ def test_login(self):
         fv = res.forms['login']
         fv['login'] = str(username)
         fv['password'] = str(password)
+        fv['remember'] = False
         res = fv.submit()
 
         # check cookies set
         cookies = self._get_cookie_headers(res)
         assert cookies
+        for cookie in cookies:
+            assert not 'max-age' in cookie.lower(), cookie
 
         # first get redirected to user/logged_in
         assert_equal(res.status, 302)
@@ -206,6 +209,32 @@ def test_login(self):
         print res
         assert 'testlogin' in res.body, res.body
 
+    def test_login_remembered(self):
+        # create test user
+        username = u'testlogin2'
+        password = u'letmein'
+        CreateTestData.create_user(name=username,
+                                   password=password)
+        user = model.User.by_name(username)
+
+        # do the login
+        offset = url_for(controller='user', action='login')
+        res = self.app.get(offset)
+        fv = res.forms['login']
+        fv['login'] = str(username)
+        fv['password'] = str(password)
+        fv['remember'] = True
+        res = fv.submit()
+
+        # check cookies set
+        cookies = self._get_cookie_headers(res)
+        assert cookies
+        # check cookie is remembered via Max-Age and Expires
+        # (both needed for cross-browser compatibility)
+        for cookie in cookies:
+            assert 'Max-Age=63072000;' in cookie, cookie
+            assert 'Expires=' in cookie, cookie
+
     def test_login_wrong_password(self):
         # create test user
         username = u'testloginwrong'
diff --git a/ckan/tests/lib/test_alphabet_pagination.py b/ckan/tests/lib/test_alphabet_pagination.py
index a1bf6ae..8afc276 100644
--- a/ckan/tests/lib/test_alphabet_pagination.py
+++ b/ckan/tests/lib/test_alphabet_pagination.py
@@ -1,5 +1,7 @@
 import re
 
+from nose.tools import assert_equal
+
 from ckan.tests import *
 from ckan.tests import regex_related
 from ckan.lib.create_test_data import CreateTestData
@@ -28,6 +30,16 @@ def setup_class(cls):
     def teardown_class(cls):
         model.repo.rebuild_db()
 
+    def test_00_model(self):
+        query = model.Session.query(model.Package)
+        page = h.AlphaPage(
+            collection=query,
+            alpha_attribute='title',
+            page='A',
+            other_text=other,
+        )
+        assert_equal(page.available, {'Other': 20, 'A': 10, 'C': 10, 'B': 10, 'E': 0, 'D': 10, 'G': 0, 'F': 0, 'I': 0, 'H': 0, 'K': 0, 'J': 0, 'M': 0, 'L': 0, 'O': 0, 'N': 0, 'Q': 0, 'P': 0, 'S': 0, 'R': 0, 'U': 0, 'T': 0, 'W': 0, 'V': 0, 'Y': 0, 'X': 0, 'Z': 0})
+
     def test_01_package_page(self):
         query = model.Session.query(model.Package)
         page = h.AlphaPage(
@@ -37,11 +49,12 @@ def test_01_package_page(self):
             other_text=other,
         )
         pager = page.pager()
-        assert pager.startswith('<div class="pager">'), pager
-        assert '<span class="pager_curpage">A</span>' in pager, pager
+        assert pager.startswith('<div class="pagination pagination-alphabet">'), pager
+        assert '<li class="active"><a href="/tag?page=A">A</a></li>' in pager, pager
         url_base = '/packages'
-        assert re.search('\<span class="pager_empty"\>B\<\/span\>', pager), pager
-        assert re.search('\<span class="pager_empty"\>Other\<\/span\>', pager), pager
+        assert re.search(r'\<li\>\<a href="\/tag\?page=B"\>B\<\/a\>\<\/li\>', pager), pager
+        assert re.search(r'\<li class="disabled"\>\<a href="\/tag\?page=E"\>E\<\/a\>\<\/li\>', pager), pager
+        assert re.search(r'\<li\>\<a href="\/tag\?page=Other"\>Other\<\/a\>\<\/li\>', pager), pager
 
 
     def test_02_package_items(self):
diff --git a/ckan/tests/lib/test_solr_package_search.py b/ckan/tests/lib/test_solr_package_search.py
index 6ec2b2f..75d54c0 100644
--- a/ckan/tests/lib/test_solr_package_search.py
+++ b/ckan/tests/lib/test_solr_package_search.py
@@ -292,7 +292,7 @@ def test_search_foreign_chars(self):
         result = search.query_for(model.Package).run({'q': 'umlaut'})
         assert result['results'] == ['gils'], result['results']
         result = search.query_for(model.Package).run({'q': u'thumb'})
-        assert result['count'] == 0, result['results']
+        assert result['results'] == ['gils'], result['results']
         result = search.query_for(model.Package).run({'q': u'th\xfcmb'})
         assert result['results'] == ['gils'], result['results']
 
diff --git a/ckanext/multilingual/solr/schema.xml b/ckanext/multilingual/solr/schema.xml
index 8475187..fb957d3 100644
--- a/ckanext/multilingual/solr/schema.xml
+++ b/ckanext/multilingual/solr/schema.xml
@@ -16,7 +16,7 @@
  limitations under the License.
 -->
 
-<schema name="ckan" version="1.3">
+<schema name="ckan" version="1.4">
 
 <types>
     <fieldType name="string" class="solr.StrField" sortMissingLast="true" omitNorms="true"/>
@@ -373,6 +373,8 @@
     <field name="tags" type="string" indexed="true" stored="true" multiValued="true"/>
     <field name="groups" type="string" indexed="true" stored="true" multiValued="true"/>
 
+    <field name="capacity" type="string" indexed="true" stored="true" multiValued="false"/>
+
     <field name="res_description" type="textgen" indexed="true" stored="true" multiValued="true"/>
     <field name="res_format" type="string" indexed="true" stored="true" multiValued="true"/>
     <field name="res_url" type="string" indexed="true" stored="true" multiValued="true"/>
@@ -390,11 +392,19 @@
     <field name="linked_from" type="text" indexed="true" stored="false" multiValued="true"/>
     <field name="child_of" type="text" indexed="true" stored="false" multiValued="true"/>
     <field name="parent_of" type="text" indexed="true" stored="false" multiValued="true"/>
+    <field name="views_total" type="int" indexed="true" stored="false"/>
+    <field name="views_recent" type="int" indexed="true" stored="false"/>
+    <field name="resources_accessed_total" type="int" indexed="true" stored="false"/>
+    <field name="resources_accessed_recent" type="int" indexed="true" stored="false"/>
 
     <field name="metadata_created" type="date" indexed="true" stored="true" multiValued="false"/>
     <field name="metadata_modified" type="date" indexed="true" stored="true" multiValued="false"/>
 
     <field name="indexed_ts" type="date" indexed="true" stored="true" default="NOW" multiValued="false"/>
+
+    <!-- Copy the title field into titleString, and treat as a string
+         (rather than text type).  This allows us to sort on the titleString -->
+    <field name="title_string" type="string" indexed="true" stored="false" />
      
     <!-- Multilingual -->
     <field name="text_en" type="text_en" indexed="true" stored="true"/>
@@ -424,6 +434,8 @@
     <field name="text_pl" type="text_pl" indexed="true" stored="true"/>
     <field name="title_pl" type="text_pl" indexed="true" stored="true"/>
 
+    <field name="data_dict" type="string" indexed="false" stored="true" />
+
     <dynamicField name="extras_*" type="text" indexed="true" stored="true" multiValued="false"/>
     <dynamicField name="*" type="string" indexed="true"  stored="false"/>
 </fields>
diff --git a/doc/index.rst b/doc/index.rst
index b1c71f0..601d4c7 100644
--- a/doc/index.rst
+++ b/doc/index.rst
@@ -2,6 +2,10 @@
 Welcome to CKAN's Administration Guide
 =======================================
 
+.. note ::
+
+   This is the documentation for CKAN version '|version|'. If you are using a different version, use the links on the bottom right corner of the page to select the appropriate documentation.
+
 This Administration Guide covers how to set up and manage `CKAN <http://ckan.org>`_ software. 
 
 * The first two sections cover your two options for installing CKAN: package or source install.
diff --git a/doc/solr-setup.rst b/doc/solr-setup.rst
index 4529797..3c158b2 100644
--- a/doc/solr-setup.rst
+++ b/doc/solr-setup.rst
@@ -71,7 +71,7 @@ so, create a symbolic link to the schema file in the config folder. Use the late
 supported by the CKAN version you are installing (it will generally be the highest one)::
 
  sudo mv /etc/solr/conf/schema.xml /etc/solr/conf/schema.xml.bak
- sudo ln -s ~/ckan/ckan/config/solr/schema-1.3.xml /etc/solr/conf/schema.xml
+ sudo ln -s ~/ckan/ckan/config/solr/schema-1.4.xml /etc/solr/conf/schema.xml
 
 Now restart jetty::
 
@@ -93,6 +93,7 @@ will have different paths in the Solr server URL::
 
  http://localhost:8983/solr/ckan-schema-1.2       # Used by CKAN up to 1.5
  http://localhost:8983/solr/ckan-schema-1.3       # Used by CKAN 1.5.1
+ http://localhost:8983/solr/ckan-schema-1.4       # Used by CKAN 1.7
  http://localhost:8983/solr/some-other-site  # Used by another site
 
 To set up a multicore Solr instance, repeat the steps on the previous section
diff --git a/doc/using-data-api.rst b/doc/using-data-api.rst
index 09caf21..50c7db2 100644
--- a/doc/using-data-api.rst
+++ b/doc/using-data-api.rst
@@ -2,22 +2,44 @@
 Using the Data API
 ==================
 
+The following provides an introduction to using the CKAN :doc:`DataStore
+<datastore>` Data API.
+
 Introduction
 ============
 
-The Data API builds directly on ElasticSearch, with a resource API endpoint
-being equivalent to a single index 'type' in ElasticSearch (we tend to refer to
-it as a 'table').  This means you can often directly re-use `ElasticSearch
-client libraries`_ when connecting to the API endpoint.
+Each 'table' in the DataStore is an ElasticSearch_ index type ('table'). As
+such the Data API for each CKAN resource is directly equivalent to a single
+index 'type' in ElasticSearch (we tend to refer to it as a 'table').
+
+This means you can (usually) directly re-use `ElasticSearch client libraries`_
+when connecting to a Data API endpoint. It also means that what follows is, in
+essence, a tutorial in using the ElasticSearch_ API.
+
+The following short set of slides provide a brief overview and introduction to
+the DataStore and the Data API.
 
-Furthermore, it means that what is presented below is essentially a tutorial in the ElasticSearch API.
+.. raw:: html
 
+   <iframe src="https://docs.google.com/presentation/embed?id=1UhEqvEPoL_VWO5okYiEPfZTLcLYWqtvRRmB1NBsWXY8&start=false&loop=false&delayms=3000" frameborder="0" width="480" height="389" allowfullscreen="true" mozallowfullscreen="true" webkitallowfullscreen="true"></iframe>
+
+.. _ElasticSearch: http://elasticsearch.org/
 .. _ElasticSearch client libraries: http://www.elasticsearch.org/guide/appendix/clients.html
 
 Quickstart
 ==========
 
-``{{endpoint}}`` refers to the data API endpoint (or ElasticSearch index / table).
+``{{endpoint}}`` refers to the data API endpoint (or ElasticSearch index /
+table). For example, on the DataHub_ this gold prices data resource
+http://datahub.io/dataset/gold-prices/resource/b9aae52b-b082-4159-b46f-7bb9c158d013
+would have its Data API endpoint at:
+http://datahub.io/api/data/b9aae52b-b082-4159-b46f-7bb9c158d013. If you were
+just using ElasticSearch standalone an example of an endpoint would be:
+http://localhost:9200/gold-prices/monthly-price-table.
+
+.. note::  every resource on a CKAN instance for which a DataStore table is
+           enabled provides links to its Data API endpoint via the Data API
+           button at the top right of the resource page.
 
 Key urls:
 
@@ -28,6 +50,8 @@ Key urls:
 
 * Schema (Mapping): ``{{endpoint}}/_mapping``
 
+.. _DataHub: http://datahub.io/
+
 Examples
 --------
 
diff --git a/pip-requirements.txt b/pip-requirements.txt
index 4460f14..9ff9d10 100644
--- a/pip-requirements.txt
+++ b/pip-requirements.txt
@@ -5,11 +5,11 @@
 # 
 #     pip install --ignore-installed -r pip-requirements.txt
 
--e git+https://github.com/okfn/ckan@release-v1.6.1#egg=ckan
+-e git+https://github.com/okfn/ckan@master#egg=ckan
 # CKAN dependencies
--r https://github.com/okfn/ckan/raw/release-v1.6.1/requires/lucid_conflict.txt
--r https://github.com/okfn/ckan/raw/release-v1.6.1/requires/lucid_present.txt
--r https://github.com/okfn/ckan/raw/release-v1.6.1/requires/lucid_missing.txt
+-r https://github.com/okfn/ckan/raw/master/requires/lucid_conflict.txt
+-r https://github.com/okfn/ckan/raw/master/requires/lucid_present.txt
+-r https://github.com/okfn/ckan/raw/master/requires/lucid_missing.txt
 
 # NOTE: Developers, please do not edit this file. Changes should go in the
 #       appropriate files in the `requires' directory.


================================================================
  Commit: fe973717d4a6ed00b326af921e3df246e5b3a67b
      https://github.com/okfn/ckan/commit/fe973717d4a6ed00b326af921e3df246e5b3a67b
  Author: Toby <toby.junk at gmail.com>
  Date:   2012-04-30 (Mon, 30 Apr 2012)

  Changed paths:
    M ckan/logic/converters.py
    M ckan/templates/_snippet/data-api-help.html
    M ckan/templates/js_strings.html
    M ckan/templates/package/layout.html
    M ckanext/multilingual/plugin.py
    M ckanext/organizations/forms.py
    M ckanext/organizations/templates/organization_package_form.html
    M doc/solr-setup.rst

  Log Message:
  -----------
  Merge branch 'master' of github.com:okfn/ckan


diff --git a/ckan/logic/converters.py b/ckan/logic/converters.py
index 6b927ae..18a67df 100644
--- a/ckan/logic/converters.py
+++ b/ckan/logic/converters.py
@@ -77,7 +77,8 @@ def callable(key, data, errors, context):
         for k in data.keys():
             if k[0] == 'tags':
                 if data[k].get('vocabulary_id') == v.id:
-                    tags.append(data[k]['name'])
+                    name = data[k].get('display_name', data[k]['name'])
+                    tags.append(name)
         data[key] = tags
     return callable
 
diff --git a/ckan/templates/_snippet/data-api-help.html b/ckan/templates/_snippet/data-api-help.html
index a387417..16349df 100644
--- a/ckan/templates/_snippet/data-api-help.html
+++ b/ckan/templates/_snippet/data-api-help.html
@@ -17,7 +17,7 @@
 <div class="modal-body">
   <p><strong>Access resource data via a web API with powerful query
     support</strong>. Further information in the <a
-    href="http://docs.ckan.org/en/latest/storage/datastore.html" target="_blank">main
+    href="http://docs.ckan.org/en/latest/using-data-api.html" target="_blank">main
     CKAN Data API and DataStore documentation</a>.</p>
 
   <div class="accordion-group">
diff --git a/ckan/templates/js_strings.html b/ckan/templates/js_strings.html
index 76a0d7e..a22ea2f 100644
--- a/ckan/templates/js_strings.html
+++ b/ckan/templates/js_strings.html
@@ -13,61 +13,62 @@
   /*
    * Used in application.js.
    */
-  CKAN.Strings.checking = "${_('Checking...')}";
-  CKAN.Strings.urlIsTooShort = "${_('Type at least two characters...')}";
-  CKAN.Strings.urlIsUnchanged = "${_('This is the current URL.')}";
-  CKAN.Strings.urlIsAvailable = "${_('This URL is available!')}";
-  CKAN.Strings.urlIsNotAvailable = "${_('This URL is already used, please use a different one.')}";
-  CKAN.Strings.failedToSave = "${_('Failed to save, possibly due to invalid data ')}";
-  CKAN.Strings.addDataset = "${_('Add Dataset')}";
-  CKAN.Strings.addGroup = "${_('Add Group')}";
-  CKAN.Strings.youHaveUnsavedChanges = "${_("You have unsaved changes. Make sure to click 'Save Changes' below before leaving this page.")}";
-  CKAN.Strings.loading = "${_('Loading...')}";
-  CKAN.Strings.noNameBrackets = "${_('(no name)')}";
-  CKAN.Strings.deleteThisResourceQuestion = "${_('Delete the resource \'%name%\'?')}";
-  CKAN.Strings.previewNotAvailableForDataType = "${_('Preview not available for data type: ')}";
-  CKAN.Strings.failedToGetCredentialsForUpload = "${_('Failed to get credentials for storage upload. Upload cannot proceed')}";
-  CKAN.Strings.checkingUploadPermissions = "${_('Checking upload permissions ...')}";
-  CKAN.Strings.uploadingFile = "${_('Uploading file ...')}";
-  CKAN.Strings.dataFile = "${_('Data File')}";
-  CKAN.Strings.api = "${_('API')}";
-  CKAN.Strings.visualization = "${_('Visualization')}";
-  CKAN.Strings.image = "${_('Image')}";
-  CKAN.Strings.metadata = "${_('Metadata')}";
-  CKAN.Strings.documentation = "${_('Documentation')}";
-  CKAN.Strings.code = "${_('Code')}";
-  CKAN.Strings.example = "${_('Example')}";
+  CKAN.Strings = ${
+    h.json.dumps(dict(
+      checking = _('Checking...'),
+      urlIsTooShort = _('Type at least two characters...'),
+      urlIsUnchanged = _('This is the current URL.'),
+      urlIsAvailable = _('This URL is available!'),
+      urlIsNotAvailable = _('This URL is already used, please use a different one.'),
+      failedToSave = _('Failed to save, possibly due to invalid data '),
+      addDataset = _('Add Dataset'),
+      addGroup = _('Add Group'),
+      youHaveUnsavedChanges = _("You have unsaved changes. Make sure to click 'Save Changes' below before leaving this page."),
+      loading = _('Loading...'),
+      noNameBrackets = _('(no name)'),
+      deleteThisResourceQuestion = _('Delete the resource \'%name%\'?'),
+      previewNotAvailableForDataType = _('Preview not available for data type: '),
+      failedToGetCredentialsForUpload = _('Failed to get credentials for storage upload. Upload cannot proceed'),
+      checkingUploadPermissions = _('Checking upload permissions ...'),
+      uploadingFile = _('Uploading file ...'),
+      dataFile = _('Data File'),
+      api = _('API'),
+      visualization = _('Visualization'),
+      image = _('Image'),
+      metadata = _('Metadata'),
+      documentation = _('Documentation'),
+      code = _('Code'),
+      example = _('Example'),
+
+      upload = _('Upload'),
+      cancel = _('Cancel'),
+      name = _('Name'),
+      description = _('Description'),
+      url = _('Url'),
+      format = _('Format'),
+      resourceType = _('Resource Type'),
+      datastoreEnabled = _('DataStore enabled'),
+      sizeBracketsBytes = _('Size (Bytes)'),
+      mimetype = _('Mimetype'),
+      created = _('Created'),
+      lastModified = _('Last Modified'),
+      mimetypeInner = _('Mimetype (Inner)'),
+      hash = _('Hash'),
+      id = _('ID'),
+      doneEditing = _('Done'),
+      resourceHasUnsavedChanges = _('This resource has unsaved changes.'),
+      edit = _('Edit'),
+      preview = _('Preview'),
+      resourceFormatPlaceholder = _('e.g. csv, html, xls, rdf, ...'),
+      unknown = _('Unknown'),
+      extraFields = _('Extra Fields'),
+      addExtraField = _('Add Extra Field'),
+      deleteResource = _('Delete Resource'),
+      youCanUseMarkdown = _('You can use %aMarkdown formatting%b here.'),
+      shouldADataStoreBeEnabled = _('Should a %aDataStore table and Data API%b be enabled for this resource?'),
+      datesAreInISO = _('Dates are in %aISO Format%b — eg. %c2012-12-25%d or %c2010-05-31T14:30%d.'),
+      dataFileUploaded = _('Data File (Uploaded)')
+    ), indent=4)}
 
-  /*
-   * Used in templates.js.
-   */
-  CKAN.Strings.upload = "${_('Upload')}";
-  CKAN.Strings.cancel = "${_('Cancel')}";
-  CKAN.Strings.name = "${_('Name')}";
-  CKAN.Strings.description = "${_('Description')}";
-  CKAN.Strings.url = "${_('Url')}";
-  CKAN.Strings.format = "${_('Format')}";
-  CKAN.Strings.resourceType = "${_('Resource Type')}";
-  CKAN.Strings.datastoreEnabled = "${_('DataStore enabled')}";
-  CKAN.Strings.sizeBracketsBytes = "${_('Size (Bytes)')}";
-  CKAN.Strings.mimetype = "${_('Mimetype')}";
-  CKAN.Strings.created = "${_('Created')}";
-  CKAN.Strings.lastModified = "${_('Last Modified')}";
-  CKAN.Strings.mimetypeInner = "${_('Mimetype (Inner)')}";
-  CKAN.Strings.hash = "${_('Hash')}";
-  CKAN.Strings.id = "${_('ID')}";
-  CKAN.Strings.doneEditing = "${_('Done')}";
-  CKAN.Strings.resourceHasUnsavedChanges = "${_('This resource has unsaved changes.')}";
-  CKAN.Strings.edit = "${_('Edit')}";
-  CKAN.Strings.preview = "${_('Preview')}";
-  CKAN.Strings.resourceFormatPlaceholder = "${_('e.g. csv, html, xls, rdf, ...')}";
-  CKAN.Strings.unknown = "${_('Unknown')}";
-  CKAN.Strings.extraFields = "${_('Extra Fields')}";
-  CKAN.Strings.addExtraField = "${_('Add Extra Field')}";
-  CKAN.Strings.deleteResource = "${_('Delete Resource')}";
-  CKAN.Strings.youCanUseMarkdown = "${_('You can use %aMarkdown formatting%b here.')}";
-  CKAN.Strings.shouldADataStoreBeEnabled = "${_('Should a %aDataStore table and Data API%b be enabled for this resource?')}";
-  CKAN.Strings.datesAreInISO = "${_('Dates are in %aISO Format%b — eg. %c2012-12-25%d or %c2010-05-31T14:30%d.')}";
-  CKAN.Strings.dataFileUploaded = "${_('Data File (Uploaded)')}";
 </script>
 
diff --git a/ckan/templates/package/layout.html b/ckan/templates/package/layout.html
index 5015093..3646d0f 100644
--- a/ckan/templates/package/layout.html
+++ b/ckan/templates/package/layout.html
@@ -34,18 +34,20 @@
         </li>
       </py:otherwise>
       </py:choose>
-      <li class="${'active' if c.action=='history' else ''}">${h.subnav_link(h.icon('page_stack') + _('History'), controller='package', action='history', id=c.pkg.name)}</li>
-
       <li class="${'active' if c.action=='related' else ''}">${h.subnav_link(h.icon('package') + _('Related') + ' (%s)' % c.related_count, controller='related', action='list', id=c.pkg.name)}</li>
 
+
       <py:if test="h.check_access('package_update',{'id':c.pkg.id})">
         <li class="divider">|</li>
+      </py:if>
+      <li class="${'active' if c.action=='history' else ''}">${h.subnav_link(h.icon('page_stack') + _('History'), controller='package', action='history', id=c.pkg.name)}</li>
+
+      <py:if test="h.check_access('package_update',{'id':c.pkg.id})">
         <li class="${'active' if c.action=='edit' else ''}">
           ${h.subnav_link(h.icon('package_edit') + _('Settings'), controller='package', action='edit', id=c.pkg.name)}
         </li>
       </py:if>
 
-
       <li class="${'active' if c.action=='authz' else ''}" py:if="h.check_access('package_edit_permissions',{'id':c.pkg.id})">
         ${h.subnav_link(h.icon('lock') + _('Authorization'), controller='package', action='authz', id=c.pkg.name)}
       </li>
diff --git a/ckanext/multilingual/plugin.py b/ckanext/multilingual/plugin.py
index a4696bd..c050f57 100644
--- a/ckanext/multilingual/plugin.py
+++ b/ckanext/multilingual/plugin.py
@@ -75,7 +75,7 @@ def translate_data_dict(data_dict):
                 translated_flattened[key] = fallback_translations.get(
                         value, value)
 
-        elif isinstance(value, int):
+        elif isinstance(value, (int, dict)):
             translated_flattened[key] = value
 
         else:
diff --git a/ckanext/organizations/forms.py b/ckanext/organizations/forms.py
index c14e5e1..de60afb 100644
--- a/ckanext/organizations/forms.py
+++ b/ckanext/organizations/forms.py
@@ -221,9 +221,9 @@ def package_form(self):
     def db_to_form_schema(self):
         '''This is an interface to manipulate data from the database
         into a format suitable for the form (optional)'''
-        schema = default_package_schema()
-        schema['groups']['capacity'] = [ ignore_missing, unicode ]
-        return schema
+        #schema = default_package_schema()
+        #schema['groups']['capacity'] = [ ignore_missing, unicode ]
+        #return schema
 
     def form_to_db_schema(self):
         schema = default_package_schema()
diff --git a/ckanext/organizations/templates/organization_package_form.html b/ckanext/organizations/templates/organization_package_form.html
index 82a4404..e5cd0ec 100644
--- a/ckanext/organizations/templates/organization_package_form.html
+++ b/ckanext/organizations/templates/organization_package_form.html
@@ -143,6 +143,49 @@
   <div class="instructions">
     <p>Upload or link data files, APIs and other materials related to your dataset.</p>
   </div>
+  <div class="row">
+    <div class="span4">
+      <ul class="resource-list resource-list-edit drag-drop-list">
+      </ul>
+      <ul class="resource-list resource-list-add">
+        <li><a href="#" class="js-resource-add">${h.icon('page_white_add')}New resource...</a></li>
+      </ul>
+    </div>
+    <div class="span8">
+      <div style="display: none;" class="resource-panel">
+        <button class="btn btn-danger resource-panel-close">x</button>
+        <div class="resource-details resource-add">
+          <ul class="nav nav-tabs">
+            <li><a data-toggle="tab" href="#link-file">Link to a file</a></li>
+            <li><a data-toggle="tab" href="#link-api">Link to an API</a></li>
+            <li><a data-toggle="tab" href="#upload-file">Upload a file</a></li>
+          </ul>
+          <div class="tab-content">
+            <div class="tab-pane" id="link-file">
+              <div class="form-inline js-add-url-form">
+                <label class="field_opt" for="url">File URL</label>
+                <input name="add-resource-url" type="text" class="input-small" placeholder="http://mydataset.com/file.csv"/>
+                <input name="add-resource-save" type="submit" class="btn btn-primary" value="Add" />
+              </div>
+            </div>
+            <div class="tab-pane" id="link-api">
+              <div class="form-inline js-add-api-form">
+                <label class="field_opt" for="url">API URL</label>
+                <input name="add-resource-url" type="text" class="input-small" placeholder="http://mydataset.com/api/"/>
+                <input name="add-resource-save" type="submit" class="btn btn-primary" value="Add" />
+              </div>
+            </div>
+            <div class="tab-pane" id="upload-file">
+              <div class="js-add-upload-form">
+              </div>
+              <div class="alert alert-block" style="display: none;"></div>
+            </div>
+          </div>
+        </div>
+      </div>
+    </div>
+  </div>
+
   <div class="js-resource-edit-barebones">
     <!-- The resource editor deletes these fields and replaces them with a dynamic editor.
          They are required for the form to render correctly when not in resource-edit mode. -->
@@ -152,22 +195,6 @@
       </py:for>
     </py:for>
   </div>
-  <ul class="resource-list resource-list-edit drag-drop-list">
-  </ul>
-  <ul class="resource-list resource-list-add">
-    <li><a href="#" class="js-resource-add">${h.icon('page_white_add')}New resource...</a></li>
-  </ul>
-  <div style="display: none;" class="resource-panel">
-    <button class="btn btn-danger resource-panel-close">x</button>
-    <div class="resource-details resource-add">
-      <ul class="button-row">
-        <li><h4>Add a resource:</h4></li>
-        <li><button class="btn js-link-file">Link to a file</button></li>
-        <li><button class="btn js-link-api">Link to an API</button></li>
-        <li class="js-upload-file ckan-logged-in" style="display: none;"><button class="btn js-upload-file">Upload a file</button></li>
-      </ul>
-    </div>
-  </div>
 </fieldset>
 
 <fieldset class="tab-pane fade" id='further-information'>
diff --git a/doc/solr-setup.rst b/doc/solr-setup.rst
index 3c158b2..395da67 100644
--- a/doc/solr-setup.rst
+++ b/doc/solr-setup.rst
@@ -50,13 +50,12 @@ and the admin site::
 
  http://localhost:8983/solr/admin
 
-.. note:: If you get the message `Could not start Jetty servlet engine because no Java Development Kit (JDK) was found.` then you will have to edit /etc/profile and add this line to the end such as this to the end (adjusting the path for your machine's jdk install):
+.. note:: If you get the message ``Could not start Jetty servlet engine because no Java Development Kit (JDK) was found.`` then you will have to edit the ``JAVA_HOME`` setting in ``/etc/default/jetty`` (adjusting the path for your machine's JDK install):
 
     ``JAVA_HOME=/usr/lib/jvm/java-6-openjdk-amd64/``
 
 Now run::
 
-       export JAVA_HOME
        sudo service jetty start
 
 


================================================================
  Commit: ded67ed21753d2d2d1a123b2325a1296080ce08b
      https://github.com/okfn/ckan/commit/ded67ed21753d2d2d1a123b2325a1296080ce08b
  Author: Toby <toby.junk at gmail.com>
  Date:   2012-04-30 (Mon, 30 Apr 2012)

  Changed paths:
    M ckan/templates/facets.html

  Log Message:
  -----------
  facets.html make comments template only


diff --git a/ckan/templates/facets.html b/ckan/templates/facets.html
index f8c104b..001be8c 100644
--- a/ckan/templates/facets.html
+++ b/ckan/templates/facets.html
@@ -5,7 +5,7 @@
   py:strip=""
   >
 
-<!--
+<!--!
 Construct a facet <div> populated with links to filtered results.
 
 name
@@ -40,7 +40,7 @@
     </div>
 </py:def>
 
-<!--
+<!--!
 Generate <li>s for facet items.  The generated tags are not wrapped by any
 other tag, ie - it's up to the caller to wrap them in something suitable.
 
@@ -75,7 +75,7 @@
     </li>
 </py:def>
 
-<!--
+<!--!
 DEPRECATED.  Provided only for backward compatibility with existing plugins.
              Use `facet_div` instead.
 
@@ -129,7 +129,7 @@
     </div>
 </py:def>
 
-<!--
+<!--!
 DEPRECATED.  Provided only for backward compatibility with existing plugins.
              Use `facet_li` instead.
 


================================================================
Compare: https://github.com/okfn/ckan/compare/681d544...ded67ed


More information about the ckan-changes mailing list