diff --git a/ckanext/odsh/fanstatic/odsh_form.js b/ckanext/odsh/fanstatic/odsh_form.js
index c4bbb1641fb8c8006888ef9d564f11cb6d31d3c4..0562849c43075ed6efff88ae05605d625f6c8e5a 100644
--- a/ckanext/odsh/fanstatic/odsh_form.js
+++ b/ckanext/odsh/fanstatic/odsh_form.js
@@ -18,7 +18,6 @@ ckan.module('odsh_form', function ($)
                         {
                             var values = $('#field-groups option:selected')
                                 .map(function (a, item) { return item.value; }).get().join(',')
-                                console.log(values)
                             $('#field-groups-value').val(values);
                         }
                     });
diff --git a/ckanext/odsh/helpers.py b/ckanext/odsh/helpers.py
index 5a92d0b02ba73d341dedf500b2c40b5fed34630b..d2d07cfb9252dc6e992bb7bdae9d85d98bf0f866 100644
--- a/ckanext/odsh/helpers.py
+++ b/ckanext/odsh/helpers.py
@@ -149,16 +149,21 @@ def odsh_create_checksum(in_string):
     return int(hashstring, base=16)
 
 
-def odsh_extract_error(key, errors):
-    if not errors or not ('extras' in errors):
+def odsh_extract_error(key, errors, field='extras'):
+    if not errors or not (field in errors):
         return None
-    ext = errors['extras']
+    ext = errors[field]
     for item in ext:
         if 'key' in item:
             for error in item['key']:
                 if error.startswith(key):
                     return error.replace(key+':', '')
 
+def odsh_extract_error_new(key, errors):
+    if not errors or not ('__extras' in errors):
+        return None
+    error = errors['__extras'][0].get(key,None)
+    return error
 
 def odsh_extract_value_from_extras(extras, key):
     if not extras:
diff --git a/ckanext/odsh/i18n/de/LC_MESSAGES/ckanext-odsh.mo b/ckanext/odsh/i18n/de/LC_MESSAGES/ckanext-odsh.mo
index 1c0637984f09888c0446060066344f8fa8344204..6c58f6b551b398f3a3e09aed54d9584bf7cb6417 100644
Binary files a/ckanext/odsh/i18n/de/LC_MESSAGES/ckanext-odsh.mo and b/ckanext/odsh/i18n/de/LC_MESSAGES/ckanext-odsh.mo differ
diff --git a/ckanext/odsh/i18n/de/LC_MESSAGES/ckanext-odsh.po b/ckanext/odsh/i18n/de/LC_MESSAGES/ckanext-odsh.po
index 191aacf1e34e0fb3f575b7ae82d620a0f100721c..950414e8c55a260ff08470e071caa4493becca38 100644
--- a/ckanext/odsh/i18n/de/LC_MESSAGES/ckanext-odsh.po
+++ b/ckanext/odsh/i18n/de/LC_MESSAGES/ckanext-odsh.po
@@ -343,4 +343,7 @@ msgid "3URI"
 msgstr "verwendet URIs"
 
 msgid "4LD"
-msgstr "Linked Data"
\ No newline at end of file
+msgstr "Linked Data"
+
+msgid "at least one group needed"
+msgstr "Bitte geben Sie mindestens eine Kategorie an"
\ No newline at end of file
diff --git a/ckanext/odsh/plugin.py b/ckanext/odsh/plugin.py
index c944fcf37445c7076424a53d8c39f555226fc6a4..354c3622953b27eb8d65423300b3becb07a43914 100644
--- a/ckanext/odsh/plugin.py
+++ b/ckanext/odsh/plugin.py
@@ -4,26 +4,23 @@ import ckan.plugins as plugins
 import ckan.plugins.toolkit as toolkit
 from ckan.lib.plugins import DefaultTranslation
 from ckan.lib.plugins import DefaultDatasetForm
-from ckan.lib.navl.dictization_functions import Missing
 from ckan.logic.validators import tag_string_convert
+from ckan.logic.schema import default_extras_schema
 from ckan.common import OrderedDict
-import ckan.model as model
 from ckanext.odsh.lib.uploader import ODSHResourceUpload
 import ckan.lib.helpers as helpers
 import helpers as odsh_helpers
 
-from itertools import count
 from routes.mapper import SubMapper
 from pylons import config
-import urllib2
-import csv
-import re
 from dateutil.parser import parse
 
 import ckan.plugins as p
 
 import logging
 
+import validation
+
 log = logging.getLogger(__name__)
 
 _ = toolkit._
@@ -50,20 +47,6 @@ def odsh_main_groups():
     return groups
 
 
-def odsh_convert_groups_string(value, context):
-    if not value:
-        return []
-    if type(value) is not list:
-        value = [value]
-    groups = helpers.groups_available()
-    ret = []
-    for v in value:
-        for g in groups:
-            if g['id'] == v:
-                ret.append(g)
-    return ret
-
-
 def odsh_now():
     return helpers.render_datetime(datetime.datetime.now(), "%Y-%m-%d")
 
@@ -77,211 +60,6 @@ def odsh_group_id_selected(selected, group_id):
 
     return False
 
-
-def known_spatial_uri(key, data, errors, context):
-    value = _extract_value(data, 'spatial_uri')
-
-    if not value:
-        raise toolkit.Invalid('spatial_uri:odsh_spatial_uri_error_label')
-
-    mapping_file = config.get('ckanext.odsh.spatial.mapping')
-    try:
-        mapping_file = urllib2.urlopen(mapping_file)
-    except Exception:
-        raise Exception("Could not load spatial mapping file!")
-
-    not_found = True
-    spatial_text = str()
-    spatial = str()
-    cr = csv.reader(mapping_file, delimiter="\t")
-    for row in cr:
-        if row[0].encode('UTF-8') == value:
-            not_found = False
-            spatial_text = row[1]
-            loaded = json.loads(row[2])
-            spatial = json.dumps(loaded['geometry'])
-            break
-    if not_found:
-        raise toolkit.Invalid(
-            'spatial_uri:odsh_spatial_uri_unknown_error_label')
-
-    # Get the current extras index
-    current_indexes = [k[1] for k in data.keys()
-                       if len(k) > 1 and k[0] == 'extras']
-
-    new_index = max(current_indexes) + 1 if current_indexes else 0
-
-    data[('extras', new_index, 'key')] = 'spatial_text'
-    data[('extras', new_index, 'value')] = spatial_text
-    data[('extras', new_index+1, 'key')] = 'spatial'
-    data[('extras', new_index+1, 'value')] = spatial
-
-
-def _extract_value(data, field):
-    key = None
-    for k in data.keys():
-        if data[k] == field:
-            key = k
-            break
-    if key is None:
-        return None
-    return data[(key[0], key[1], 'value')]
-
-def _set_value(data, field, value):
-    key = None
-    for k in data.keys():
-        if data[k] == field:
-            key = k
-            break
-    if key is None:
-        return None
-    data[(key[0], key[1], 'value')] = value
-
-def odsh_validate_extra_date(key, field, data, errors, context):
-    value = _extract_value(data, field)
-
-    if not value:
-        if field == 'temporal_end':
-            return # temporal_end is optional
-        # Statistikamt Nord does not always provide temporal_start/end,
-        # but their datasets have to be accepted as they are.
-        if not ('id',) in data or data[('id',)][:7] != 'StaNord':
-            raise toolkit.Invalid(field+':odsh_'+field+'_error_label')
-    else:
-        if re.match(r'\d\d\d\d-\d\d-\d\d', value):
-            try:
-                dt=parse(value)
-                _set_value(data, field, dt.isoformat())
-                return
-            except ValueError:
-                pass
-        raise toolkit.Invalid(field+':odsh_'+field+'_not_date_error_label')
-
-
-def odsh_validate_extra_date_factory(field):
-    return lambda key, data, errors, context: odsh_validate_extra_date(key, field, data, errors, context)
-
-def odsh_validate_licenseAttributionByText(key, data, errors, context):
-    register = model.Package.get_license_register()
-    isByLicense=False
-    for k in data:
-        if len(k) > 0 and k[0] == 'license_id' and data[k] and not isinstance(data[k], Missing) and \
-            'Namensnennung' in register[data[k]].title:
-            isByLicense = True
-            break
-    hasAttribution=False
-    for k in data:
-        if data[k] == 'licenseAttributionByText':
-            if isinstance(data[(k[0], k[1], 'value')], Missing):
-                del data[(k[0], k[1], 'value')]
-                del data[(k[0], k[1], 'key')]
-                break
-            else:
-                value = data[(k[0], k[1], 'value')]
-                hasAttribution = value != ''
-                break
-    if isByLicense and not hasAttribution:
-        raise toolkit.Invalid('licenseAttributionByText:odsh_licence_text_missing_error_label')
-    if not isByLicense and hasAttribution:
-        raise toolkit.Invalid('licenseAttributionByText:odsh_licence_text_not_allowed_error_label')
-
-def odsh_tag_name_validator(value, context):
-    tagname_match = re.compile('[\w \-.\:\(\)]*$', re.UNICODE)
-    if not tagname_match.match(value):
-        raise toolkit.Invalid(_('Tag "%s" must be alphanumeric '
-                                'characters or symbols: -_.:()') % (value))
-    return value
-
-
-def odsh_tag_string_convert(key, data, errors, context):
-    '''Takes a list of tags that is a comma-separated string (in data[key])
-    and parses tag names. These are added to the data dict, enumerated. They
-    are also validated.'''
-    if isinstance(data[key], basestring):
-        tags = [tag.strip()
-                for tag in data[key].split(',')
-                if tag.strip()]
-    else:
-        tags = data[key]
-
-
-    current_index = max([int(k[1]) for k in data.keys()
-                         if len(k) == 3 and k[0] == 'tags'] + [-1])
-
-
-
-    for num, tag in zip(count(current_index+1), tags):
-        data[('tags', num, 'name')] = tag
-
-    for tag in tags:
-        toolkit.get_validator('tag_length_validator')(tag, context)
-        odsh_tag_name_validator(tag, context)
-
-def odsh_group_convert(key, data, errors, context):
-    # print('GROUPS')
-    print(key)
-    print(data)
-
-
-def odsh_validate_extra_groups(key, data, errors, context):
-    value = _extract_value(data, 'groups')
-    print('GROUPS')
-    print(value)
-    if not value:
-        return
-    groups = [g.strip() for g in value.split(',') if value.strip()]
-    # data[('groups', 0, 'id')]='soci'
-    # data[('groups', 1, 'id')]='ener'
-    print('STRIP')
-    print(groups)
-    for k in data.keys():
-        print(k)
-        if len(k) == 3 and k[0] == 'groups':
-            print('del')
-            data[k]=''
-            # del data[k]
-    print(data)
-
-    # for num, tag in zip(range(len(groups)), groups):
-    #     data[('groups', num, 'id')] = tag
-    #     # print(data[('groups', num, 'id')])
-
-def odsh_group_string_convert(key, data, errors, context):
-    '''Takes a list of groups that is a comma-separated string (in data[key])
-    and parses groups names. These are added to the data dict, enumerated. 
-    They are also validated.'''
-    print('GROUPSTRING')
-    print(key)
-
-    if isinstance(data[key], basestring):
-        tags = [tag.strip()
-                for tag in data[key].split(',')
-                if tag.strip()]
-    else:
-        tags = data[key]
-
-    print(tags)
-
-    current_index = max([int(k[1]) for k in data.keys()
-                         if len(k) == 3 and k[0] == 'groups'] + [-1])
-
-    # for num, tag in zip(count(current_index+1), tags):
-    #     data[('groups', num, 'id')] = tag
-    #     print(data[('groups', num, 'id')])
-
-
-
-    # current_index = max([int(k[1]) for k in data.keys()
-    #                      if len(k) == 3 and k[0] == 'groups'] + [-1])
-
-    # for num, tag in zip(count(current_index+1), tags):
-    #     data[('groups', num, 'id')] = tag
-
-    # for tag in tags:
-    #     toolkit.get_validator('tag_length_validator')(tag, context)
-    #     odsh_tag_name_validator(tag, context)
-
-
 class OdshIcapPlugin(plugins.SingletonPlugin):
     plugins.implements(plugins.IUploader, inherit=True)
 
@@ -323,6 +101,7 @@ class OdshPlugin(plugins.SingletonPlugin, DefaultTranslation, DefaultDatasetForm
                 'odsh_upload_known_formats': odsh_helpers.odsh_upload_known_formats,
                 'odsh_encodeurl': odsh_helpers.odsh_encodeurl,
                 'odsh_extract_error': odsh_helpers.odsh_extract_error,
+                'odsh_extract_error_new': odsh_helpers.odsh_extract_error_new,
                 'odsh_extract_value_from_extras': odsh_helpers.odsh_extract_value_from_extras,
                 'odsh_create_checksum': odsh_helpers.odsh_create_checksum,
                 'presorted_license_options': odsh_helpers.presorted_license_options
@@ -446,8 +225,11 @@ class OdshPlugin(plugins.SingletonPlugin, DefaultTranslation, DefaultDatasetForm
         for field in ['title', 'notes','license_id']:
             schema.update({field: [toolkit.get_converter('not_empty')]})
         
-        # schema.update({'groups_string': [toolkit.get_converter('odsh_group_string_convert')]})
-        # schema.update({'groupss': [toolkit.get_converter('odsh_group_convert')]})
+        ##schema.update({'group_string': [toolkit.get_converter('odsh_group_string_convert')]})
+        # for i, item in enumerate(schema['groups']):
+        #schema['id'].update({'id': schema['groups']['id']+[toolkit.get_converter('odsh_group_convert')]})
+        ##schema['groups'].update({'id': schema['groups']['id']})
+    ##        schema.update({'groups': [toolkit.get_converter('empty')]})
 
         for i, item in enumerate(schema['tags']['name']):
             if item == toolkit.get_validator('tag_name_validator'):
@@ -455,7 +237,7 @@ class OdshPlugin(plugins.SingletonPlugin, DefaultTranslation, DefaultDatasetForm
                     'odsh_tag_name_validator')
         for i, item in enumerate(schema['tag_string']):
             if item == tag_string_convert:
-                schema['tag_string'][i] = odsh_tag_string_convert
+                schema['tag_string'][i] = validation.tag_string_convert
 
         schema['resources'].update({
             'url': [toolkit.get_converter('not_empty')],
@@ -468,10 +250,13 @@ class OdshPlugin(plugins.SingletonPlugin, DefaultTranslation, DefaultDatasetForm
                 toolkit.get_converter('odsh_validate_temporal_start'),
                 toolkit.get_converter('odsh_validate_temporal_end'),
                 toolkit.get_converter('known_spatial_uri'),
-                toolkit.get_converter('licenseAttributionByText'),
-                # toolkit.get_converter('odsh_validate_extra_groups')
+                toolkit.get_converter('licenseAttributionByText')
             ]
         })
+        ##schema.update({'title': [toolkit.get_converter('odsh_validate_extras')]+ schema['title']})
+        schema.update({'__extras':  [toolkit.get_converter('odsh_validate_extras')] })
+        # eschema = schema['extras']
+        ##schema.update({'extras':  None })
 
     def create_package_schema(self):
         schema = super(OdshPlugin, self).create_package_schema()
@@ -498,17 +283,7 @@ class OdshPlugin(plugins.SingletonPlugin, DefaultTranslation, DefaultDatasetForm
         return []
 
     def get_validators(self):
-        return {'odsh_convert_groups_string': odsh_convert_groups_string,
-                'licenseAttributionByText': odsh_validate_licenseAttributionByText,
-                'known_spatial_uri': known_spatial_uri,
-                'odsh_validate_issued': odsh_validate_extra_date_factory('issued'),
-                'odsh_validate_temporal_start': odsh_validate_extra_date_factory('temporal_start'),
-                'odsh_validate_temporal_end': odsh_validate_extra_date_factory('temporal_end'),
-                'odsh_tag_name_validator': odsh_tag_name_validator,
-                'odsh_group_string_convert':odsh_group_string_convert,
-                'odsh_group_convert':odsh_group_convert,
-                'odsh_validate_extra_groups':odsh_validate_extra_groups
-                }
+        return validation.get_validators()
 
     # Add the custom parameters to Solr's facet queries
     # use several daterange queries agains temporal_start and temporal_end field
diff --git a/ckanext/odsh/templates/package/snippets/info.html b/ckanext/odsh/templates/package/snippets/info.html
index 54d5419f196e175bc2841e83094182850ce2db5f..396f62caf4fb9f8797a78019358029dd90b421fc 100644
--- a/ckanext/odsh/templates/package/snippets/info.html
+++ b/ckanext/odsh/templates/package/snippets/info.html
@@ -21,12 +21,6 @@ Example:
             <div class="groups-detail info-detail">
                 <div>{{ _('Kategorie') }}:</div>
                 {{ pkg.groups|map(attribute='display_name')|join('; ') }}
-                {% if h.check_access('package_update', {'id':pkg.id }) %}
-                <div>
-                    {% link_for _('Edit Categories'), controller='package', action='groups', id=pkg.name,
-                    class_='btn btn-edit-categories', icon='wrench' %}
-                </div>
-                {% endif %}
             </div>
             {% endblock %}
 
diff --git a/ckanext/odsh/templates/package/snippets/package_basic_fields.html b/ckanext/odsh/templates/package/snippets/package_basic_fields.html
index 683e0f7a6ea3ec7003ce60875b47334fd550a1fe..1d7e441549a414e4c9a3e3cbc6d675e18a778bae 100644
--- a/ckanext/odsh/templates/package/snippets/package_basic_fields.html
+++ b/ckanext/odsh/templates/package/snippets/package_basic_fields.html
@@ -266,39 +266,42 @@ is_required=true,placeholder=_('Enter title')) }}
     {% endif %}
 
     <!-- field groups -->
-    {#
-    <div class="control-group">
+    {% set error_groups = h.odsh_extract_error_new('groups', errors) %}
+    <div class="control-group {{ " error" if error_groups }}">
         {% set groups_label='Kategorien'%}
         {% set multiselect_nonSelectedText='keine' %}
         {% set multiselect_allSelectedText='alle' %}
         {% set multiselect_nSelectedText='gewählt' %}
-        <!-- {% set error = errors.groups %} -->
         <label for="field-groups" class="control-label">{{ groups_label }}
             <span title="{{ _("This field is required") }}" class="control-required">*</span>
         </label>
         <div class="controls">
-            {% set existing_groups = data.get('groups') %}
-            {% if existing_groups %}
-            {% set existing_groups_string = existing_groups|map(attribute='id')|join(',') %}
-            {% else %}
-            {% set existing_groups_string = h.odsh_extract_value_from_extras(data.extras,'groups') %}
-            {% endif %}
-            {{existing_groups_string}}
-            {{existing_groups}}
-            {% if data.id %}
-                <input name='groups' value='[{"id":"ener"}]'></input>
-            {% else %}
-            <select id='field-groups' multiple="multiple" data-module="odsh_form" data-module-multiselect='true'
-                data-module-nonSelectedText="{{multiselect_nonSelectedText}}" data-module-allSelectedText="{{multiselect_allSelectedText}}"
-                data-module-nSelectedText="{{multiselect_nSelectedText}}">
-                {% for option in h.groups_available()%}
-                <option value={{option.id}} {% if existing_groups_string!=None and option['id'] in existing_groups_string %}selected="selected"
-                    {% endif %}>
-                    {{ option['display_name'] }}</option>
-                {% endfor %}
-            </select>
-            {{ form.input_extra('groups', value=existing_groups_string, index=h.odsh_create_checksum('groups'), type='hidden')}}
-            {% endif %}
+            <div class="row-fluid">
+                <div class="span6">
+                    {% set existing_groups = data.get('groups') %}
+                    {% if existing_groups %}
+                    {% set existing_groups_string = existing_groups|map(attribute='id')|join(',') %}
+                    {% else %}
+                    {% set existing_groups_string = h.odsh_extract_value_from_extras(data.extras,'groups') %}
+                    {% endif %}
+                    <select id='field-groups' multiple="multiple" data-module="odsh_form" data-module-multiselect='true'
+                        data-module-nonSelectedText="{{multiselect_nonSelectedText}}" data-module-allSelectedText="{{multiselect_allSelectedText}}"
+                        data-module-nSelectedText="{{multiselect_nSelectedText}}" data-module-update='{{data.id != None}}'>
+                        {% for option in h.groups_available()%}
+                        <option value={{option.id}} {% if existing_groups_string!=None and option['id'] in existing_groups_string %}selected="selected"
+                            {% endif %}>
+                            {{ option['display_name'] }}</option>
+                        {% endfor %}
+                    </select>
+                    <div id='selected-groups'>
+                        {{ form.input_extra('groups', value=existing_groups_string, index=h.odsh_create_checksum('groups'), type='hidden')}}
+                    </div>
+                </div>
+                <div class="span6 inline-error">
+                    {% if error_groups %} 
+                    {{_(error_groups)}}
+                    {% endif %}
+                </div>
+            </div>
         </div>
-    </div>
-    #}
\ No newline at end of file
+    </div>
\ No newline at end of file
diff --git a/ckanext/odsh/templates/package/snippets/resource_form.html b/ckanext/odsh/templates/package/snippets/resource_form.html
index 91eaae9a5354a38cf88c56962c6dca3a7d756fca..7652b0b67e4cc9ed4e885e2549f75f63416f075b 100644
--- a/ckanext/odsh/templates/package/snippets/resource_form.html
+++ b/ckanext/odsh/templates/package/snippets/resource_form.html
@@ -6,8 +6,6 @@
 {% set active = data and data.state=='active' %}
 {% set action = form_action or h.url_for(controller='package', action='new_resource', id=pkg_name) %}
 
-{{errors}}
-
 <form id="resource-edit" class="dataset-form dataset-resource-form {%if(data)%}resource-edit-form{%endif%}" method="post" action="{{ action }}" data-module="basic-form resource-form"
     enctype="multipart/form-data" novalidate>
     {% block stages %}
diff --git a/ckanext/odsh/templates/snippets/package_item.html b/ckanext/odsh/templates/snippets/package_item.html
index 74f0e095bcf9dc59cf20dbd11eba93f16c753e4f..dc465a7b73a676196e9aedbb9edc5b1d431ffa00 100644
--- a/ckanext/odsh/templates/snippets/package_item.html
+++ b/ckanext/odsh/templates/snippets/package_item.html
@@ -32,7 +32,7 @@ Example:
 {% set timerange_label ='Zeitraum' %}
 {% set access_count_label ='Aufrufe' %}
 {% set issued_extra = h.odsh_extract_value_from_extras(package.extras,'issued') %}
-{% set issued = h.odsh_render_datetime(value) if value else h.odsh_render_datetime(package.metadata_created)%} 
+{% set issued = h.odsh_render_datetime(value) if issued_extra else h.odsh_render_datetime(package.metadata_created)%} 
 {% set categories = package.groups|map(attribute='display_name')|join(', ') if package.groups else '-' %}
 
 {% block package_item %}
diff --git a/ckanext/odsh/validation.py b/ckanext/odsh/validation.py
new file mode 100644
index 0000000000000000000000000000000000000000..3a6d6ef8d77cce82e51b8191c2590fb6028da5ed
--- /dev/null
+++ b/ckanext/odsh/validation.py
@@ -0,0 +1,180 @@
+import csv
+import re
+import urllib2
+import json
+from itertools import count
+from dateutil.parser import parse
+
+import ckan.plugins.toolkit as toolkit
+import ckan.model as model
+from ckan.lib.navl.dictization_functions import Missing
+
+from pylons import config
+
+def _extract_value(data, field):
+    key = None
+    for k in data.keys():
+        if data[k] == field:
+            key = k
+            break
+    if key is None:
+        return None
+    return data[(key[0], key[1], 'value')]
+
+def validate_extra_groups(data):
+    value = _extract_value(data, 'groups')
+    if not value:
+        raise toolkit.Invalid({'groups':'at least one group needed'})
+
+    groups = [g.strip() for g in value.split(',') if value.strip()]
+    for k in data.keys():
+        if len(k) == 3 and k[0] == 'groups':
+            data[k]=''
+            # del data[k]
+    if len(groups)==0:
+        raise toolkit.Invalid({'groups':'at least one group needed'})
+
+    for num, tag in zip(range(len(groups)), groups):
+        data[('groups', num, 'id')] = tag
+
+def validate_extras(key, data, errors, context):
+    validate_extra_groups(data)
+
+def _set_value(data, field, value):
+    key = None
+    for k in data.keys():
+        if data[k] == field:
+            key = k
+            break
+    if key is None:
+        return None
+    data[(key[0], key[1], 'value')] = value
+
+def validate_extra_date(key, field, data, errors, context):
+    value = _extract_value(data, field)
+
+    if not value:
+        if field == 'temporal_end':
+            return # temporal_end is optional
+        # Statistikamt Nord does not always provide temporal_start/end,
+        # but their datasets have to be accepted as they are.
+        if not ('id',) in data or data[('id',)][:7] != 'StaNord':
+            raise toolkit.Invalid(field+':odsh_'+field+'_error_label')
+    else:
+        if re.match(r'\d\d\d\d-\d\d-\d\d', value):
+            try:
+                dt=parse(value)
+                _set_value(data, field, dt.isoformat())
+                return
+            except ValueError:
+                pass
+        raise toolkit.Invalid(field+':odsh_'+field+'_not_date_error_label')
+
+
+def validate_extra_date_factory(field):
+    return lambda key, data, errors, context: validate_extra_date(key, field, data, errors, context)
+
+def validate_licenseAttributionByText(key, data, errors, context):
+    register = model.Package.get_license_register()
+    isByLicense=False
+    for k in data:
+        if len(k) > 0 and k[0] == 'license_id' and data[k] and not isinstance(data[k], Missing) and \
+            'Namensnennung' in register[data[k]].title:
+            isByLicense = True
+            break
+    hasAttribution=False
+    for k in data:
+        if data[k] == 'licenseAttributionByText':
+            if isinstance(data[(k[0], k[1], 'value')], Missing):
+                del data[(k[0], k[1], 'value')]
+                del data[(k[0], k[1], 'key')]
+                break
+            else:
+                value = data[(k[0], k[1], 'value')]
+                hasAttribution = value != ''
+                break
+    if isByLicense and not hasAttribution:
+        raise toolkit.Invalid('licenseAttributionByText:odsh_licence_text_missing_error_label')
+    if not isByLicense and hasAttribution:
+        raise toolkit.Invalid('licenseAttributionByText:odsh_licence_text_not_allowed_error_label')
+
+def known_spatial_uri(key, data, errors, context):
+    value = _extract_value(data, 'spatial_uri')
+
+    if not value:
+        raise toolkit.Invalid('spatial_uri:odsh_spatial_uri_error_label')
+
+    mapping_file = config.get('ckanext.odsh.spatial.mapping')
+    try:
+        mapping_file = urllib2.urlopen(mapping_file)
+    except Exception:
+        raise Exception("Could not load spatial mapping file!")
+
+    not_found = True
+    spatial_text = str()
+    spatial = str()
+    cr = csv.reader(mapping_file, delimiter="\t")
+    for row in cr:
+        if row[0].encode('UTF-8') == value:
+            not_found = False
+            spatial_text = row[1]
+            loaded = json.loads(row[2])
+            spatial = json.dumps(loaded['geometry'])
+            break
+    if not_found:
+        raise toolkit.Invalid(
+            'spatial_uri:odsh_spatial_uri_unknown_error_label')
+
+    # Get the current extras index
+    current_indexes = [k[1] for k in data.keys()
+                       if len(k) > 1 and k[0] == 'extras']
+
+    new_index = max(current_indexes) + 1 if current_indexes else 0
+
+    data[('extras', new_index, 'key')] = 'spatial_text'
+    data[('extras', new_index, 'value')] = spatial_text
+    data[('extras', new_index+1, 'key')] = 'spatial'
+    data[('extras', new_index+1, 'value')] = spatial
+
+def tag_name_validator(value, context):
+    tagname_match = re.compile('[\w \-.\:\(\)]*$', re.UNICODE)
+    if not tagname_match.match(value):
+        raise toolkit.Invalid(_('Tag "%s" must be alphanumeric '
+                                'characters or symbols: -_.:()') % (value))
+    return value
+
+def tag_string_convert(key, data, errors, context):
+    '''Takes a list of tags that is a comma-separated string (in data[key])
+    and parses tag names. These are added to the data dict, enumerated. They
+    are also validated.'''
+    if isinstance(data[key], basestring):
+        tags = [tag.strip()
+                for tag in data[key].split(',')
+                if tag.strip()]
+    else:
+        tags = data[key]
+
+
+    current_index = max([int(k[1]) for k in data.keys()
+                         if len(k) == 3 and k[0] == 'tags'] + [-1])
+
+
+
+    for num, tag in zip(count(current_index+1), tags):
+        data[('tags', num, 'name')] = tag
+
+    for tag in tags:
+        toolkit.get_validator('tag_length_validator')(tag, context)
+        tag_name_validator(tag, context)
+
+
+def get_validators():
+    return {
+            'licenseAttributionByText': validate_licenseAttributionByText,
+            'known_spatial_uri': known_spatial_uri,
+            'odsh_validate_issued': validate_extra_date_factory('issued'),
+            'odsh_validate_temporal_start': validate_extra_date_factory('temporal_start'),
+            'odsh_validate_temporal_end': validate_extra_date_factory('temporal_end'),
+            'odsh_tag_name_validator': tag_name_validator,
+            'odsh_validate_extras':validate_extras
+            }
\ No newline at end of file