[ckan-changes] [okfn/ckan] caf8ef: [2303] New paster command rdf-export will dump ALL...

GitHub noreply at github.com
Tue Apr 17 16:30:48 UTC 2012


  Branch: refs/heads/feature-1821-multilingual-extension
  Home:   https://github.com/okfn/ckan
  Commit: caf8efed4fb9a11661b4ea2f832ab8fd7bb33b98
      https://github.com/okfn/ckan/commit/caf8efed4fb9a11661b4ea2f832ab8fd7bb33b98
  Author: Ross Jones <rossdjones at gmail.com>
  Date:   2012-04-17 (Tue, 17 Apr 2012)

  Changed paths:
    M ckan/lib/cli.py
    M setup.py

  Log Message:
  -----------
  [2303] New paster command rdf-export will dump ALL active datasets to a specified folder in our default RDF format


diff --git a/ckan/lib/cli.py b/ckan/lib/cli.py
index 008cf7d..5146aa9 100644
--- a/ckan/lib/cli.py
+++ b/ckan/lib/cli.py
@@ -375,6 +375,66 @@ def command(self):
         else:
             print 'Command %s not recognized' % cmd
 
+
+class RDFExport(CkanCommand):
+    '''
+    This command dumps out all currently active datasets as RDF into the
+    specified folder.
+
+    Usage:
+      paster rdf-export /path/to/store/output
+    '''
+    summary = __doc__.split('\n')[0]
+    usage = __doc__
+
+    def command(self):
+        self._load_config()
+
+        if not self.args:
+            # default to run
+            print RDFExport.__doc__
+        else:
+            self.export_datasets( self.args[0] )
+
+    def export_datasets(self, out_folder):
+        '''
+        Export datasets as RDF to an output folder.
+        '''
+        import urlparse
+        import urllib2
+        import pylons.config as config
+        import ckan.model as model
+        import ckan.logic as logic
+        import ckan.lib.helpers as h
+
+        # Create output folder if not exists
+        if not os.path.isdir( out_folder ):
+            os.makedirs( out_folder )
+
+        fetch_url = config['ckan.site_url']
+        user = logic.get_action('get_site_user')({'model': model, 'ignore_auth': True}, {})
+        context = {'model': model, 'session': model.Session, 'user': user['name']}
+        dataset_names = logic.get_action('package_list')(context, {})
+        for dataset_name in dataset_names:
+            dd = logic.get_action('package_show')(context, {'id':dataset_name })
+            if not dd['state'] == 'active':
+                continue
+
+            url = h.url_for( controller='package',action='read',
+                                                  id=dd['name'])
+
+            url = urlparse.urljoin(fetch_url, url[1:]) + '.rdf'
+            try:
+                fname = os.path.join( out_folder, dd['name'] ) + ".rdf"
+                r = urllib2.urlopen(url).read()
+                with open(fname, 'wb') as f:
+                    f.write(r)
+            except IOError, ioe:
+                sys.stderr.write( str(ioe) + "\n" )
+
+
+
+
 class Sysadmin(CkanCommand):
     '''Gives sysadmin rights to a named user
 
@@ -720,7 +780,7 @@ class Celery(CkanCommand):
     summary = __doc__.split('\n')[0]
     usage = __doc__
 
-    def command(self):        
+    def command(self):
         if not self.args:
             self.run_()
         else:
diff --git a/setup.py b/setup.py
index a8f3389..ee1f06d 100644
--- a/setup.py
+++ b/setup.py
@@ -70,6 +70,7 @@
     rights = ckan.lib.authztool:RightsCommand
     roles = ckan.lib.authztool:RolesCommand
     celeryd = ckan.lib.cli:Celery
+    rdf-export = ckan.lib.cli:RDFExport
 
     [console_scripts]
     ckan-admin = bin.ckan_admin:Command


================================================================
  Commit: 20edc092e09f48c44043bc2fe21a3033abbddfd7
      https://github.com/okfn/ckan/commit/20edc092e09f48c44043bc2fe21a3033abbddfd7
  Author: Ross Jones <rossdjones at gmail.com>
  Date:   2012-04-17 (Tue, 17 Apr 2012)

  Changed paths:
    M ckan/lib/cli.py
    M setup.py

  Log Message:
  -----------
  Merge branch 'feature-2303-rdf-export-command'


diff --git a/ckan/lib/cli.py b/ckan/lib/cli.py
index 008cf7d..5146aa9 100644
--- a/ckan/lib/cli.py
+++ b/ckan/lib/cli.py
@@ -375,6 +375,66 @@ def command(self):
         else:
             print 'Command %s not recognized' % cmd
 
+
+class RDFExport(CkanCommand):
+    '''
+    This command dumps out all currently active datasets as RDF into the
+    specified folder.
+
+    Usage:
+      paster rdf-export /path/to/store/output
+    '''
+    summary = __doc__.split('\n')[0]
+    usage = __doc__
+
+    def command(self):
+        self._load_config()
+
+        if not self.args:
+            # default to run
+            print RDFExport.__doc__
+        else:
+            self.export_datasets( self.args[0] )
+
+    def export_datasets(self, out_folder):
+        '''
+        Export datasets as RDF to an output folder.
+        '''
+        import urlparse
+        import urllib2
+        import pylons.config as config
+        import ckan.model as model
+        import ckan.logic as logic
+        import ckan.lib.helpers as h
+
+        # Create output folder if not exists
+        if not os.path.isdir( out_folder ):
+            os.makedirs( out_folder )
+
+        fetch_url = config['ckan.site_url']
+        user = logic.get_action('get_site_user')({'model': model, 'ignore_auth': True}, {})
+        context = {'model': model, 'session': model.Session, 'user': user['name']}
+        dataset_names = logic.get_action('package_list')(context, {})
+        for dataset_name in dataset_names:
+            dd = logic.get_action('package_show')(context, {'id':dataset_name })
+            if not dd['state'] == 'active':
+                continue
+
+            url = h.url_for( controller='package',action='read',
+                                                  id=dd['name'])
+
+            url = urlparse.urljoin(fetch_url, url[1:]) + '.rdf'
+            try:
+                fname = os.path.join( out_folder, dd['name'] ) + ".rdf"
+                r = urllib2.urlopen(url).read()
+                with open(fname, 'wb') as f:
+                    f.write(r)
+            except IOError, ioe:
+                sys.stderr.write( str(ioe) + "\n" )
+
+
+
+
 class Sysadmin(CkanCommand):
     '''Gives sysadmin rights to a named user
 
@@ -720,7 +780,7 @@ class Celery(CkanCommand):
     summary = __doc__.split('\n')[0]
     usage = __doc__
 
-    def command(self):        
+    def command(self):
         if not self.args:
             self.run_()
         else:
diff --git a/setup.py b/setup.py
index a8f3389..ee1f06d 100644
--- a/setup.py
+++ b/setup.py
@@ -70,6 +70,7 @@
     rights = ckan.lib.authztool:RightsCommand
     roles = ckan.lib.authztool:RolesCommand
     celeryd = ckan.lib.cli:Celery
+    rdf-export = ckan.lib.cli:RDFExport
 
     [console_scripts]
     ckan-admin = bin.ckan_admin:Command


================================================================
  Commit: ba48fb51a868ffb5668df40b9c91e5307d32c071
      https://github.com/okfn/ckan/commit/ba48fb51a868ffb5668df40b9c91e5307d32c071
  Author: John Glover <j at johnglover.net>
  Date:   2012-04-17 (Tue, 17 Apr 2012)

  Changed paths:
    M ckan/public/scripts/application.js

  Log Message:
  -----------
  [xs][scripts] bug fix: prepend CKAN.SITE_URL to resource edit format icon call


diff --git a/ckan/public/scripts/application.js b/ckan/public/scripts/application.js
index 40513fd..995e703 100644
--- a/ckan/public/scripts/application.js
+++ b/ckan/public/scripts/application.js
@@ -537,7 +537,7 @@ CKAN.View.Resource = Backbone.View.extend({
     }
     self.updateIconTimer = setTimeout(function() {
         // AJAX to server API
-        $.getJSON('/api/2/util/resource/format_icon?format='+encodeURIComponent(self.formatBox.val()), function(data) {
+        $.getJSON(CKAN.SITE_URL + '/api/2/util/resource/format_icon?format='+encodeURIComponent(self.formatBox.val()), function(data) {
           if (data && data.icon && data.format==self.formatBox.val()) {
             self.li.find('.js-resource-icon').attr('src',data.icon);
             self.table.find('.js-resource-icon').attr('src',data.icon);


================================================================
  Commit: 386b2c15c0c2112c348ad275a485458e25158bff
      https://github.com/okfn/ckan/commit/386b2c15c0c2112c348ad275a485458e25158bff
  Author: Ross Jones <rossdjones at gmail.com>
  Date:   2012-04-17 (Tue, 17 Apr 2012)

  Changed paths:
    M ckanext/organizations/templates/organization_form.html

  Log Message:
  -----------
  Organization template fixes for disappearing users after edit


diff --git a/ckanext/organizations/templates/organization_form.html b/ckanext/organizations/templates/organization_form.html
index df0b9d7..f774043 100644
--- a/ckanext/organizations/templates/organization_form.html
+++ b/ckanext/organizations/templates/organization_form.html
@@ -50,7 +50,6 @@
     </div>
   </div>
 
-
     <dt class="parent-label" py:if="c.is_superuser_or_groupadmin">
 		<label class="field_opt" for="parent">Parent Organization</label>
 	</dt>
@@ -109,14 +108,27 @@
   </dl>
 </fieldset>
 
+<?python
+    import ckan.model as model
+    users = []
+    users.extend( { "name": user.name,
+                        "capacity": "admin" }
+                        for user in c.group.members_of_type( model.User, "admin"  ).all() )
+    users.extend( { "name": user.name,
+                        "capacity": "editor" }
+                        for user in c.group.members_of_type( model.User, 'editor' ).all() )
+?>
 <fieldset id="users">
   <h3>Users <span py:if="c.users">(${len(c.users.all())})</span></h3>
 <a py:if="c.group" href="${h.url_for(controller='ckanext.organizations.controllers:OrganizationController', action='users', id=c.group.name)}">Manage users</a>
 
-  <dl py:if="c.users">
-    <py:for each="user in c.users">
+  <dl py:if="users">
+    <py:for each="num, user in enumerate(users)">
 		<dd>
 			<label>${user['name']}</label>
+			<input type="hidden" name="users__${num}__name" value="${user['name']}"/>
+			<input type="hidden" name="users__${num}__capacity" value="${user['capacity']}"/>
+
 		</dd>
     </py:for>
   </dl>


================================================================
  Commit: 1ee9e8e611ea5d43c29e29603898de95193705e8
      https://github.com/okfn/ckan/commit/1ee9e8e611ea5d43c29e29603898de95193705e8
  Author: Ross Jones <rossdjones at gmail.com>
  Date:   2012-04-17 (Tue, 17 Apr 2012)

  Changed paths:
    M ckan/public/scripts/application.js

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


diff --git a/ckan/public/scripts/application.js b/ckan/public/scripts/application.js
index 40513fd..995e703 100644
--- a/ckan/public/scripts/application.js
+++ b/ckan/public/scripts/application.js
@@ -537,7 +537,7 @@ CKAN.View.Resource = Backbone.View.extend({
     }
     self.updateIconTimer = setTimeout(function() {
         // AJAX to server API
-        $.getJSON('/api/2/util/resource/format_icon?format='+encodeURIComponent(self.formatBox.val()), function(data) {
+        $.getJSON(CKAN.SITE_URL + '/api/2/util/resource/format_icon?format='+encodeURIComponent(self.formatBox.val()), function(data) {
           if (data && data.icon && data.format==self.formatBox.val()) {
             self.li.find('.js-resource-icon').attr('src',data.icon);
             self.table.find('.js-resource-icon').attr('src',data.icon);


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

  Changed paths:
    M ckan/config/environment.py
    M ckan/plugins/interfaces.py

  Log Message:
  -----------
  new ITemplateHelpers interface


diff --git a/ckan/config/environment.py b/ckan/config/environment.py
index a792b6e..fa91541 100644
--- a/ckan/config/environment.py
+++ b/ckan/config/environment.py
@@ -104,6 +104,20 @@ def find_controller(self, controller):
     else:
         config['pylons.h'] = h
 
+    # extend helper functions with ones supplied by plugins
+    from ckan.plugins import PluginImplementations
+    from ckan.plugins.interfaces import ITemplateHelpers
+
+    extra_helpers = []
+    for plugin in PluginImplementations(ITemplateHelpers):
+        helpers = plugin.get_helpers()
+        for helper in helpers:
+            if helper in extra_helpers:
+                raise Exception('overwritting extra helper %s' % helper)
+            extra_helpers.append(helper)
+            setattr(config['pylons.h'], helper, helpers[helper])
+
+
     ## redo template setup to use genshi.search_path (so remove std template setup)
     template_paths = [paths['templates'][0]]
     extra_template_paths = config.get('extra_template_paths', '')
diff --git a/ckan/plugins/interfaces.py b/ckan/plugins/interfaces.py
index a04d8fd..826e41d 100644
--- a/ckan/plugins/interfaces.py
+++ b/ckan/plugins/interfaces.py
@@ -13,7 +13,7 @@
     'IPackageController', 'IPluginObserver',
     'IConfigurable', 'IConfigurer', 'IAuthorizer',
     'IActions', 'IResourceUrlChange', 'IDatasetForm',
-    'IGroupForm',
+    'IGroupForm', 'ITemplateHelpers',
 ]
 
 from inspect import isclass
@@ -385,6 +385,16 @@ def get_auth_functions(self):
         implementation overrides
         """
 
+class ITemplateHelpers(Interface):
+    """
+    Allow adding extra template functions available via h variable
+    """
+    def get_helpers(self):
+        """
+        Should return a dict, the keys being the name of the helper
+        function and the values being the functions themselves.
+        """
+
 class IDatasetForm(Interface):
     """
     Allows customisation of the package controller as a plugin.


================================================================
  Commit: 94180bd19634374150fe52c9d6f36d4bd02d6b0d
      https://github.com/okfn/ckan/commit/94180bd19634374150fe52c9d6f36d4bd02d6b0d
  Author: Toby <toby.junk at gmail.com>
  Date:   2012-04-17 (Tue, 17 Apr 2012)

  Changed paths:
    M ckan/lib/helpers.py

  Log Message:
  -----------
  pass config varw into h.snippet()


diff --git a/ckan/lib/helpers.py b/ckan/lib/helpers.py
index 7996e7f..0824c15 100644
--- a/ckan/lib/helpers.py
+++ b/ckan/lib/helpers.py
@@ -659,6 +659,7 @@ def snippet(template_name, **kw):
     globs = kw
     globs['h'] = pylons_globs['h']
     globs['c'] = pylons_globs['c']
+    globs['config'] = pylons_globs['config']
     stream = template.generate(**globs)
     for item in PluginImplementations(IGenshiStreamFilter):
         stream = item.filter(stream)


================================================================
  Commit: c42ab5a6a925ab596f376dcc5a3eb781dd10bbb9
      https://github.com/okfn/ckan/commit/c42ab5a6a925ab596f376dcc5a3eb781dd10bbb9
  Author: Ross Jones <rossdjones at gmail.com>
  Date:   2012-04-17 (Tue, 17 Apr 2012)

  Changed paths:
    M ckanext/organizations/templates/organization_form.html

  Log Message:
  -----------
  [xs,noticket] Changes to get organization_form.html to match the group template


diff --git a/ckanext/organizations/templates/organization_form.html b/ckanext/organizations/templates/organization_form.html
index f774043..5a1dc6a 100644
--- a/ckanext/organizations/templates/organization_form.html
+++ b/ckanext/organizations/templates/organization_form.html
@@ -1,108 +1,118 @@
+<form
+  class="form-horizontal ${'has-errors' if errors else ''}"
+  id="group-edit"
+  action=""
+  method="post"
+  xmlns:i18n="http://genshi.edgewall.org/i18n"
+  xmlns:py="http://genshi.edgewall.org/"
+  xmlns:xi="http://www.w3.org/2001/XInclude">
 
-<form id="organization-edit" action="" method="post"
-    py:attrs="{'class':'has-errors'} if errors else {}"
-    xmlns:i18n="http://genshi.edgewall.org/i18n"
-    xmlns:py="http://genshi.edgewall.org/"
-    xmlns:xi="http://www.w3.org/2001/XInclude">
+<xi:include href="_util.html" />
 
 <div class="error-explanation" py:if="error_summary">
 <h2>Errors in form</h2>
 <p>The form contains invalid entries:</p>
 <ul>
-  <li py:for="key, error in error_summary.items()">${"%s: %s" % (key, error)}</li>
+  <li py:for="key, error in error_summary.items()">${"%s: %s" % (key if not key=='Name' else 'URL', error)}</li>
 </ul>
 </div>
 
-<input type="hidden" id="type" name="type" value="organization" />
-<input type="hidden" id="approval_status" name="approval_status" value="pending" />
-
 <fieldset id="basic-information">
-  <dl>
-    <dt><label class="field_opt" for="name">Organization name</label></dt>
-    <dd><input class="js-title" id="title" name="title" type="text" value="${data.get('title', '')}"/></dd>
-
-
-    <dt><label class="field_opt" for="title">Url</label></dt>
-    <dd class="name-field">
-      <span class="js-url-text url-text">${g.site_url+h.url_for('organization_index')+'/'}<span class="js-url-viewmode js-url-suffix"> </span><a style="display: none;" href="#" class="url-edit js-url-editlink js-url-viewmode">(edit)</a></span>
-      <input style="display: none;" id="name" maxlength="100" name="name" type="text" class="url-input js-url-editmode js-url-input" value="${data.get('name', '')}" />
+  <div class="control-group">
+    <label for="name" class="control-label">Title</label>
+    <div class="controls">
+      <input class="js-title" id="title" name="title" type="text" value="${data.get('title', '')}"/>
+    </div>
+  </div>
+  <div class="control-group">
+    <label for="title" class="control-label">Url</label>
+    <div class="controls">
+      <div class="input-prepend">
+        <span class="add-on">${h.url(controller='group', action='index')+'/'}</span>
+        <input maxlength="100" name="name" type="text" class="js-url-input" value="${data.get('name', '')}" />
+      </div>
       <p class="js-url-is-valid"> </p>
-    </dd>
-    <dd style="display: none;" class="js-url-editmode instructions basic">2+ chars, lowercase, using only 'a-z0-9' and '-_'</dd>
-    <dd class="field_error" py:if="errors.get('name', '')">${errors.get('name', '')}</dd>
-
-    <dt class="description-label"><label class="field_opt" for="title">Organization Description</label></dt>
-    <dd class="description-field"><div class="markdown-editor">
-      <ul class="button-row">
-        <li><button class="pretty-button js-markdown-edit depressed">Edit</button></li>
-        <li><button class="pretty-button js-markdown-preview">Preview</button></li>
-      </ul>
-      <textarea class="markdown-input" name="description" id="notes" placeholder="${_('Start with a summary sentence ...')}">${data.get('description','')}</textarea>
-      <div class="markdown-preview" style="display: none;"></div>
-      <span class="hints">You can use <a href="http://daringfireball.net/projects/markdown/syntax" target="_blank">Markdown formatting</a> here.</span>
-    </div></dd>
-
+      <p class="url-is-long">Warning: URL is very long. Consider changing it to something shorter.</p>
+      <p>2+ characters, lowercase, using only 'a-z0-9' and '-_'</p>
+      <p class="field_error" py:if="errors.get('name', '')">${errors.get('name', '')}</p>
+    </div>
+  </div>
+  <div class="control-group">
+    <label for="" class="control-label">Description</label>
+    <div class="controls">
+      ${markdown_editor('description', data.get('description'), 'notes', _('Start with a summary sentence ...'))}
+    </div>
+  </div>
   <div class="control-group">
     <label for="name" class="control-label">Image URL:</label>
     <div class="controls">
       <input id="image_url" name="image_url" type="text" value="${data.get('image_url', '')}"/>
-      <p>The URL for the image that is associated with this group.</p>
+      <p>The URL for the image that is associated with this organization.</p>
     </div>
   </div>
+  <div class="state-field control-group" py:if="c.is_sysadmin or c.auth_for_change_state">
+    <label for="" class="control-label">State</label>
+    <div class="controls">
+      <select id="state" name="state" >
+        <option py:attrs="{'selected': 'selected' if data.get('state') == 'active' else None}" value="active">active</option>
+        <option py:attrs="{'selected': 'selected' if data.get('state') == 'deleted' else None}" value="deleted">deleted</option>
+      </select>
+    </div>
+  </div>
+</fieldset>
 
-    <dt class="parent-label" py:if="c.is_superuser_or_groupadmin">
-		<label class="field_opt" for="parent">Parent Organization</label>
-	</dt>
-
-	<dd py:if="c.group and not c.is_superuser_or_groupadmin">
+  <div class="control-group" py:if="c.is_superuser_or_groupadmin">
+    <label class="control-label" for="parent">Parent Organization</label>
+    <div class="controls" py:if="c.group and not c.is_superuser_or_groupadmin">
 		<span py:if="c.parent is not None" class="js-title">
 			${ c.parent.title }
 		</span>
 		<span py:if="c.parent is None" class="js-title">
 			No parent organization
 		</span>
-	</dd>
-
-    <dd py:if="c.is_superuser_or_groupadmin" class="parent-field">
+	</div>
+    <div class="controls" py:if="c.is_superuser_or_groupadmin">
 		<select id="parent" name="parent" class="chzn-select" data-placeholder="Please choose a organization">
 			<option value=""></option>
     		<py:for each="pg in c.possible_parents">
 				<option py:attrs="{'selected': 'selected' if c.parent and pg.id == c.parent.id else None}" value="${pg.id}">${pg.title}</option>
 			</py:for>
 		</select>
-	</dd>
+	</div>
+  </div>
 
 
-    <dt class="state-label" py:if="c.is_sysadmin or c.auth_for_change_state"><label class="field_opt" for="state">State</label></dt>
-    <dd class="state-field" py:if="c.is_sysadmin or c.auth_for_change_state">
-    <select id="state" name="state" >
-      <option py:attrs="{'selected': 'selected' if data.get('state') == 'active' else None}" value="active">active</option>
-      <option py:attrs="{'selected': 'selected' if data.get('state') == 'deleted' else None}" value="deleted">deleted</option>
-    </select>
-    </dd>
-  </dl>
-</fieldset>
-
 <fieldset id="extras">
   <h3>Extras</h3>
   <dl>
     <py:with vars="extras = data.get('extras', [])">
     <py:for each="num, extra in enumerate(data.get('extras', []))">
-    <dt><label for="extras__${num}__value">${extra.get('key')}</label></dt>
-    <dd>
-      <input id="extras__${num}__key" name="extras__${num}__key" type="hidden" value="${extra.get('key')}" />
-      <input id="extras__${num}__value" name="extras__${num}__value" type="text" value="${extra.get('value')}" />
-      <input type="checkbox" name="extras__${num}__deleted" checked="${extra.get('deleted')}">Delete</input>
-    </dd>
+    <div class="control-group">
+      <label class="control-label" for="extras__${num}__value">${extra.get('key')}</label>
+      <div class="controls">
+        <input id="extras__${num}__key" name="extras__${num}__key" type="hidden" value="${extra.get('key')}" />
+        <input id="extras__${num}__value" name="extras__${num}__value" type="text" value="${extra.get('value')}" />
+        <label class="checkbox" style="display: inline-block;">
+          <input type="checkbox" name="extras__${num}__deleted" checked="${extra.get('deleted')}" />Delete
+        </label>
+      </div>
+    </div>
     </py:for>
-
+    <hr py:if="len(extras)" class="extras-divider" />
     <py:for each="num in range(len(extras), len(extras) + 4)">
-    <dt><label for="extras__${num}__key">New key</label></dt>
-    <dd>
-      <input class="medium-width" id="extras__${num}__key" name="extras__${num}__key" type="text" />
-      with value
-      <input class="medium-width" id="extras__${num}__value" name="extras__${num}__value" type="text" />
-    </dd>
+      <div class="control-group">
+        <label class="control-label" for="extras__${num}__key">Add...</label>
+        <div class="controls">
+          <label>
+            <span class="extras-label">Key =</span>
+            <input class="medium-width" id="extras__${num}__key" name="extras__${num}__key" type="text" />
+          </label>
+          <label>
+            <span class="extras-label">Value =</span>
+            <input class="medium-width" id="extras__${num}__value" name="extras__${num}__value" type="text" />
+          </label>
+        </div>
+      </div>
     </py:for>
     </py:with>
   </dl>
@@ -111,12 +121,13 @@
 <?python
     import ckan.model as model
     users = []
-    users.extend( { "name": user.name,
-                        "capacity": "admin" }
-                        for user in c.group.members_of_type( model.User, "admin"  ).all() )
-    users.extend( { "name": user.name,
-                        "capacity": "editor" }
-                        for user in c.group.members_of_type( model.User, 'editor' ).all() )
+    if c.group:
+        users.extend( { "name": user.name,
+                            "capacity": "admin" }
+                            for user in c.group.members_of_type( model.User, "admin"  ).all() )
+        users.extend( { "name": user.name,
+                            "capacity": "editor" }
+                            for user in c.group.members_of_type( model.User, 'editor' ).all() )
 ?>
 <fieldset id="users">
   <h3>Users <span py:if="c.users">(${len(c.users.all())})</span></h3>
@@ -136,10 +147,10 @@
 </fieldset>
 
 
-<div class="form-submit">
-  <input id="save" class="pretty-button primary" name="save" type="submit" value="${_('Save Changes')}" />
+<div class="form-actions">
+  <input id="save" class="btn btn-primary" name="save" type="submit" value="${_('Save Changes')}" />
   <py:if test="c.group">
-    <input id="cancel" class="pretty-button href-action" name="cancel" type="reset" value="${_('Cancel')}" action="${h.url_for(controller='group', action='read', id=c.group.name)}" />
+    <input id="cancel" class="btn href-action" name="cancel" type="reset" value="${_('Cancel')}" action="${h.url_for(controller='group', action='read', id=c.group.name)}" />
   </py:if>
 </div>
 </form>


================================================================
  Commit: bad42b82985a5f7a3d3dfababd6dbeff1a2b4405
      https://github.com/okfn/ckan/commit/bad42b82985a5f7a3d3dfababd6dbeff1a2b4405
  Author: Ross Jones <rossdjones at gmail.com>
  Date:   2012-04-17 (Tue, 17 Apr 2012)

  Changed paths:
    M ckanext/organizations/templates/organization_read.html

  Log Message:
  -----------
  Adding missing group type in template


diff --git a/ckanext/organizations/templates/organization_read.html b/ckanext/organizations/templates/organization_read.html
index 944192e..d946d31 100644
--- a/ckanext/organizations/templates/organization_read.html
+++ b/ckanext/organizations/templates/organization_read.html
@@ -10,6 +10,8 @@
     <py:def function="page_logo">${c.group.image_url}</py:def>
   </py:if>
 
+<input type="hidden" id="type" name="type" value="organization" />
+
   <?python
     from pylons import config
     from ckan import model


================================================================
  Commit: dbf246234d27d3ff7615219bb88570cd550d6c7f
      https://github.com/okfn/ckan/commit/dbf246234d27d3ff7615219bb88570cd550d6c7f
  Author: Ross Jones <rossdjones at gmail.com>
  Date:   2012-04-17 (Tue, 17 Apr 2012)

  Changed paths:
    M ckan/config/environment.py
    M ckan/lib/helpers.py
    M ckan/plugins/interfaces.py

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


diff --git a/ckan/config/environment.py b/ckan/config/environment.py
index a792b6e..fa91541 100644
--- a/ckan/config/environment.py
+++ b/ckan/config/environment.py
@@ -104,6 +104,20 @@ def find_controller(self, controller):
     else:
         config['pylons.h'] = h
 
+    # extend helper functions with ones supplied by plugins
+    from ckan.plugins import PluginImplementations
+    from ckan.plugins.interfaces import ITemplateHelpers
+
+    extra_helpers = []
+    for plugin in PluginImplementations(ITemplateHelpers):
+        helpers = plugin.get_helpers()
+        for helper in helpers:
+            if helper in extra_helpers:
+                raise Exception('overwritting extra helper %s' % helper)
+            extra_helpers.append(helper)
+            setattr(config['pylons.h'], helper, helpers[helper])
+
+
     ## redo template setup to use genshi.search_path (so remove std template setup)
     template_paths = [paths['templates'][0]]
     extra_template_paths = config.get('extra_template_paths', '')
diff --git a/ckan/lib/helpers.py b/ckan/lib/helpers.py
index 7996e7f..0824c15 100644
--- a/ckan/lib/helpers.py
+++ b/ckan/lib/helpers.py
@@ -659,6 +659,7 @@ def snippet(template_name, **kw):
     globs = kw
     globs['h'] = pylons_globs['h']
     globs['c'] = pylons_globs['c']
+    globs['config'] = pylons_globs['config']
     stream = template.generate(**globs)
     for item in PluginImplementations(IGenshiStreamFilter):
         stream = item.filter(stream)
diff --git a/ckan/plugins/interfaces.py b/ckan/plugins/interfaces.py
index a04d8fd..826e41d 100644
--- a/ckan/plugins/interfaces.py
+++ b/ckan/plugins/interfaces.py
@@ -13,7 +13,7 @@
     'IPackageController', 'IPluginObserver',
     'IConfigurable', 'IConfigurer', 'IAuthorizer',
     'IActions', 'IResourceUrlChange', 'IDatasetForm',
-    'IGroupForm',
+    'IGroupForm', 'ITemplateHelpers',
 ]
 
 from inspect import isclass
@@ -385,6 +385,16 @@ def get_auth_functions(self):
         implementation overrides
         """
 
+class ITemplateHelpers(Interface):
+    """
+    Allow adding extra template functions available via h variable
+    """
+    def get_helpers(self):
+        """
+        Should return a dict, the keys being the name of the helper
+        function and the values being the functions themselves.
+        """
+
 class IDatasetForm(Interface):
     """
     Allows customisation of the package controller as a plugin.


================================================================
  Commit: f5cb30eb9399a08d23f06c178957435981489818
      https://github.com/okfn/ckan/commit/f5cb30eb9399a08d23f06c178957435981489818
  Author: Sean Hammond <seanhammond at lavabit.com>
  Date:   2012-04-17 (Tue, 17 Apr 2012)

  Changed paths:
    M doc/api.rst
    M doc/apiv3.rst
    M doc/i18n.rst
    M doc/index.rst
    A doc/multilingual.rst
    M doc/paster.rst
    M doc/post-installation.rst

  Log Message:
  -----------
  Initial docs for ckanext-multilingual


diff --git a/doc/api.rst b/doc/api.rst
index 18e6823..98d84dc 100644
--- a/doc/api.rst
+++ b/doc/api.rst
@@ -1,4 +1,5 @@
 .. index:: API
+.. _api:
 
 ========
 CKAN API
diff --git a/doc/apiv3.rst b/doc/apiv3.rst
index 22624f6..f7c7b98 100644
--- a/doc/apiv3.rst
+++ b/doc/apiv3.rst
@@ -73,6 +73,7 @@ revision_show                          id
 group_show                             id
 tag_show                               id
 user_show                              id
+term_translation_show                  "**terms**" A list of strings, the terms that you want to search for translations of, e.g. "russian", "romantic novel". "**lang_codes**" A list of strings, language codes for the languages that you want to search for translations to, e.g. "en", "de". Optional, if no lang_codes are given translations to all languages will be returned.
 package_show_rest                      id
 group_show_rest                        id
 tag_show_rest                          id
@@ -122,6 +123,8 @@ group_update_rest                      (group keys)
 user_role_update                       user OR authorization_group, domain_object, roles
 user_role_bulk_update                  user_roles, domain_object
 vocabulary_update                      (vocabulary keys)
+term_translation_update                "**term**" The term that you want to create (or update) a translation for, e.g. "russian", "romantic novel". "**term_translation**" the translation of the term, e.g. "Russisch", "Liebesroman". "**lang_code**" the language code for the translation, e.g. "fr", "de".
+term_translation_update_many           "**data**" A list of dictionaries with keys matching the parameter keys for term_translation_update
 ====================================== ===========================
 
 delete.py:
@@ -205,6 +208,7 @@ key                      example value                          notes
 ======================== ====================================== =============
 id                       "b10871ea-b4ae-4e2e-bec9-a8d8ff357754" (Read-only)
 name                     "country-uk"                           (Read-only) Add/remove tags from a package or group using update_package or update_group
+display_name             "country-uk"                           (Read-only) display_name is the name of the tag that is displayed to user (as opposed to name which is used to identify the tag, e.g. in URLs). display_name is is usually the same as name but may be different, for example display_names may be translated by the ckanext-multilingual extension.
 state                    "active"                               (Read-only) Add/remove tags from a package or group using update_package or update_group
 revision_timestamp       "2009-08-08T12:46:40.920443"           (Read-only)
 vocabulary_id            "Genre"                                (Read-only) Vocabulary name or id. Optional.
@@ -230,6 +234,16 @@ name                     "Genre"
 tags                     [{"name":"government-spending"}, {"name": "climate"}] List of tags belonging to this vocabulary.
 ======================== ===================================================== =============
 
+Term Translation:
+
+================ ========================= ==================================
+key              example value             notes
+================ ========================= ==================================
+term             "russian"                 The term that is being translated.
+term_translation "Russisch"                The translation of the term.
+lang_code        "de"                      The language of the translation, a language code string.
+================ ========================= ==================================
+
 Parameters
 ==========
 
diff --git a/doc/i18n.rst b/doc/i18n.rst
index 07c2c50..7361525 100644
--- a/doc/i18n.rst
+++ b/doc/i18n.rst
@@ -1,3 +1,5 @@
+.. _i18n:
+
 =====================
 Internationalize CKAN
 =====================
diff --git a/doc/index.rst b/doc/index.rst
index 4fe9aaf..ed011a7 100644
--- a/doc/index.rst
+++ b/doc/index.rst
@@ -39,6 +39,7 @@ Customizing and Extending
    datastore
    background-tasks
    geospatial
+   multilingual
 
 Publishing Datasets
 ===================
diff --git a/doc/multilingual.rst b/doc/multilingual.rst
new file mode 100644
index 0000000..a465b04
--- /dev/null
+++ b/doc/multilingual.rst
@@ -0,0 +1,39 @@
+=====================================
+Translating Datasets, Groups and Tags
+=====================================
+
+For translating CKAN's web interface see :ref:`i18n`. In addition to user interface internationalization, a CKAN administrator can also enter translations into CKAN's database for terms that may appear in the contents of datasets, groups or tags created by users. When a user is viewing the CKAN site, if the translation terms database contains a translation in the user's language for the name or description of a dataset or resource, the name of a tag or group, etc. then the translated term will be shown to the user in place of the original.
+
+Setup and Configuration
+-----------------------
+
+By default term translations are disabled. To enable them, you have to specify the multilingual plugins using the ``ckan.plugins`` setting in your CKAN configuration file, for example:
+
+::
+
+  # List the names of CKAN extensions to activate.
+  ckan.plugins = multilingual_dataset multilingual_group multilingual_tag
+
+Of course, you won't see any terms getting translated until you load some term translations into the database. You can do this using the ``term_translation_update`` and ``term_translation_update_many`` actions of the CKAN API, See :ref:`api` for more details.
+
+Loading Test Translations
+-------------------------
+
+If you want to quickly test the term translation feature without having to provide your own translations, you can load CKAN's test translations into the database by running this command from your shell:
+
+::
+
+  paster --plugin=ckan create-test-data translations
+
+See :ref:`paster` for more details.
+
+Testing The Multilingual Extension
+----------------------------------
+
+If you have a source installation of CKAN you can test the multilingual extension by running the tests located in ``ckanext/multilingual/tests``. You must first install the packages needed for running CKAN tests into your virtual environment, and then run this command from your shell:
+
+::
+
+  nosetests --ckan ckanext/multilingual/tests
+
+See :ref:`basic-tests` for more information.
diff --git a/doc/paster.rst b/doc/paster.rst
index 19d197b..bf94e9f 100644
--- a/doc/paster.rst
+++ b/doc/paster.rst
@@ -1,3 +1,5 @@
+.. _paster:
+
 ===============================
 Common CKAN Administrator Tasks
 ===============================
diff --git a/doc/post-installation.rst b/doc/post-installation.rst
index 5e39124..5ef039f 100644
--- a/doc/post-installation.rst
+++ b/doc/post-installation.rst
@@ -45,7 +45,13 @@ It can be handy to have some test data to start with. You can get test data like
     paster --plugin=ckan create-test-data --config=/etc/ckan/std/std.ini
 
 You now have a CKAN instance that you can log in to, with some test data to check everything
-works. 
+works.
+
+You can also create various specialised test data collections for testing specific features of CKAN. For example, ``paster --plugin=ckan create-test-data translations`` creates some test data with some translations for testing the ckanext-multilingual extension. For more information, see:
+
+::
+
+    paster --plugin=ckan create-test-data --help
 
 .. _deployment-notes:
 


================================================================
  Commit: 0a7dc1d8f4a068e6f72e11fbcb8339bf3acf1b60
      https://github.com/okfn/ckan/commit/0a7dc1d8f4a068e6f72e11fbcb8339bf3acf1b60
  Author: Sean Hammond <seanhammond at lavabit.com>
  Date:   2012-04-17 (Tue, 17 Apr 2012)

  Changed paths:
    M ckan/config/environment.py
    M ckan/lib/cli.py
    M ckan/lib/helpers.py
    M ckan/plugins/interfaces.py
    M ckan/public/scripts/application.js
    M ckanext/organizations/templates/organization_form.html
    M ckanext/organizations/templates/organization_read.html
    M setup.py

  Log Message:
  -----------
  Merge branch 'master' of github.com:okfn/ckan into feature-1821-multilingual-extension

Conflicts:
	ckan/plugins/interfaces.py


diff --git a/ckan/config/environment.py b/ckan/config/environment.py
index a792b6e..fa91541 100644
--- a/ckan/config/environment.py
+++ b/ckan/config/environment.py
@@ -104,6 +104,20 @@ def find_controller(self, controller):
     else:
         config['pylons.h'] = h
 
+    # extend helper functions with ones supplied by plugins
+    from ckan.plugins import PluginImplementations
+    from ckan.plugins.interfaces import ITemplateHelpers
+
+    extra_helpers = []
+    for plugin in PluginImplementations(ITemplateHelpers):
+        helpers = plugin.get_helpers()
+        for helper in helpers:
+            if helper in extra_helpers:
+                raise Exception('overwritting extra helper %s' % helper)
+            extra_helpers.append(helper)
+            setattr(config['pylons.h'], helper, helpers[helper])
+
+
     ## redo template setup to use genshi.search_path (so remove std template setup)
     template_paths = [paths['templates'][0]]
     extra_template_paths = config.get('extra_template_paths', '')
diff --git a/ckan/lib/cli.py b/ckan/lib/cli.py
index 008cf7d..5146aa9 100644
--- a/ckan/lib/cli.py
+++ b/ckan/lib/cli.py
@@ -375,6 +375,66 @@ def command(self):
         else:
             print 'Command %s not recognized' % cmd
 
+
+class RDFExport(CkanCommand):
+    '''
+    This command dumps out all currently active datasets as RDF into the
+    specified folder.
+
+    Usage:
+      paster rdf-export /path/to/store/output
+    '''
+    summary = __doc__.split('\n')[0]
+    usage = __doc__
+
+    def command(self):
+        self._load_config()
+
+        if not self.args:
+            # default to run
+            print RDFExport.__doc__
+        else:
+            self.export_datasets( self.args[0] )
+
+    def export_datasets(self, out_folder):
+        '''
+        Export datasets as RDF to an output folder.
+        '''
+        import urlparse
+        import urllib2
+        import pylons.config as config
+        import ckan.model as model
+        import ckan.logic as logic
+        import ckan.lib.helpers as h
+
+        # Create output folder if not exists
+        if not os.path.isdir( out_folder ):
+            os.makedirs( out_folder )
+
+        fetch_url = config['ckan.site_url']
+        user = logic.get_action('get_site_user')({'model': model, 'ignore_auth': True}, {})
+        context = {'model': model, 'session': model.Session, 'user': user['name']}
+        dataset_names = logic.get_action('package_list')(context, {})
+        for dataset_name in dataset_names:
+            dd = logic.get_action('package_show')(context, {'id':dataset_name })
+            if not dd['state'] == 'active':
+                continue
+
+            url = h.url_for( controller='package',action='read',
+                                                  id=dd['name'])
+
+            url = urlparse.urljoin(fetch_url, url[1:]) + '.rdf'
+            try:
+                fname = os.path.join( out_folder, dd['name'] ) + ".rdf"
+                r = urllib2.urlopen(url).read()
+                with open(fname, 'wb') as f:
+                    f.write(r)
+            except IOError, ioe:
+                sys.stderr.write( str(ioe) + "\n" )
+
+
+
+
 class Sysadmin(CkanCommand):
     '''Gives sysadmin rights to a named user
 
@@ -720,7 +780,7 @@ class Celery(CkanCommand):
     summary = __doc__.split('\n')[0]
     usage = __doc__
 
-    def command(self):        
+    def command(self):
         if not self.args:
             self.run_()
         else:
diff --git a/ckan/lib/helpers.py b/ckan/lib/helpers.py
index 7996e7f..0824c15 100644
--- a/ckan/lib/helpers.py
+++ b/ckan/lib/helpers.py
@@ -659,6 +659,7 @@ def snippet(template_name, **kw):
     globs = kw
     globs['h'] = pylons_globs['h']
     globs['c'] = pylons_globs['c']
+    globs['config'] = pylons_globs['config']
     stream = template.generate(**globs)
     for item in PluginImplementations(IGenshiStreamFilter):
         stream = item.filter(stream)
diff --git a/ckan/plugins/interfaces.py b/ckan/plugins/interfaces.py
index c893ba4..26a9f46 100644
--- a/ckan/plugins/interfaces.py
+++ b/ckan/plugins/interfaces.py
@@ -15,6 +15,7 @@
     'IActions', 'IResourceUrlChange', 'IDatasetForm',
     'IGroupForm',
     'ITagController',
+    'ITemplateHelpers',
 ]
 
 from inspect import isclass
@@ -401,6 +402,16 @@ def get_auth_functions(self):
         implementation overrides
         """
 
+class ITemplateHelpers(Interface):
+    """
+    Allow adding extra template functions available via h variable
+    """
+    def get_helpers(self):
+        """
+        Should return a dict, the keys being the name of the helper
+        function and the values being the functions themselves.
+        """
+
 class IDatasetForm(Interface):
     """
     Allows customisation of the package controller as a plugin.
diff --git a/ckan/public/scripts/application.js b/ckan/public/scripts/application.js
index 40513fd..995e703 100644
--- a/ckan/public/scripts/application.js
+++ b/ckan/public/scripts/application.js
@@ -537,7 +537,7 @@ CKAN.View.Resource = Backbone.View.extend({
     }
     self.updateIconTimer = setTimeout(function() {
         // AJAX to server API
-        $.getJSON('/api/2/util/resource/format_icon?format='+encodeURIComponent(self.formatBox.val()), function(data) {
+        $.getJSON(CKAN.SITE_URL + '/api/2/util/resource/format_icon?format='+encodeURIComponent(self.formatBox.val()), function(data) {
           if (data && data.icon && data.format==self.formatBox.val()) {
             self.li.find('.js-resource-icon').attr('src',data.icon);
             self.table.find('.js-resource-icon').attr('src',data.icon);
diff --git a/ckanext/organizations/templates/organization_form.html b/ckanext/organizations/templates/organization_form.html
index df0b9d7..5a1dc6a 100644
--- a/ckanext/organizations/templates/organization_form.html
+++ b/ckanext/organizations/templates/organization_form.html
@@ -1,122 +1,145 @@
+<form
+  class="form-horizontal ${'has-errors' if errors else ''}"
+  id="group-edit"
+  action=""
+  method="post"
+  xmlns:i18n="http://genshi.edgewall.org/i18n"
+  xmlns:py="http://genshi.edgewall.org/"
+  xmlns:xi="http://www.w3.org/2001/XInclude">
 
-<form id="organization-edit" action="" method="post"
-    py:attrs="{'class':'has-errors'} if errors else {}"
-    xmlns:i18n="http://genshi.edgewall.org/i18n"
-    xmlns:py="http://genshi.edgewall.org/"
-    xmlns:xi="http://www.w3.org/2001/XInclude">
+<xi:include href="_util.html" />
 
 <div class="error-explanation" py:if="error_summary">
 <h2>Errors in form</h2>
 <p>The form contains invalid entries:</p>
 <ul>
-  <li py:for="key, error in error_summary.items()">${"%s: %s" % (key, error)}</li>
+  <li py:for="key, error in error_summary.items()">${"%s: %s" % (key if not key=='Name' else 'URL', error)}</li>
 </ul>
 </div>
 
-<input type="hidden" id="type" name="type" value="organization" />
-<input type="hidden" id="approval_status" name="approval_status" value="pending" />
-
 <fieldset id="basic-information">
-  <dl>
-    <dt><label class="field_opt" for="name">Organization name</label></dt>
-    <dd><input class="js-title" id="title" name="title" type="text" value="${data.get('title', '')}"/></dd>
-
-
-    <dt><label class="field_opt" for="title">Url</label></dt>
-    <dd class="name-field">
-      <span class="js-url-text url-text">${g.site_url+h.url_for('organization_index')+'/'}<span class="js-url-viewmode js-url-suffix"> </span><a style="display: none;" href="#" class="url-edit js-url-editlink js-url-viewmode">(edit)</a></span>
-      <input style="display: none;" id="name" maxlength="100" name="name" type="text" class="url-input js-url-editmode js-url-input" value="${data.get('name', '')}" />
+  <div class="control-group">
+    <label for="name" class="control-label">Title</label>
+    <div class="controls">
+      <input class="js-title" id="title" name="title" type="text" value="${data.get('title', '')}"/>
+    </div>
+  </div>
+  <div class="control-group">
+    <label for="title" class="control-label">Url</label>
+    <div class="controls">
+      <div class="input-prepend">
+        <span class="add-on">${h.url(controller='group', action='index')+'/'}</span>
+        <input maxlength="100" name="name" type="text" class="js-url-input" value="${data.get('name', '')}" />
+      </div>
       <p class="js-url-is-valid"> </p>
-    </dd>
-    <dd style="display: none;" class="js-url-editmode instructions basic">2+ chars, lowercase, using only 'a-z0-9' and '-_'</dd>
-    <dd class="field_error" py:if="errors.get('name', '')">${errors.get('name', '')}</dd>
-
-    <dt class="description-label"><label class="field_opt" for="title">Organization Description</label></dt>
-    <dd class="description-field"><div class="markdown-editor">
-      <ul class="button-row">
-        <li><button class="pretty-button js-markdown-edit depressed">Edit</button></li>
-        <li><button class="pretty-button js-markdown-preview">Preview</button></li>
-      </ul>
-      <textarea class="markdown-input" name="description" id="notes" placeholder="${_('Start with a summary sentence ...')}">${data.get('description','')}</textarea>
-      <div class="markdown-preview" style="display: none;"></div>
-      <span class="hints">You can use <a href="http://daringfireball.net/projects/markdown/syntax" target="_blank">Markdown formatting</a> here.</span>
-    </div></dd>
-
+      <p class="url-is-long">Warning: URL is very long. Consider changing it to something shorter.</p>
+      <p>2+ characters, lowercase, using only 'a-z0-9' and '-_'</p>
+      <p class="field_error" py:if="errors.get('name', '')">${errors.get('name', '')}</p>
+    </div>
+  </div>
+  <div class="control-group">
+    <label for="" class="control-label">Description</label>
+    <div class="controls">
+      ${markdown_editor('description', data.get('description'), 'notes', _('Start with a summary sentence ...'))}
+    </div>
+  </div>
   <div class="control-group">
     <label for="name" class="control-label">Image URL:</label>
     <div class="controls">
       <input id="image_url" name="image_url" type="text" value="${data.get('image_url', '')}"/>
-      <p>The URL for the image that is associated with this group.</p>
+      <p>The URL for the image that is associated with this organization.</p>
     </div>
   </div>
+  <div class="state-field control-group" py:if="c.is_sysadmin or c.auth_for_change_state">
+    <label for="" class="control-label">State</label>
+    <div class="controls">
+      <select id="state" name="state" >
+        <option py:attrs="{'selected': 'selected' if data.get('state') == 'active' else None}" value="active">active</option>
+        <option py:attrs="{'selected': 'selected' if data.get('state') == 'deleted' else None}" value="deleted">deleted</option>
+      </select>
+    </div>
+  </div>
+</fieldset>
 
-
-    <dt class="parent-label" py:if="c.is_superuser_or_groupadmin">
-		<label class="field_opt" for="parent">Parent Organization</label>
-	</dt>
-
-	<dd py:if="c.group and not c.is_superuser_or_groupadmin">
+  <div class="control-group" py:if="c.is_superuser_or_groupadmin">
+    <label class="control-label" for="parent">Parent Organization</label>
+    <div class="controls" py:if="c.group and not c.is_superuser_or_groupadmin">
 		<span py:if="c.parent is not None" class="js-title">
 			${ c.parent.title }
 		</span>
 		<span py:if="c.parent is None" class="js-title">
 			No parent organization
 		</span>
-	</dd>
-
-    <dd py:if="c.is_superuser_or_groupadmin" class="parent-field">
+	</div>
+    <div class="controls" py:if="c.is_superuser_or_groupadmin">
 		<select id="parent" name="parent" class="chzn-select" data-placeholder="Please choose a organization">
 			<option value=""></option>
     		<py:for each="pg in c.possible_parents">
 				<option py:attrs="{'selected': 'selected' if c.parent and pg.id == c.parent.id else None}" value="${pg.id}">${pg.title}</option>
 			</py:for>
 		</select>
-	</dd>
-
+	</div>
+  </div>
 
-    <dt class="state-label" py:if="c.is_sysadmin or c.auth_for_change_state"><label class="field_opt" for="state">State</label></dt>
-    <dd class="state-field" py:if="c.is_sysadmin or c.auth_for_change_state">
-    <select id="state" name="state" >
-      <option py:attrs="{'selected': 'selected' if data.get('state') == 'active' else None}" value="active">active</option>
-      <option py:attrs="{'selected': 'selected' if data.get('state') == 'deleted' else None}" value="deleted">deleted</option>
-    </select>
-    </dd>
-  </dl>
-</fieldset>
 
 <fieldset id="extras">
   <h3>Extras</h3>
   <dl>
     <py:with vars="extras = data.get('extras', [])">
     <py:for each="num, extra in enumerate(data.get('extras', []))">
-    <dt><label for="extras__${num}__value">${extra.get('key')}</label></dt>
-    <dd>
-      <input id="extras__${num}__key" name="extras__${num}__key" type="hidden" value="${extra.get('key')}" />
-      <input id="extras__${num}__value" name="extras__${num}__value" type="text" value="${extra.get('value')}" />
-      <input type="checkbox" name="extras__${num}__deleted" checked="${extra.get('deleted')}">Delete</input>
-    </dd>
+    <div class="control-group">
+      <label class="control-label" for="extras__${num}__value">${extra.get('key')}</label>
+      <div class="controls">
+        <input id="extras__${num}__key" name="extras__${num}__key" type="hidden" value="${extra.get('key')}" />
+        <input id="extras__${num}__value" name="extras__${num}__value" type="text" value="${extra.get('value')}" />
+        <label class="checkbox" style="display: inline-block;">
+          <input type="checkbox" name="extras__${num}__deleted" checked="${extra.get('deleted')}" />Delete
+        </label>
+      </div>
+    </div>
     </py:for>
-
+    <hr py:if="len(extras)" class="extras-divider" />
     <py:for each="num in range(len(extras), len(extras) + 4)">
-    <dt><label for="extras__${num}__key">New key</label></dt>
-    <dd>
-      <input class="medium-width" id="extras__${num}__key" name="extras__${num}__key" type="text" />
-      with value
-      <input class="medium-width" id="extras__${num}__value" name="extras__${num}__value" type="text" />
-    </dd>
+      <div class="control-group">
+        <label class="control-label" for="extras__${num}__key">Add...</label>
+        <div class="controls">
+          <label>
+            <span class="extras-label">Key =</span>
+            <input class="medium-width" id="extras__${num}__key" name="extras__${num}__key" type="text" />
+          </label>
+          <label>
+            <span class="extras-label">Value =</span>
+            <input class="medium-width" id="extras__${num}__value" name="extras__${num}__value" type="text" />
+          </label>
+        </div>
+      </div>
     </py:for>
     </py:with>
   </dl>
 </fieldset>
 
+<?python
+    import ckan.model as model
+    users = []
+    if c.group:
+        users.extend( { "name": user.name,
+                            "capacity": "admin" }
+                            for user in c.group.members_of_type( model.User, "admin"  ).all() )
+        users.extend( { "name": user.name,
+                            "capacity": "editor" }
+                            for user in c.group.members_of_type( model.User, 'editor' ).all() )
+?>
 <fieldset id="users">
   <h3>Users <span py:if="c.users">(${len(c.users.all())})</span></h3>
 <a py:if="c.group" href="${h.url_for(controller='ckanext.organizations.controllers:OrganizationController', action='users', id=c.group.name)}">Manage users</a>
 
-  <dl py:if="c.users">
-    <py:for each="user in c.users">
+  <dl py:if="users">
+    <py:for each="num, user in enumerate(users)">
 		<dd>
 			<label>${user['name']}</label>
+			<input type="hidden" name="users__${num}__name" value="${user['name']}"/>
+			<input type="hidden" name="users__${num}__capacity" value="${user['capacity']}"/>
+
 		</dd>
     </py:for>
   </dl>
@@ -124,10 +147,10 @@
 </fieldset>
 
 
-<div class="form-submit">
-  <input id="save" class="pretty-button primary" name="save" type="submit" value="${_('Save Changes')}" />
+<div class="form-actions">
+  <input id="save" class="btn btn-primary" name="save" type="submit" value="${_('Save Changes')}" />
   <py:if test="c.group">
-    <input id="cancel" class="pretty-button href-action" name="cancel" type="reset" value="${_('Cancel')}" action="${h.url_for(controller='group', action='read', id=c.group.name)}" />
+    <input id="cancel" class="btn href-action" name="cancel" type="reset" value="${_('Cancel')}" action="${h.url_for(controller='group', action='read', id=c.group.name)}" />
   </py:if>
 </div>
 </form>
diff --git a/ckanext/organizations/templates/organization_read.html b/ckanext/organizations/templates/organization_read.html
index 944192e..d946d31 100644
--- a/ckanext/organizations/templates/organization_read.html
+++ b/ckanext/organizations/templates/organization_read.html
@@ -10,6 +10,8 @@
     <py:def function="page_logo">${c.group.image_url}</py:def>
   </py:if>
 
+<input type="hidden" id="type" name="type" value="organization" />
+
   <?python
     from pylons import config
     from ckan import model
diff --git a/setup.py b/setup.py
index 3370531..33f6d6b 100644
--- a/setup.py
+++ b/setup.py
@@ -70,6 +70,7 @@
     rights = ckan.lib.authztool:RightsCommand
     roles = ckan.lib.authztool:RolesCommand
     celeryd = ckan.lib.cli:Celery
+    rdf-export = ckan.lib.cli:RDFExport
 
     [console_scripts]
     ckan-admin = bin.ckan_admin:Command


================================================================
  Commit: 35388f883f7b743d0b703fe3e51a2fafaa5e9aed
      https://github.com/okfn/ckan/commit/35388f883f7b743d0b703fe3e51a2fafaa5e9aed
  Author: Sean Hammond <seanhammond at lavabit.com>
  Date:   2012-04-17 (Tue, 17 Apr 2012)

  Changed paths:
    M ckan/lib/helpers.py
    M ckan/lib/helpers_clean.py
    M ckan/templates/facets.html

  Log Message:
  -----------
  Merge branch 'feature-1821-multilingual-extension' of github.com:okfn/ckan into feature-1821-multilingual-extension


diff --git a/ckan/lib/helpers.py b/ckan/lib/helpers.py
index 0824c15..fd26fe7 100644
--- a/ckan/lib/helpers.py
+++ b/ckan/lib/helpers.py
@@ -7,6 +7,7 @@
 """
 import email.utils
 import datetime
+import logging
 import re
 import urllib
 
@@ -48,6 +49,8 @@
 except ImportError:
     import simplejson as json
 
+_log = logging.getLogger(__name__)
+
 def redirect_to(*args, **kw):
     '''A routes.redirect_to wrapper to retain the i18n settings'''
     kw['__ckan_no_root'] = True
@@ -334,8 +337,24 @@ def _subnav_named_route(text, routename, **kwargs):
 def default_group_type():
     return str( config.get('ckan.default.group_type', 'group') )
 
+def new_facet_items(name, limit=10):
+    if not c.new_facets or \
+       not c.new_facets.get(name) or \
+       not c.new_facets.get(name).get('items'):
+        return []
+    facets = []
+    for facet_item in c.new_facets.get(name)['items']:
+        if not len(facet_item['name'].strip()):
+            continue
+        if not (name, facet_item['name']) in request.params.items():
+            facets.append(facet_item)
+    return sorted(facets, key=lambda item: item['count'], reverse=True)[:limit]
 
 def facet_items(*args, **kwargs):
+    """
+    DEPRECATED: Use the new facet data structure, and `new_facet_items()`
+    """
+    _log.warning('Deprecated function: ckan.lib.helpers:facet_items().  Will be removed in v1.8')
     # facet_items() used to need c passing as the first arg
     # this is depriciated as pointless
     # throws error if ckan.restrict_template_vars is True
diff --git a/ckan/lib/helpers_clean.py b/ckan/lib/helpers_clean.py
index 54c50c8..cc42060 100644
--- a/ckan/lib/helpers_clean.py
+++ b/ckan/lib/helpers_clean.py
@@ -28,6 +28,7 @@
            subnav_link,
            subnav_named_route,
            default_group_type,
+           new_facet_items,
            facet_items,
            facet_title,
          #  am_authorized, # depreciated
diff --git a/ckan/templates/facets.html b/ckan/templates/facets.html
index 9e135e7..05c19b5 100644
--- a/ckan/templates/facets.html
+++ b/ckan/templates/facets.html
@@ -5,51 +5,126 @@
   py:strip=""
   >
 
-<py:def function="facet_div(name, title, limit=5)">
-    <div py:if="c.new_facets[name]['items'][:limit]" class="facet-box">
+<!--
+Construct a facet <div> populated with links to filtered results.
+
+name
+  The field name identifying the facet field, eg. "tags"
+
+title
+  The title of the facet, eg. "Tags", or "Tag Cloud"
+
+label_function
+  Renders the human-readable label for each facet value.
+  If defined, this should be a callable that accepts a `facet_item`.
+  eg. lambda facet_item: facet_item.display_name.upper()
+  By default it displays the facet item's display name, which should
+  usually be good enough
+
+if_empty
+  A string, which if defined, and the list of possible facet items is empty,
+  is displayed in lieu of an empty list.
+
+count_label
+  A callable which accepts an integer, and returns a string.  This controls
+  how a facet-item's count is displayed.
+  
+-->
+<py:def function="facet_div(name, title, limit=5, label_function=lambda item: item.display_name, if_empty=None, count_label=lambda c: ' (%d)'%c)">
+    <div py:if="if_empty is not None or h.new_facet_items(name, limit)" class="facet-box">
         <h2>${h.facet_title(title)}</h2>
         <ul class="facet-options">
-            <li py:for="facet_item in c.new_facets[name]['items'][:limit]"
+            <li py:for="facet_item in h.new_facet_items(name, limit)"
                 py:if="not (name, facet_item.name) in c.fields">
                 <a href="${c.drill_down_url(**{name: facet_item.name})}">
-                    ${facet_item.display_name}
+                    ${label_function(facet_item)}
                 </a>
-                (${facet_item['count']})
+                ${count_label(facet_item['count'])}
             </li>
         </ul>
+        <p py:if="not h.new_facet_items(name, limit)">${if_empty}</p>
     </div>
 </py:def>
 
+<!--
+DEPRECATED.  Provided only for backward compatibility with existing plugins.
+             Use `facet_div` instead.
+
+Similar to the above, `facet_div` function; this helper creates a <div>
+populated with links to filtered search results.
+
+Differences with the preferred `facet_div` function:
+
+ * `title` argument is a callable
+ * `label` is a callable that accepts a string cf. `label_function` which is
+   a callable that accepts a `facet_item`.
+
+code
+  The field name identifying the facet field, eg. "tags"
+
+title
+  A callable used to render the title for the facet.  The callable must accept
+  one string argument, the `code` argument passed in above.  Obviously, this
+  is a bit redundant.
+
+label
+  Renders the human-readable label for each facet value.
+  If defined, this should be a callable that accepts a facet_item's name as a
+  string.
+
+if_empty
+  A string, which if defined, and the list of possible facet items is empty,
+  is displayed in lieu of an empty list.
+
+count_label
+  A callable which accepts an integer, and returns a string.  This controls
+  how a facet-item's count is displayed.
+  
+-->
 <py:def function="facet_sidebar(code, limit=5, label=lambda n: n, title=h.facet_title, if_empty=None, count_label=lambda c: ' (%d)'%c)">
-    <div py:if="if_empty is not None or len(h.facet_items(code, limit=limit))" class="facet-box">
+    <?python
+      import logging
+      logging.getLogger('ckan.templates.facets').warning("Deprecated function: ckan/templates/facets.html:facet_sidebar()")
+    ?>
+    <div py:if="if_empty is not None or h.new_facet_items(code, limit)" class="facet-box">
         <h2>${title(code)}</h2>
         <ul class="facet-options">
-            <li py:for="name, count in h.facet_items(code, limit=limit)"
-                py:if="not (code, name) in c.fields">
-                  <a href="${c.drill_down_url(**{code: name})}">
-                  <span py:if="'format' in code.lower()">${h.icon(h.format_icon(name))}</span>
-                    ${label(name)}</a>${count_label(count)}
+            <li py:for="facet_item in h.new_facet_items(code, limit)"
+                py:if="not (code, facet_item.name) in c.fields">
+                  <a href="${c.drill_down_url(**{code: facet_item.name})}">
+                  <span py:if="'format' in code.lower()">${h.icon(h.format_icon(facet_item.name))}</span>
+                    ${label(facet_item.name)}</a>${count_label(facet_item['count'])}
             </li>
         </ul>
-        <p py:if="not len(h.facet_items(code, limit=limit))">${if_empty}</p>
+        <p py:if="not h.new_facet_items(code, limit)">${if_empty}</p>
     </div>
 </py:def>
 
+<!--
+Construct a possibly empty list of <li> elements containing links to further
+search results
+
+This is different from `facet_sidebar` and `facet_div` (above) in that it
+requires the caller to wrap up the resulting <li> elements in whatever dom
+element they need.  But it *does* allow for filters to displayed in a
+hierarchy.
+
+label
+  Renders the human-readable label for each facet value.
+  If defined, this should be a callable that accepts a facet_item's name as a
+  string.
+
+if_empty
+  If if_empty is not None and there are no facets to filter on, then a single
+  <li> element is generated, with the text specified by if_empty
+
+-->
 <py:def function="facet_list_items(code, limit=5, label=lambda n: n, if_empty=None)">
-        <!-- Creates a possibly empty list of <li> elements containing links to
-        further search results
-
-        This is different from facet_sidebar() (above) in that it requires the
-        caller to wrap up the resulting <li> elements in whatever dom element
-        they need.  But it does allow for filters to displayed in a hierarchy.
-
-        If if_empty is not None and there are no facets to filter on, then a
-        single <li> element is generated, with the text specified by if_empty
-        -->
-        <li py:if="if_empty and len(h.facet_items(code, limit=limit)) == 0">${if_empty}</li>
-        <li py:for="name, count in h.facet_items(code, limit=limit)"
-            py:if="not (code, name) in c.fields">
-            <a href="${c.drill_down_url(**{code: name})}">${label(name)}</a> (${count})
+        
+        <li py:if="if_empty and not h.new_facet_items(code, limit)">${if_empty}</li>
+        <li py:for="facet_item in h.new_facet_items(code, limit)"
+            py:if="not (code, facet_item.name) in c.fields">
+            <a href="${c.drill_down_url(**{code: facet_item.name})}">${label(facet_item.name)}</a> (facet_item['count'])
         </li>
 </py:def>
 


================================================================
Compare: https://github.com/okfn/ckan/compare/dabd498...35388f8


More information about the ckan-changes mailing list