Extend the vocabularies example
Extend the vocabularies example

Add two example vocabularies: Genre and Composer

Show a dataset's vocabulary tags in the sidebar on the dataset read page

Vocabulary tags cannot be linked to the tag's page, because their aren't
URLs for showing tags with vocabularies yet.

file:a/README.rst -> file:b/README.rst
--- a/README.rst
+++ b/README.rst
@@ -12,6 +12,11 @@
 * A custom Pylons controller for overriding some core CKAN behaviour
 
 * A custom Package edit form
+
+* A custom Group edit form
+
+* A plugin that allows for custom forms to be used for datasets based on 
+  their "type".
 
 * A custom User registration and edition form
 

--- a/ckanext/example/controller_plugin.py
+++ /dev/null
@@ -1,223 +1,1 @@
-import logging
-from ckan.lib.base import BaseController, render, c, model, abort, request
-from ckan.lib.base import redirect, _, config, h
-import ckan.logic.action.create as create
-import ckan.logic.action.update as update
-import ckan.logic.action.get as get
-from ckan.logic.converters import date_to_db, date_to_form, convert_to_extras, convert_from_extras
-from ckan.lib.navl.dictization_functions import DataError, flatten_dict, unflatten
-from ckan.logic import NotFound, NotAuthorized, ValidationError
-from ckan.logic import tuplize_dict, clean_dict, parse_params
-from ckan.logic.schema import package_form_schema
-from ckan.plugins import IDatasetForm
-from ckan.plugins import implements, SingletonPlugin
-from ckan.lib.package_saver import PackageSaver
-from ckan.lib.field_types import DateType, DateConvertError
-from ckan.authz import Authorizer
-from ckan.lib.navl.dictization_functions import Invalid
-from ckanext.dgu.forms.package_gov_fields import GeoCoverageType
-from ckan.lib.navl.dictization_functions import validate, missing
-import ckan.logic.validators as val
-import ckan.logic.schema as default_schema
-from ckan.lib.navl.validators import (ignore_missing,
-                                      not_empty,
-                                      empty,
-                                      ignore,
-                                      keep_extras,
-                                     )
 
-log = logging.getLogger(__name__)
-
-geographic_granularity = [('', ''),
-                          ('national', 'national'),
-                          ('regional', 'regional'),
-                          ('local authority', 'local authority'),
-                          ('ward', 'ward'),
-                          ('point', 'point'),
-                          ('other', 'other - please specify')]
-
-update_frequency = [('', ''),
-                    ('never', 'never'),
-                    ('discontinued', 'discontinued'),
-                    ('annual', 'annual'),
-                    ('quarterly', 'quarterly'),
-                    ('monthly', 'monthly'),
-                    ('other', 'other - please specify')]
-
-temporal_granularity = [("",""),
-                       ("year","year"),
-                       ("quarter","quarter"),
-                       ("month","month"),
-                       ("week","week"),
-                       ("day","day"),
-                       ("hour","hour"),
-                       ("point","point"),
-                       ("other","other - please specify")]
-
-
-class ExamplePackageController(SingletonPlugin):
-
-    implements(IDatasetForm, inherit=True)
-
-    def package_form(self):
-        return 'controller/package_plugin.html'
-
-    def is_fallback(self):
-        """
-        Returns true iff this provides the fallback behaviour, when no other
-        plugin instance matches a package's type.
-
-        As this is not the fallback controller we should return False.  If 
-        we were wanting to act as the fallback, we'd return True
-        """
-        return False
-
-    def package_types(self):
-        """
-        Returns an iterable of package type strings.
-
-        If a request involving a package of one of those types is made, then
-        this plugin instance will be delegated to.
-
-        There must only be one plugin registered to each package type.  Any
-        attempts to register more than one plugin instance to a given package
-        type will raise an exception at startup.
-        """
-        return ["example"]
-
-    def _setup_template_variables(self, context, data_dict=None):
-        c.licences = [('', '')] + model.Package.get_license_options()
-        c.geographic_granularity = geographic_granularity
-        c.update_frequency = update_frequency
-        c.temporal_granularity = temporal_granularity 
-
-        c.publishers = self.get_publishers()
-
-        c.is_sysadmin = Authorizer().is_sysadmin(c.user)
-        c.resource_columns = model.Resource.get_columns()
-
-        ## This is messy as auths take domain object not data_dict
-        pkg = context.get('package') or c.pkg
-        if pkg:
-            c.auth_for_change_state = Authorizer().am_authorized(
-                c, model.Action.CHANGE_STATE, pkg)
-
-    def _form_to_db_schema(self):
-
-        schema = {
-            'title': [not_empty, unicode],
-            'name': [not_empty, unicode, val.name_validator, val.package_name_validator],
-            'notes': [not_empty, unicode],
-
-            'date_released': [date_to_db, convert_to_extras],
-            'date_updated': [date_to_db, convert_to_extras],
-            'date_update_future': [date_to_db, convert_to_extras],
-            'update_frequency': [use_other, unicode, convert_to_extras],
-            'update_frequency-other': [],
-            'precision': [unicode, convert_to_extras],
-            'geographic_granularity': [use_other, unicode, convert_to_extras],
-            'geographic_granularity-other': [],
-            'geographic_coverage': [ignore_missing, convert_geographic_to_db, convert_to_extras],
-            'temporal_granularity': [use_other, unicode, convert_to_extras],
-            'temporal_granularity-other': [],
-            'temporal_coverage-from': [date_to_db, convert_to_extras],
-            'temporal_coverage-to': [date_to_db, convert_to_extras],
-            'url': [unicode],
-            'taxonomy_url': [unicode, convert_to_extras],
-
-            'resources': default_schema.default_resource_schema(),
-            
-            'published_by': [not_empty, unicode, convert_to_extras],
-            'published_via': [ignore_missing, unicode, convert_to_extras],
-            'author': [ignore_missing, unicode],
-            'author_email': [ignore_missing, unicode],
-            'mandate': [ignore_missing, unicode, convert_to_extras],
-            'license_id': [ignore_missing, unicode],
-            'tag_string': [ignore_missing, val.tag_string_convert],
-            'national_statistic': [ignore_missing, convert_to_extras],
-            'state': [val.ignore_not_admin, ignore_missing],
-
-            'log_message': [unicode, val.no_http],
-
-            '__extras': [ignore],
-            '__junk': [empty],
-        }
-        return schema
-    
-    def _db_to_form_schema(data):
-        schema = {
-            'date_released': [convert_from_extras, ignore_missing, date_to_form],
-            'date_updated': [convert_from_extras, ignore_missing, date_to_form],
-            'date_update_future': [convert_from_extras, ignore_missing, date_to_form],
-            'update_frequency': [convert_from_extras, ignore_missing, extract_other(update_frequency)],
-            'precision': [convert_from_extras, ignore_missing],
-            'geographic_granularity': [convert_from_extras, ignore_missing, extract_other(geographic_granularity)],
-            'geographic_coverage': [convert_from_extras, ignore_missing, convert_geographic_to_form],
-            'temporal_granularity': [convert_from_extras, ignore_missing, extract_other(temporal_granularity)],
-            'temporal_coverage-from': [convert_from_extras, ignore_missing, date_to_form],
-            'temporal_coverage-to': [convert_from_extras, ignore_missing, date_to_form],
-            'taxonomy_url': [convert_from_extras, ignore_missing],
-
-            'resources': default_schema.default_resource_schema(),
-            'extras': {
-                'key': [],
-                'value': [],
-                '__extras': [keep_extras]
-            },
-            'tags': {
-                '__extras': [keep_extras]
-            },
-            
-            'published_by': [convert_from_extras, ignore_missing],
-            'published_via': [convert_from_extras, ignore_missing],
-            'mandate': [convert_from_extras, ignore_missing],
-            'national_statistic': [convert_from_extras, ignore_missing],
-            '__extras': [keep_extras],
-            '__junk': [ignore],
-        }
-        return schema
-
-    def _check_data_dict(self, data_dict):
-        return
-
-    def get_publishers(self):
-        return [('pub1', 'pub2')]
-
-
-def use_other(key, data, errors, context):
-
-    other_key = key[-1] + '-other'
-    other_value = data.get((other_key,), '').strip()
-    if other_value:
-        data[key] = other_value
-
-def extract_other(option_list):
-
-    def other(key, data, errors, context):
-        value = data[key]
-        if value in dict(option_list).keys():
-            return
-        elif value is missing:
-            data[key] = ''
-            return
-        else:
-            data[key] = 'other'
-            other_key = key[-1] + '-other'
-            data[(other_key,)] = value
-    return other
-            
-def convert_geographic_to_db(value, context):
-
-    if isinstance(value, list):
-        regions = value
-    elif value:
-        regions = [value]
-    else:
-        regions = []
-        
-    return GeoCoverageType.get_instance().form_to_db(regions)
-
-def convert_geographic_to_form(value, context):
-
-    return GeoCoverageType.get_instance().db_to_form(value)
-

--- /dev/null
+++ b/ckanext/example/forms.py
@@ -1,1 +1,276 @@
-
+import os, logging
+from ckan.authz import Authorizer
+from ckan.logic.converters import convert_to_extras,\
+    convert_from_extras, convert_to_tags, convert_from_tags, free_tags_only
+from ckan.logic import get_action
+from ckan.logic.schema import package_form_schema, group_form_schema
+from ckan.lib.base import c, model
+from ckan.plugins import IDatasetForm, IGroupForm, IConfigurer, IConfigurable
+from ckan.plugins import IGenshiStreamFilter
+from ckan.plugins import implements, SingletonPlugin
+from ckan.lib.navl.validators import ignore_missing, not_empty, keep_extras
+
+log = logging.getLogger(__name__)
+
+class ExampleGroupForm(SingletonPlugin):
+    """This plugin demonstrates how a class packaged as a CKAN
+    extension might extend CKAN behaviour by providing custom forms
+    based on the type of a Group.
+
+    In this case, we implement two extension interfaces to provide custom 
+    forms for specific types of group.
+
+      - ``IConfigurer`` allows us to override configuration normally
+        found in the ``ini``-file.  Here we use it to specify where the
+        form templates can be found.
+        
+      - ``IGroupForm`` allows us to provide a custom form for a dataset
+        based on the 'type' that may be set for a group.  Where the 
+        'type' matches one of the values in group_types then this 
+        class will be used. 
+    """
+    implements(IGroupForm, inherit=True)
+    implements(IConfigurer, inherit=True)    
+    
+    def update_config(self, config):
+        """
+        This IConfigurer implementation causes CKAN to look in the
+        ```templates``` directory when looking for the group_form()
+        """
+        here = os.path.dirname(__file__)
+        rootdir = os.path.dirname(os.path.dirname(here))
+        template_dir = os.path.join(rootdir, 'ckanext',
+                                    'example', 'theme', 'templates')
+        config['extra_template_paths'] = ','.join([template_dir,
+                config.get('extra_template_paths', '')])
+            
+    def group_form(self):
+        """
+        Returns a string representing the location of the template to be
+        rendered.  e.g. "forms/group_form.html".
+        """        
+        return 'forms/group_form.html'
+
+    def group_types(self):
+        """
+        Returns an iterable of group type strings.
+
+        If a request involving a group of one of those types is made, then
+        this plugin instance will be delegated to.
+
+        There must only be one plugin registered to each group type.  Any
+        attempts to register more than one plugin instance to a given group
+        type will raise an exception at startup.
+        """
+        return ["testgroup"]
+
+    def is_fallback(self):
+        """
+        Returns true iff this provides the fallback behaviour, when no other
+        plugin instance matches a group's type.
+
+        As this is not the fallback controller we should return False.  If 
+        we were wanting to act as the fallback, we'd return True
+        """
+        return False                
+                
+    def form_to_db_schema(self):
+        """
+        Returns the schema for mapping group data from a form to a format
+        suitable for the database.
+        """
+        return group_form_schema()
+
+    def db_to_form_schema(self):
+        """
+        Returns the schema for mapping group data from the database into a
+        format suitable for the form (optional)
+        """
+        return {}
+        
+    def check_data_dict(self, data_dict):
+        """
+        Check if the return data is correct.
+
+        raise a DataError if not.
+        """
+
+    def setup_template_variables(self, context, data_dict):
+        """
+        Add variables to c just prior to the template being rendered.
+        """                
+                
+
+class ExampleDatasetForm(SingletonPlugin):
+    """This plugin demonstrates how a theme packaged as a CKAN
+    extension might extend CKAN behaviour.
+
+    In this case, we implement three extension interfaces:
+
+      - ``IConfigurer`` allows us to override configuration normally
+        found in the ``ini``-file.  Here we use it to specify where the
+        form templates can be found.
+      - ``IDatasetForm`` allows us to provide a custom form for a dataset
+        based on the type_name that may be set for a package.  Where the 
+        type_name matches one of the values in package_types then this 
+        class will be used. 
+    """
+    implements(IDatasetForm, inherit=True)
+    implements(IConfigurer, inherit=True)    
+    implements(IConfigurable)
+    implements(IGenshiStreamFilter)
+    
+    def update_config(self, config):
+        """
+        This IConfigurer implementation causes CKAN to look in the
+        ```templates``` directory when looking for the package_form()
+        """
+        here = os.path.dirname(__file__)
+        rootdir = os.path.dirname(os.path.dirname(here))
+        template_dir = os.path.join(rootdir, 'ckanext',
+                                    'example', 'theme', 'templates')
+        config['extra_template_paths'] = ','.join([template_dir,
+                config.get('extra_template_paths', '')])
+
+    def configure(self, config):
+        '''
+        Adds some new vocabularies to the database if they don't already exist.
+
+        '''
+        # Add a 'genre' vocabulary with some tags.
+        self.genre_vocab = model.Vocabulary.get('Genre')
+        if not self.genre_vocab:
+            log.info("Adding vocab Genre")
+            self.genre_vocab = model.Vocabulary('Genre')
+            model.Session.add(self.genre_vocab)
+            model.Session.commit()
+            log.info("Adding example tags to vocab %s" % self.genre_vocab.name)
+            jazz_tag = model.Tag('jazz', self.genre_vocab.id)
+            soul_tag = model.Tag('soul', self.genre_vocab.id)
+            model.Session.add(jazz_tag)
+            model.Session.add(soul_tag)
+            model.Session.commit()
+
+        # Add a 'composer' vocabulary with some tags.
+        self.composer_vocab = model.Vocabulary.get('Composer')
+        if not self.composer_vocab:
+            log.info("Adding vocab Composer")
+            self.composer_vocab = model.Vocabulary('Composer')
+            model.Session.add(self.composer_vocab)
+            model.Session.commit()
+            log.info("Adding example tags to vocab %s" %
+                    self.composer_vocab.name)
+            mintzer_tag = model.Tag('Bob Mintzer', self.composer_vocab.id)
+            lewis_tag = model.Tag('Steve Lewis', self.composer_vocab.id)
+            model.Session.add(mintzer_tag)
+            model.Session.add(lewis_tag)
+            model.Session.commit()
+
+    def package_form(self):
+        """
+        Returns a string representing the location of the template to be
+        rendered.  e.g. "package/new_package_form.html".
+        """        
+        return 'forms/dataset_form.html'
+
+    def is_fallback(self):
+        """
+        Returns true iff this provides the fallback behaviour, when no other
+        plugin instance matches a package's type.
+
+        As this is not the fallback controller we should return False.  If 
+        we were wanting to act as the fallback, we'd return True
+        """
+        return True
+
+    def package_types(self):
+        """
+        Returns an iterable of package type strings.
+
+        If a request involving a package of one of those types is made, then
+        this plugin instance will be delegated to.
+
+        There must only be one plugin registered to each package type.  Any
+        attempts to register more than one plugin instance to a given package
+        type will raise an exception at startup.
+        """
+        return ["example_dataset_form"]
+
+    def setup_template_variables(self, context, data_dict=None):
+        """
+        Adds variables to c just prior to the template being rendered that can
+        then be used within the form
+        """        
+        c.licences = [('', '')] + model.Package.get_license_options()
+        c.publishers = [('Example publisher', 'Example publisher 2')]
+        c.is_sysadmin = Authorizer().is_sysadmin(c.user)
+        c.resource_columns = model.Resource.get_columns()
+        c.genre_tags = get_action('tag_list')(context, {'vocabulary_id': self.genre_vocab.name})
+        c.composer_tags = get_action('tag_list')(context, {'vocabulary_id': self.composer_vocab.name})
+
+        ## This is messy as auths take domain object not data_dict
+        pkg = context.get('package') or c.pkg
+        if pkg:
+            c.auth_for_change_state = Authorizer().am_authorized(
+                c, model.Action.CHANGE_STATE, pkg)
+
+    def form_to_db_schema(self):
+        """
+        Returns the schema for mapping package data from a form to a format
+        suitable for the database.
+        """
+        schema = package_form_schema()
+        schema.update({
+            'published_by': [not_empty, unicode, convert_to_extras],
+            'genre_tags': [ignore_missing, convert_to_tags(self.genre_vocab.name)],
+            'composer_tags': [ignore_missing, convert_to_tags(self.composer_vocab.name)],
+        })
+        return schema
+    
+    def db_to_form_schema(self):
+        """
+        Returns the schema for mapping package data from the database into a
+        format suitable for the form (optional)
+        """
+        schema = package_form_schema()
+        schema.update({
+            'tags': {
+                '__extras': [keep_extras, free_tags_only]
+            },
+            'genre_tags_selected': [convert_from_tags(self.genre_vocab.name),
+                ignore_missing],
+            'composer_tags_selected': [
+                convert_from_tags(self.composer_vocab.name), ignore_missing],
+            'published_by': [convert_from_extras, ignore_missing],
+        })
+        return schema
+
+    def check_data_dict(self, data_dict):
+        """
+        Check if the return data is correct and raises a DataError if not.
+        """
+        return
+
+    def filter(self, stream):
+        # Add vocab tags to the bottom of the sidebar.
+        from pylons import request
+        from genshi.filters import Transformer
+        from genshi.input import HTML
+        routes = request.environ.get('pylons.routes_dict')
+        if routes.get('controller') == 'package' \
+            and routes.get('action') == 'read':
+                for vocab in (self.genre_vocab, self.composer_vocab):
+                    vocab_tags = [tag for tag in c.pkg_dict.get('tags', [])
+                            if tag.get('vocabulary_id') == vocab.id]
+                    if not vocab_tags:
+                        continue
+                    html = '<li class="sidebar-section">'
+                    html = html + '<h3>%s</h3>' % vocab.name
+                    html = html + '<ul class="tags clearfix">'
+                    for tag in vocab_tags:
+                        html = html + '<li>%s</li>' % tag.get('name')
+                    html = html + "</ul></li>"
+                    stream = stream | Transformer("//div[@id='sidebar']")\
+                        .append(HTML(html))
+        return stream
+

--- a/ckanext/example/plugin.py
+++ b/ckanext/example/plugin.py
@@ -1,6 +1,8 @@
 import os
 from logging import getLogger
 
+from pylons import request
+from genshi.input import HTML
 from genshi.filters.transform import Transformer
 
 from ckan.plugins import implements, SingletonPlugin
@@ -21,9 +23,11 @@
         found in the ``ini``-file.  Here we use it to specify the site
         title, and to tell CKAN to look in this package for templates
         and resources that customise the core look and feel.
+        
       - ``IGenshiStreamFilter`` allows us to filter and transform the
         HTML stream just before it is rendered.  In this case we use
         it to rename "frob" to "foobar"
+        
       - ``IRoutes`` allows us to add new URLs, or override existing
         URLs.  In this example we use it to override the default
         ``/register`` behaviour with a custom controller
@@ -68,9 +72,26 @@
         This example filter renames 'frob' to 'foobar' (this string is
         found in the custom ``home/index.html`` template provided as
         part of the package).
+
+        It also adds the chosen JQuery plugin to the page if viewing the
+        dataset edit page (provides a better UX for working with tags with vocabularies)
         """
         stream = stream | Transformer('//p[@id="examplething"]/text()')\
                  .substitute(r'frob', r'foobar')
+
+        routes = request.environ.get('pylons.routes_dict')
+        if routes.get('controller') == 'package' \
+            and routes.get('action') == 'edit':
+                stream = stream | Transformer('head').append(HTML(
+                    '<link rel="stylesheet" href="/css/chosen.css" />'
+                ))
+                stream = stream | Transformer('body').append(HTML(
+                    '''
+                    <script src="/scripts/chosen.jquery.min.js" type="text/javascript"></script>'
+                    <script type="text/javascript">$(".chzn-select").chosen();</script>
+                    '''
+                ))
+
         return stream
 
     def before_map(self, map):

 Binary files /dev/null and b/ckanext/example/theme/public/css/chosen-sprite.png differ
--- /dev/null
+++ b/ckanext/example/theme/public/css/chosen.css
@@ -1,1 +1,390 @@
-
+/* @group Base */
+.chzn-container {
+  font-size: 13px;
+  position: relative;
+  display: inline-block;
+  zoom: 1;
+  *display: inline;
+}
+.chzn-container .chzn-drop {
+  background: #fff;
+  border: 1px solid #aaa;
+  border-top: 0;
+  position: absolute;
+  top: 29px;
+  left: 0;
+  -webkit-box-shadow: 0 4px 5px rgba(0,0,0,.15);
+  -moz-box-shadow   : 0 4px 5px rgba(0,0,0,.15);
+  -o-box-shadow     : 0 4px 5px rgba(0,0,0,.15);
+  box-shadow        : 0 4px 5px rgba(0,0,0,.15);
+  z-index: 999;
+}
+/* @end */
+
+/* @group Single Chosen */
+.chzn-container-single .chzn-single {
+  background-color: #ffffff;
+  filter: progid:DXImageTransform.Microsoft.gradient( startColorstr='#ffffff', endColorstr='#eeeeee', GradientType=0 );   
+  background-image: -webkit-gradient(linear, 0% 0%, 0% 100%, color-stop(20%, #ffffff), color-stop(50%, #f6f6f6), color-stop(52%, #eeeeee), color-stop(100%, #f4f4f4));
+  background-image: -webkit-linear-gradient(top, #ffffff 20%, #f6f6f6 50%, #eeeeee 52%, #f4f4f4 100%);
+  background-image: -moz-linear-gradient(top, #ffffff 20%, #f6f6f6 50%, #eeeeee 52%, #f4f4f4 100%);
+  background-image: -o-linear-gradient(top, #ffffff 20%, #f6f6f6 50%, #eeeeee 52%, #f4f4f4 100%);
+  background-image: -ms-linear-gradient(top, #ffffff 20%, #f6f6f6 50%, #eeeeee 52%, #f4f4f4 100%);
+  background-image: linear-gradient(top, #ffffff 20%, #f6f6f6 50%, #eeeeee 52%, #f4f4f4 100%); 
+  -webkit-border-radius: 5px;
+  -moz-border-radius   : 5px;
+  border-radius        : 5px;
+  -moz-background-clip   : padding;
+  -webkit-background-clip: padding-box;
+  background-clip        : padding-box;
+  border: 1px solid #aaaaaa;
+  -webkit-box-shadow: 0 0 3px #ffffff inset, 0 1px 1px rgba(0,0,0,0.1);
+  -moz-box-shadow   : 0 0 3px #ffffff inset, 0 1px 1px rgba(0,0,0,0.1);
+  box-shadow        : 0 0 3px #ffffff inset, 0 1px 1px rgba(0,0,0,0.1);
+  display: block;
+  overflow: hidden;
+  white-space: nowrap;
+  position: relative;
+  height: 23px;
+  line-height: 24px;
+  padding: 0 0 0 8px;
+  color: #444444;
+  text-decoration: none;
+}
+.chzn-container-single .chzn-single span {
+  margin-right: 26px;
+  display: block;
+  overflow: hidden;
+  white-space: nowrap;
+  -o-text-overflow: ellipsis;
+  -ms-text-overflow: ellipsis;
+  text-overflow: ellipsis;
+}
+.chzn-container-single .chzn-single abbr {
+  display: block;
+  position: absolute;
+  right: 26px;
+  top: 6px;
+  width: 12px;
+  height: 13px;
+  font-size: 1px;
+  background: url(chosen-sprite.png) right top no-repeat;
+}
+.chzn-container-single .chzn-single abbr:hover {
+  background-position: right -11px;
+}
+.chzn-container-single .chzn-single div {
+  position: absolute;
+  right: 0;
+  top: 0;
+  display: block;
+  height: 100%;
+  width: 18px;
+}
+.chzn-container-single .chzn-single div b {
+  background: url('chosen-sprite.png') no-repeat 0 0;
+  display: block;
+  width: 100%;
+  height: 100%;
+}
+.chzn-container-single .chzn-search {
+  padding: 3px 4px;
+  position: relative;
+  margin: 0;
+  white-space: nowrap;
+  z-index: 1010;
+}
+.chzn-container-single .chzn-search input {
+  background: #fff url('chosen-sprite.png') no-repeat 100% -22px;
+  background: url('chosen-sprite.png') no-repeat 100% -22px, -webkit-gradient(linear, 0% 0%, 0% 100%, color-stop(1%, #eeeeee), color-stop(15%, #ffffff));
+  background: url('chosen-sprite.png') no-repeat 100% -22px, -webkit-linear-gradient(top, #eeeeee 1%, #ffffff 15%);
+  background: url('chosen-sprite.png') no-repeat 100% -22px, -moz-linear-gradient(top, #eeeeee 1%, #ffffff 15%);
+  background: url('chosen-sprite.png') no-repeat 100% -22px, -o-linear-gradient(top, #eeeeee 1%, #ffffff 15%);
+  background: url('chosen-sprite.png') no-repeat 100% -22px, -ms-linear-gradient(top, #eeeeee 1%, #ffffff 15%);
+  background: url('chosen-sprite.png') no-repeat 100% -22px, linear-gradient(top, #eeeeee 1%, #ffffff 15%);
+  margin: 1px 0;
+  padding: 4px 20px 4px 5px;
+  outline: 0;
+  border: 1px solid #aaa;
+  font-family: sans-serif;
+  font-size: 1em;
+}
+.chzn-container-single .chzn-drop {
+  -webkit-border-radius: 0 0 4px 4px;
+  -moz-border-radius   : 0 0 4px 4px;
+  border-radius        : 0 0 4px 4px;
+  -moz-background-clip   : padding;
+  -webkit-background-clip: padding-box;
+  background-clip        : padding-box;
+}
+/* @end */
+
+.chzn-container-single-nosearch .chzn-search input {
+  position: absolute;
+  left: -9000px;
+}
+
+/* @group Multi Chosen */
+.chzn-container-multi .chzn-choices {
+  background-color: #fff;
+  background-image: -webkit-gradient(linear, 0% 0%, 0% 100%, color-stop(1%, #eeeeee), color-stop(15%, #ffffff));
+  background-image: -webkit-linear-gradient(top, #eeeeee 1%, #ffffff 15%);
+  background-image: -moz-linear-gradient(top, #eeeeee 1%, #ffffff 15%);
+  background-image: -o-linear-gradient(top, #eeeeee 1%, #ffffff 15%);
+  background-image: -ms-linear-gradient(top, #eeeeee 1%, #ffffff 15%);
+  background-image: linear-gradient(top, #eeeeee 1%, #ffffff 15%);
+  border: 1px solid #aaa;
+  margin: 0;
+  padding: 0;
+  cursor: text;
+  overflow: hidden;
+  height: auto !important;
+  height: 1%;
+  position: relative;
+}
+.chzn-container-multi .chzn-choices li {
+  float: left;
+  list-style: none;
+}
+.chzn-container-multi .chzn-choices .search-field {
+  white-space: nowrap;
+  margin: 0;
+  padding: 0;
+}
+.chzn-container-multi .chzn-choices .search-field input {
+  color: #666;
+  background: transparent !important;
+  border: 0 !important;
+  font-family: sans-serif;
+  font-size: 100%;
+  height: 15px;
+  padding: 5px;
+  margin: 1px 0;
+  outline: 0;
+  -webkit-box-shadow: none;
+  -moz-box-shadow   : none;
+  -o-box-shadow     : none;
+  box-shadow        : none;
+}
+.chzn-container-multi .chzn-choices .search-field .default {
+  color: #999;
+}
+.chzn-container-multi .chzn-choices .search-choice {
+  -webkit-border-radius: 3px;
+  -moz-border-radius   : 3px;
+  border-radius        : 3px;
+  -moz-background-clip   : padding;
+  -webkit-background-clip: padding-box;
+  background-clip        : padding-box;
+  background-color: #e4e4e4;
+  filter: progid:DXImageTransform.Microsoft.gradient( startColorstr='#f4f4f4', endColorstr='#eeeeee', GradientType=0 ); 
+  background-image: -webkit-gradient(linear, 0% 0%, 0% 100%, color-stop(20%, #f4f4f4), color-stop(50%, #f0f0f0), color-stop(52%, #e8e8e8), color-stop(100%, #eeeeee));
+  background-image: -webkit-linear-gradient(top, #f4f4f4 20%, #f0f0f0 50%, #e8e8e8 52%, #eeeeee 100%);
+  background-image: -moz-linear-gradient(top, #f4f4f4 20%, #f0f0f0 50%, #e8e8e8 52%, #eeeeee 100%);
+  background-image: -o-linear-gradient(top, #f4f4f4 20%, #f0f0f0 50%, #e8e8e8 52%, #eeeeee 100%);
+  background-image: -ms-linear-gradient(top, #f4f4f4 20%, #f0f0f0 50%, #e8e8e8 52%, #eeeeee 100%);
+  background-image: linear-gradient(top, #f4f4f4 20%, #f0f0f0 50%, #e8e8e8 52%, #eeeeee 100%); 
+  -webkit-box-shadow: 0 0 2px #ffffff inset, 0 1px 0 rgba(0,0,0,0.05);
+  -moz-box-shadow   : 0 0 2px #ffffff inset, 0 1px 0 rgba(0,0,0,0.05);
+  box-shadow        : 0 0 2px #ffffff inset, 0 1px 0 rgba(0,0,0,0.05);
+  color: #333;
+  border: 1px solid #aaaaaa;
+  line-height: 13px;