Friday 23 January 2015

Linking to static pages from the Navbar correctly in CKAN

I ran into some issues trying to link to a static page in CKAN from the NavBar. I figured out a solution through lots of hacking and browsing through existing codebases. Here's a write up of the problem and solution.

Creating a New Static Page

In CKAN, you may want to add a new static page. The docs give a bit of help on how to add static files but not new pages.
It's easy enough by putting a page in your extensions templates folder, updating your config to load that templates folder, and linking to your page from the header.html file amidst the other navbar items. For example:

In plugin.py
 def update_config()
        here = os.path.dirname(__file__)
        rootdir = os.path.dirname(os.path.dirname(here))
        template_dir = os.path.join(rootdir, 'ckanext',
                                    '<ext_name>', 'templates')
        config['extra_template_paths'] = ','.join([template_dir,
                config.get('extra_template_paths', '')])

Where your file 'my_new_page.html' is your new static file in ckanext/<ext_name>/templates/

Copy templates/header.html from the CKAN source directory to the extensions templates directory e.g. ckanext/<ext_name>/templates/header.html
Then add a list item and href to your new page inside the block header_site_navigation as such:

<li><a href="/my_new_page.html"></a></li>

Et voila!

Making the Link Smarter

If you want to make it like the other Navbar links that become highlighted when the user is currently on that page it gets a little tricky (but can be done). In the templates/header.html you will have added a list item to; you will have seen this helper method: build_nav_main. Like so:
 {{ h.build_nav_main(
                ('search', _('Datasets'))) }}

You can use this method to construct your link which will become highlighted when the page is active. The right parameter is the text that will displayed in the link, this can be anything. The left link is CKANs internal alias for the page. It stores this internal alias alongside the URL path and uses this key/value to to construct the link and ascertain whether you're on the page.

To create this you will need to make use of controllers and implement the IRoutes interface.

The IRoutes interface has the function before_map() which adds custom routes.
The first parameters is your internal alias.
The second is the relative URL path
The third and fourth must specify a controller and an action on the controller.
The 3rd and 4th parameter cannot be replaced with an absolute URL - it must be controller and action.
Include this into your plugin.py

    plugins.implements(plugins.IRoutes, inherit=True)
    def before_map(self, map):
        map.connect('my_page', '/my_page', controller='ckanext.myextension.controller:SpecialRouteController', action='my_page')
        return map

This will add a route aliased as 'my_page' which on the URL /my_page calls an action on the SpecialRouteController.

Create a controller.py in the same directory as your plugin.py.    
In your controller.py you need to add something like this:

import ckan.lib.base as base
from ckan.controllers.home import HomeController
class SpecialRouteController(HomeController):
    def my_page(self):
        return base.render('/my_new_page.html')
Then go back to your header.html file and add something like this:
 {{ h.build_nav_main(
                ('my_page', _('Datasets'))) }}

Now when the build_nav_main constructs the link, it will lookup my_page in the routes, call the my_page action on the SpecialRoutesController and render your static file in the templates directory.


Monday 19 January 2015

Filtering by Vocabulary Tags in CKAN

CKAN has great documentation for learning how to add vocabulary tags to a dataset form, but the tutorial is cut a bit short in that once you've added a vocabulary to a dataset; a common use-case would be to filter your search results by your tags. The tutorial doesn't show you how to. This blog post gives some advice on how to accomplish this but I didn't find this to work (at least on the version I'm using - v2.2.1).

What worked for me was to add my vocabulary to the datasets' facets using the 'dataset_facets()' function in the IFacets interface.

To do that we need to first find the name of the vocabulary. I think the format rule is vocab_<vocabulary_name>. But to be sure, you can check as I did (though I'm sure there are better ways than this to find out):

First I tagged one of my datasets with one of the tags in my vocabulary (in this case 'Finland'). I then went to solr (at http://localhost:8983/solr/admin/) and queried for the string.
The XML results had a snippet containing this:
<arr name="vocab_country_codes">
<str>Finland</str>
</arr>
This shows that vocab_country_codes was the internal name used by CKAN. Once you've assertained the internal name for your vocabulary, go to your extensions plugin.py and add it as a facet to your dataset.
    p.implements(p.IFacets, inherit=True)
    def dataset_facets(self, facets_dict, package_type):
        facets_dict['vocab_country_codes'] = p.toolkit._('Country Codes')
        return facets_dict
Restart your server and you should find the filter option for your vocabulary on the dataset index page. The IFacets interface also has group_facets() and organization_facets() which can be used in the same way.


Tuesday 4 June 2013

Upgrading to Rails 3



This blog is simply just a narrative of my progress upgrading a large rails 2.3.18 project to rails 3. Hopefully it might make someone's life a bit easier.

Specs From: Rails 2.3.18, Ruby 1.8.7 and Ubuntu 12.04
Specs To: Rails 3.0.6, Ruby 1.8.7 and Ubuntu 12.04


I started on a new branch working my way through the omgbloglol guide to upgrading rails 2 to 3. Rather than installing rails through git clone, I included it in the gemfile as the current head state is into the Rails 4


Include:

gem “rails”, “<=3.0.6”

in the Gemfile and run bundle install to install rails.


The first problem was factory_girl latest gem requires ruby 1.9.2. I was hoping to keep working in baby steps by using ruby1.8.7 until rails 3.0.X is up, running and tested so I've specified version 2.6.5 in the Gemfile which is the last version to work for ruby 1.8.7.
gem “factory_girl”, “<=2.6.5”


Next issue was courtesy of rdoc, or so I thought

$ rails --version
ERROR: 'rake/rdoctask' is obsolete and no longer supported. Use 'rdoc/task' (available in RDoc 2.4.2+) instead.
/home/niall/.rvm/gems/ruby-1.8.7-p371/gems/rake-10.0.4/lib/rake/rdoctask.rb:1
/home/niall/.rvm/gems/ruby-1.8.7-p371/gems/rails-0.9.5/Rakefile:3:in `require'
/home/niall/.rvm/gems/ruby-1.8.7-p371/gems/rails-0.9.5/Rakefile:3

Checking the Rakefile, everything seemed okay - though as it happens, we have many baked-in plugins that have their own Rakefiles which has the obsolete require style included. Once this was replaced I got the same error. Reading the error a little more carefully I noted it says rails-0.9.5
I was asking for any rails  less than 3.X, so when another gem ‘validates_url_format_of’ required activerecord version have 2.3.4, Bundler used the desired activerecord and found a version of rails to fit in with that which happens to be as old as mankind. Specifying which rails version to use confirms this:

$ bundle update
Bundler could not find compatible versions for gem "activerecord":
 In Gemfile:
   validates_url_format_of (>= 0) ruby depends on
     activerecord (~> 2.3.4) ruby

   rails (= 3.0.6) ruby depends on
     activerecord (3.0.6)

If you've been getting this error it's probable the gem is incompatible. You'll need to find an alternative or lower your rails version to match. I commented out the gem validates_url_format_of and used an alternative.

After running rails new to get the new files necessary for a rails 3 app, I felt it was a good time to keep git up to speed. Running 

$ git status 

shows absolutely boat loads of script files have been modified. git diff says old mode <number> new mode <number>. These mean files have changed permission, in this case from when rails new was ran.

If you wish to ignore these on git, run:
$ git config core.filemode falsecour
Commit any new files that running rails new have created/modified.

subclasses_of is now deprecated is another error encountered along our path.
One of the plugins kept in the vendor folder has a line containing something to the extent of: 

Object.subclasses_of(SomeClass).each

which has now been deprecated. The appropriate translation for this is: 

SomeClass.descendants.each

The final issue we faced was this error message:

`deprecation_caller_message': stack level too deep (SystemStackError) with a total of  4763 levels in the error stack.

This error is due to recursion in the stack trace. Ours was because we used a logger called better_logging which used the deprecated constant RAILS_ROOT rather than the new method Rails.env. In order to deliver the error message, the error handler would call better_logging which would crash, which would trigger the error handler to call better_logging which carries on until the error stack overflow-eth. 
Sorry to be of little help but we simply switched to using an up-to-speed gem rather than the plugin when this happened.