[ckan-changes] commit/ckan: 5 new changesets

Bitbucket commits-noreply at bitbucket.org
Thu May 19 17:48:42 UTC 2011


5 new changesets in ckan:

http://bitbucket.org/okfn/ckan/changeset/9103c03a24f3/
changeset:   r3097:9103c03a24f3
branch:      feature-1078-refactor-wui-to-use-logic-layer
user:        kindly
date:        2011-05-18 23:35:54
summary:     [forms] changes to accomodatedgu form
affected #:  6 files (1021 bytes)

--- a/ckan/lib/navl/dictization_functions.py	Tue May 10 11:27:25 2011 +0100
+++ b/ckan/lib/navl/dictization_functions.py	Wed May 18 22:35:54 2011 +0100
@@ -115,6 +115,15 @@
     for key, value in new_data.items():
         if key in full_schema:
             continue
+
+        ## check if any thing naugthy is placed against subschemas
+        initial_tuple = key[::2]
+        if initial_tuple in [initial_key[:len(initial_tuple)] 
+                             for initial_key in flattented_schema]:
+            if data[key] <> []:
+                raise DataError('Only lists of dicts can be placed against'
+                                'subschema %s' % key)
+                
         if key[:-1] in key_combinations:
             extras_key = key[:-1] + ('__extras',)
             extras = new_data.get(extras_key, {})
@@ -314,7 +323,7 @@
 
     for key, value in data.iteritems():
         new_key = old_key + [key]
-        if isinstance(value, list):
+        if isinstance(value, list) and value and isinstance(value[0], dict):
             flattened = flatten_list(value, flattened, new_key)
         else:
             flattened[tuple(new_key)] = value


--- a/ckan/lib/navl/validators.py	Tue May 10 11:27:25 2011 +0100
+++ b/ckan/lib/navl/validators.py	Wed May 18 22:35:54 2011 +0100
@@ -78,6 +78,14 @@
         data.pop(key, None)
         raise StopOnError
 
+def ignore_empty(key, data, errors, context):
+
+    value = data.get(key)
+
+    if value is missing or not value:
+        data.pop(key, None)
+        raise StopOnError
+
 def convert_int(value, context):
 
     try:


--- a/ckan/logic/__init__.py	Tue May 10 11:27:25 2011 +0100
+++ b/ckan/logic/__init__.py	Wed May 18 22:35:54 2011 +0100
@@ -19,11 +19,25 @@
 
 log = logging.getLogger(__name__)
 
+def parse_params(params):
+    parsed = {}
+    for key in params:
+        value = params.getall(key)
+        if not value:
+            value = ''
+        if len(value) == 1:
+            value = value[0]
+        parsed[key] = value
+    return parsed
+
+
 def clean_dict(data_dict):
     for key, value in data_dict.items():
         if not isinstance(value, list):
             continue
         for inner_dict in value[:]:
+            if isinstance(inner_dict, basestring):
+                break
             if not any(inner_dict.values()):
                 value.remove(inner_dict)
             else:


--- a/ckan/logic/schema.py	Tue May 10 11:27:25 2011 +0100
+++ b/ckan/logic/schema.py	Wed May 18 22:35:54 2011 +0100
@@ -4,7 +4,8 @@
                                       empty,
                                       ignore,
                                       if_empty_same_as,
-                                      not_missing
+                                      not_missing,
+                                      ignore_empty
                                      )
 from ckan.logic.validators import (package_id_not_changed,
                                    package_id_exists,
@@ -27,7 +28,7 @@
 def default_resource_schema():
 
     schema = {
-        'id': [ignore_missing, unicode],
+        'id': [ignore_empty, unicode],
         'revistion_id': [ignore_missing, unicode],
         'resource_group_id': [ignore],
         'package_id': [ignore],
@@ -69,7 +70,6 @@
         'url': [ignore_missing, unicode],#, URL(add_http=False)],
         'version': [ignore_missing, unicode],
         'state': [ignore_not_admin, ignore_missing],
-        'tag_string': [ignore_missing, tag_string_convert],
         '__extras': [ignore],
         '__junk': [empty],
         'resources': default_resource_schema(),


--- a/ckan/logic/validators.py	Tue May 10 11:27:25 2011 +0100
+++ b/ckan/logic/validators.py	Wed May 18 22:35:54 2011 +0100
@@ -18,6 +18,7 @@
 
     if 'http:' in value:
         raise Invalid(_('No links are allowed in the log_message.'))
+    return value
 
 def package_id_exists(value, context):
 


--- a/ckan/templates/package/new_package_form.html	Tue May 10 11:27:25 2011 +0100
+++ b/ckan/templates/package/new_package_form.html	Wed May 18 22:35:54 2011 +0100
@@ -177,17 +177,18 @@
 
 <label for="log_message">Edit summary (briefly describe the changes you have made)</label><textarea id="log_message" name="log_message" class="short wide">${data.get('log_message', '')}</textarea>
+
 <div class="ckan-logged-in" style="display: none;">
-<p>Author: 127.0.0.1</p>
+  <p>Author: ${c.author}</p></div><div class="ckan-logged-out">
-  <label>Author: 127.0.0.1</label>
-
+  <label>Author: ${c.author}</label><p class="hints">
   Since you have not signed in this will just be your IP address.
-  <a href="/user/login" target="_blank">Click here to sign in</a> before saving (opens in new window).
+  <a href="${h.url_for(controller='user', action='login', id=None)}" target="_blank">Click here to sign in</a> before saving (opens in new window).
   </p></div>
+
 <div class="submit"><input name="preview" type="submit" value="Preview" /><input id="save" name="save" type="submit" value="Save" />


http://bitbucket.org/okfn/ckan/changeset/cee59b055cf7/
changeset:   r3098:cee59b055cf7
branch:      feature-1078-refactor-wui-to-use-logic-layer
user:        kindly
date:        2011-05-18 23:36:48
summary:     [merge] default
affected #:  7 files (3.5 KB)

--- a/ckan/config/deployment.ini_tmpl	Wed May 18 22:35:54 2011 +0100
+++ b/ckan/config/deployment.ini_tmpl	Wed May 18 22:36:48 2011 +0100
@@ -183,7 +183,7 @@
 handlers = console, file
 
 [logger_ckan]
-level = DEBUG
+level = INFO
 handlers = console, file
 qualname = ckan
 propagate = 0
@@ -201,10 +201,10 @@
 formatter = generic
 
 [handler_file]
-class = handlers.RotatingFileHandler
+class = logging.handlers.RotatingFileHandler
 formatter = generic
 level = NOTSET
-args = ('%(here)s/ckan.log', 'a', 2000000, 9)
+args = ('%(here)s/ckan.log', 'a', 2e7, 9)
 
 [formatter_generic]
 format = %(asctime)s %(levelname)-5.5s [%(name)s] %(message)s


--- a/ckan/model/authz.py	Wed May 18 22:35:54 2011 +0100
+++ b/ckan/model/authz.py	Wed May 18 22:36:48 2011 +0100
@@ -187,18 +187,29 @@
 
     @classmethod
     def add_user_to_role(cls, user, role, domain_obj):
-        '''role assignment already exists
-        NB: leaves caller to commit change'''
+        '''NB: Leaves the caller to commit the change. If called twice without a
+        commit, will add the role to the database twice. Since some other
+        functions count the number of occurrences, that leaves a fairly obvious
+        bug. But adding a commit here seems to break various tests.
+        So don't call this twice without committing, I guess...
+        '''
+        # Here we're trying to guard against adding the same role twice, but
+        # that won't work if the transaction hasn't been committed yet, which allows a role to be added twice (you can do this from the interface)
         if cls.user_has_role(user, role, domain_obj):
             return
         objectrole = cls(role=role, user=user)
         if cls.name is not None:
             setattr(objectrole, cls.name, domain_obj)
         Session.add(objectrole)
-        
+         
     @classmethod
     def add_authorization_group_to_role(cls, authorization_group, role, domain_obj):
-        # role assignment already exists
+        '''NB: Leaves the caller to commit the change. If called twice without a
+        commit, will add the role to the database twice. Since some other
+        functions count the number of occurrences, that leaves a fairly obvious
+        bug. But adding a commit here seems to break various tests.
+        So don't call this twice without committing, I guess...
+        '''
         if cls.authorization_group_has_role(authorization_group, role, domain_obj):
             return
         objectrole = cls(role=role, authorized_group=authorization_group)
@@ -209,16 +220,16 @@
     @classmethod
     def remove_user_from_role(cls, user, role, domain_obj):
         q = cls._user_query(user, role, domain_obj)
-        uo_role = q.one()
-        Session.delete(uo_role)
+        for uo_role in q.all():
+            Session.delete(uo_role)
         Session.commit()
         Session.remove()
 
     @classmethod
     def remove_authorization_group_from_role(cls, authorization_group, role, domain_obj):
         q = cls._authorized_group_query(authorization_group, role, domain_obj)
-        ago_role = q.one()
-        Session.delete(ago_role)
+        for ago_role in q.all():
+            Session.delete(ago_role)
         Session.commit()
         Session.remove()
 


--- a/ckan/templates/_util.html	Wed May 18 22:35:54 2011 +0100
+++ b/ckan/templates/_util.html	Wed May 18 22:36:48 2011 +0100
@@ -49,36 +49,38 @@
 				${h.link_to(package.title or package.name, h.url_for(controller='package', action='read', id=package.name))}
 			</span>
 			
-			<p class="search_meta">
-    			<py:if test="package.resources">
-        			<ul class="package_formats">
-        			    <py:for each="resource in package.resources">
-        			        <py:if test="resource.format and not resource.format == ''">
-        				        <li>${resource.format}</li>
-                            </py:if>
-        				</py:for>
-        			</ul>
-        		</py:if>
-        		<ul class="openness">
-            		<py:choose>
-                        <li py:when="package.isopen() and package.resources">
-                            <a href="http://www.opendefinition.org/okd/" title="This package satisfies the Open Knowledge Definition.">
-                                <img src="http://assets.okfn.org/images/ok_buttons/od_80x15_blue.png" alt="[Open Data]" />
-                            </a>
-                        </li>
-                        <li py:when="package.isopen() and package.resources">
-                            <a href="http://www.opendefinition.org/okd/" title="This package satisfies the Open Knowledge Definition.">
-                                <img src="http://assets.okfn.org/images/ok_buttons/oc_80x15_blue.png" alt="[Open Content]" />
-                            </a>
-                        </li>
-                        <li py:otherwise="">
-                            <span class="closed">
-                                ${h.icon('lock')} Data Not Open
-                            </span>
-                        </li>
-                    </py:choose>
-                </ul>
-            </p>
+			<div class="search_meta">
+        <py:if test="package.resources">
+          <ul class="package_formats">
+            <py:for each="resource in package.resources">
+              <py:if test="resource.format and not resource.format == ''">
+                <li>${resource.format}</li>
+              </py:if>
+            </py:for>
+          </ul>
+        </py:if>
+        <ul class="openness">
+          <py:if test="package.isopen()">
+            <li>
+              <a href="http://opendefinition.org/okd/" title="This package satisfies the Open Definition.">
+                  <img src="http://assets.okfn.org/images/ok_buttons/od_80x15_blue.png" alt="[Open Data]" />
+              </a>
+            </li>
+            <li>
+              <a href="http://opendefinition.org/okd/" title="This package satisfies the Open Definition.">
+                  <img src="http://assets.okfn.org/images/ok_buttons/oc_80x15_blue.png" alt="[Open Content]" />
+              </a>
+            </li>
+          </py:if>
+          <py:if test="not package.isopen()">
+            <li>
+              <span class="closed">
+                ${h.icon('lock')} Not Openly Licensed
+              </span>
+            </li>
+          </py:if>
+        </ul>
+      </div></div><div class="extract">
 			${h.markdown_extract(package.notes)}


--- a/ckan/tests/models/test_authz.py	Wed May 18 22:35:54 2011 +0100
+++ b/ckan/tests/models/test_authz.py	Wed May 18 22:36:48 2011 +0100
@@ -214,7 +214,8 @@
         mreditor = model.User(name=u'mreditor')
         mrreader = model.User(name=u'mrreader')
         tester = model.User(name=u'tester')
-        for obj in [anna, war, mradmin, mreditor, mrreader, tester]:
+        anauthzgroup = model.AuthorizationGroup(name=u'anauthzgroup')
+        for obj in [anna, war, mradmin, mreditor, mrreader, tester, anauthzgroup]:
             model.Session.add(obj)
         model.repo.commit_and_remove()
 
@@ -293,6 +294,53 @@
                                              action=model.Action.READ,
                                              domain_object=self.anna)
 
+    def test_3_add_twice_remove_twice(self):
+        tester = model.User.by_name(u'tester')
+        war = model.Package.by_name(u'warandpeace')
+
+        def tester_roles():
+            return [x.role \
+             for x in model.Session.query(model.PackageRole).all() \
+             if x.user and x.user.name=='tester' and x.package.name==u'warandpeace']
+          
+        assert len(tester_roles()) == 0, "wrong number of roles for tester"
+        model.add_user_to_role(tester, model.Role.ADMIN, war)
+        model.repo.commit_and_remove()
+        assert len(tester_roles()) == 1, "wrong number of roles for tester"
+        model.add_user_to_role(tester, model.Role.ADMIN, war)
+        model.repo.commit_and_remove()
+
+        assert len(tester_roles()) == 1, "wrong number of roles for tester"
+        model.remove_user_from_role(tester, model.Role.ADMIN, war)
+        assert len(tester_roles()) == 0, "wrong number of roles for tester"
+        model.remove_user_from_role(tester, model.Role.ADMIN, war)
+        assert len(tester_roles()) == 0, "wrong number of roles for tester"
+
+    def test_4_add_twice_remove_twice_for_authzgroups(self):
+        aag = model.AuthorizationGroup.by_name(u'anauthzgroup')
+        war = model.Package.by_name(u'warandpeace')
+
+        def aag_roles():
+            return [x.role \
+             for x in model.Session.query(model.PackageRole).all() \
+             if x.authorized_group and x.authorized_group.name=='anauthzgroup' and x.package.name==u'warandpeace']
+          
+        assert len(aag_roles()) == 0, "wrong number of roles for anauthzgroup"
+        model.add_authorization_group_to_role(aag, model.Role.ADMIN, war)
+        model.repo.commit_and_remove()
+        assert len(aag_roles()) == 1, "wrong number of roles for anauthzgroup"
+        model.add_authorization_group_to_role(aag, model.Role.ADMIN, war)
+        model.repo.commit_and_remove()
+
+        assert len(aag_roles()) == 1, "wrong number of roles for anauthzgroup"
+        model.remove_authorization_group_from_role(aag, model.Role.ADMIN, war)
+        assert len(aag_roles()) == 0, "wrong number of roles for anauthzgroup"
+        model.remove_authorization_group_from_role(aag, model.Role.ADMIN, war)
+        assert len(aag_roles()) == 0, "wrong number of roles for anauthzgroup"
+
+
+
+
 class TestMigrate:
     @classmethod
     def setup_class(self):


--- a/doc/deb.rst	Wed May 18 22:35:54 2011 +0100
+++ b/doc/deb.rst	Wed May 18 22:36:48 2011 +0100
@@ -93,7 +93,7 @@
 ``present``
     Those that already exist as packages in Ubuntu Lucid
 
-``misssing``
+``missing``
     Those that don't exist as packages in Ubuntu Lucid
 
 ``conflict``
@@ -138,7 +138,7 @@
 
 .. note ::
 
-   These are already packaged, so you don't need to pacakge them yourself, this
+   These are already packaged, so you don't need to package them yourself, this
    section just describes how you *could* do if you wanted to.
 
 Python dependencies listed in the ``lucid_missing.txt`` file are ``pip``
@@ -169,7 +169,7 @@
     cd missing
     bin/python -m buildkit.update_all .
 
-For each pacakge you'll be loaded into ``vim`` to edit the changelog. Save and
+For each package you'll be loaded into ``vim`` to edit the changelog. Save and
 quit when you are done. Names, version numbers and dependencies are
 automatically generated.
 
@@ -177,7 +177,7 @@
 
    Most of the time you will never use the automatic process above for lazy
    batch packaging. You'll more likely generate a single package with explicit
-   version numners using the ``buildkit.deb`` command or build your package
+   version numbers using the ``buildkit.deb`` command or build your package
    manually. Both approaches are described later.
 
 Packaging Conflicting Python dependencies from ``lucid_conflicts.txt``
@@ -185,15 +185,15 @@
 
 .. note ::
 
-   These are already packaged, so you don't need to pacakge them yourself, this
+   These are already packaged, so you don't need to package them yourself, this
    section just describes how you *could* do if you wanted to.
 
 Python packages where CKAN depends on a version that is different from the one
 in the Ubuntu Lucid repositories are handled slightly differently. If we were
 to simply package them up and make them available the same way we do with
 missing packages there is a slim chance that any existing software which used
-the other version of the libray would stop working. To avoid the risk of
-interfering with other software on the sytem we take the following approach:
+the other version of the library would stop working. To avoid the risk of
+interfering with other software on the system we take the following approach:
 
 * Create a ``python-ckan-deps`` package with copies of all the libraries we need
 * Change the ``python-ckan`` library to automatically try to import
@@ -245,7 +245,7 @@
 
 ``/path/to/virtualenv``
     I think this can just be the path to the directory containing the 
-    installed source code directoty you wish to package, it doesn't
+    installed source code directory you wish to package, it doesn't
     have to be a virtualenv does it?
 
 ``ckanext-qa``
@@ -254,7 +254,7 @@
 
 ``1.3~01+lucid``
     The version number of the package. There are three parts to this:
-
+x
     ``1.3``
         This should always match exactly the version number specified in the 
         ``setup.py`` file for the library being packaged.
@@ -279,7 +279,7 @@
 
 ``python-owslib python-ckanext-csw ... etc``
 
-    Any extra arguments are treated as the Debain names of dependencies.  These
+    Any extra arguments are treated as the Debian names of dependencies.  These
     always begin ``python-`` for Python libraries and would usually follow
     ``ckanext-`` for all CKAN extensions. 
 
@@ -305,7 +305,7 @@
 
 .. note ::
    
-    All pacakges that CKAN itself depends on are already packaged according to
+    All packages that CKAN itself depends on are already packaged according to
     the settings in the three ``requires`` files that from part of the ``ckan``
     source distribution so you shouldn't need to use the approach above to 
     package any of them, you should only need to do this for your own extensions
@@ -644,7 +644,7 @@
     cd missing
     bin/python -m buildkit.update_all .
 
-For each pacakge you'll be loaded into ``vim`` to edit the changelog. Save and
+For each package you'll be loaded into ``vim`` to edit the changelog. Save and
 quit when you are done. Names, version numbers and dependencies are
 automatically generated.
 


http://bitbucket.org/okfn/ckan/changeset/4ca5436ce549/
changeset:   r3099:4ca5436ce549
branch:      feature-1078-refactor-wui-to-use-logic-layer
user:        kindly
date:        2011-05-19 17:14:22
summary:     [forms] depreciate formalchemy for save/edit
affected #:  9 files (16.0 KB)

--- a/ckan/config/routing.py	Wed May 18 22:36:48 2011 +0100
+++ b/ckan/config/routing.py	Thu May 19 16:14:22 2011 +0100
@@ -166,8 +166,9 @@
     map.redirect("/packages/{url:.*}", "/package/{url}")
     map.connect('/package', controller='package', action='search')
 
-    map.connect('/package/new', controller='package_logic', action='new')
-    map.connect('/package/edit/{id}', controller='package_logic', action='edit')
+    ##to get back formalchemy uncomment these lines
+    ##map.connect('/package/new', controller='package_formalchemy', action='new')
+    ##map.connect('/package/edit/{id}', controller='package_formalchemy', action='edit')
 
     map.connect('/package/{action}', controller='package',
         requirements=dict(action='|'.join([
@@ -177,7 +178,6 @@
             'search'
             ]))
         )
-    # for backwards compatability
     map.connect('/package', controller='package', action='index')
     map.connect('/package/{action}/{id}', controller='package',
         requirements=dict(action='|'.join([
@@ -191,8 +191,9 @@
     map.redirect("/groups", "/group")
     map.redirect("/groups/{url:.*}", "/group/{url}")
 
-    map.connect('/group/new', controller='group_logic', action='new')
-    map.connect('/group/edit/{id}', controller='group_logic', action='edit')
+    ##to get back formalchemy uncomment these lines
+    ##map.connect('/group/new', controller='group_formalchemy', action='new')
+    ##map.connect('/group/edit/{id}', controller='group_formalchemy', action='edit')
 
     map.connect('/group', controller='group', action='index')
     map.connect('/group/list', controller='group', action='list')


--- a/ckan/controllers/group.py	Wed May 18 22:36:48 2011 +0100
+++ b/ckan/controllers/group.py	Thu May 19 16:14:22 2011 +0100
@@ -1,22 +1,50 @@
 import genshi
 
 from sqlalchemy.orm import eagerload_all
-from ckan.lib.base import *
+from ckan.lib.base import BaseController, c, model, request, render, h
+from ckan.lib.base import ValidationException, abort
 from pylons.i18n import get_lang, _
 import ckan.authz as authz
-from ckan import forms
-from ckan.model import System
+from ckan.authz import Authorizer
 from ckan.lib.helpers import Page
 from ckan.plugins import PluginImplementations, IGroupController
+import ckan.logic.action.create as create
+import ckan.logic.action.update as update
+import ckan.logic.action.get as get
+from ckan.lib.navl.dictization_functions import DataError, unflatten, validate
+from ckan.logic import NotFound, NotAuthorized, ValidationError
+from ckan.logic.schema import group_form_schema
+from ckan.logic import tuplize_dict, clean_dict, parse_params
+import ckan.forms
 
 class GroupController(BaseController):
-    
+
+    ## hooks for subclasses 
+    group_form = 'group/new_group_form.html'
+
+    def _form_to_db_schema(self):
+        return group_form_schema()
+
+    def _db_to_form_schema(self):
+        '''This is an interface to manipulate data from the database
+        into a format suitable for the form (optional)'''
+
+    def _setup_template_variables(self, context):
+        c.is_sysadmin = Authorizer().is_sysadmin(c.user)
+
+        ## This is messy as auths take domain object not data_dict
+        group = context.get('group') or c.pkg
+        if group:
+            c.auth_for_change_state = Authorizer().am_authorized(
+                c, model.Action.CHANGE_STATE, group)
+
+    ## end hooks
+
     def __init__(self):
         BaseController.__init__(self)
         self.extensions = PluginImplementations(IGroupController)
     
     def index(self):
-        from ckan.lib.helpers import Page
         
         if not self.authorizer.am_authorized(c, model.Action.SITE_READ, model.System):
             abort(401, _('Not authorized to see this page'))
@@ -32,6 +60,7 @@
         )
         return render('group/index.html')
 
+
     def read(self, id):
         c.group = model.Group.get(id)
         if c.group is None:
@@ -56,109 +85,99 @@
             extension.read(c.group)
         return render('group/read.html')
 
-    def new(self):
-        record = model.Group
-        c.error = ''
-        
-        auth_for_create = self.authorizer.am_authorized(c, model.Action.GROUP_CREATE, model.System())
+    def new(self, data=None, errors=None, error_summary=None):
+        context = {'model': model, 'session': model.Session,
+                   'user': c.user or c.author, 'extras_as_string': True,
+                   'schema': self._form_to_db_schema(),
+                   'save': 'save' in request.params}
+
+        auth_for_create = Authorizer().am_authorized(c, model.Action.GROUP_CREATE, model.System())
         if not auth_for_create:
             abort(401, _('Unauthorized to create a group'))
+
+        if context['save'] and not data:
+            return self._save_new(context)
         
-        is_admin = self.authorizer.is_sysadmin(c.user)
-        fs = ckan.forms.get_group_fieldset(is_admin=is_admin)
+        data = data or {}
+        errors = errors or {}
+        error_summary = error_summary or {}
+        vars = {'data': data, 'errors': errors, 'error_summary': error_summary}
 
-        if request.params.has_key('save'):
-            rev = model.repo.new_revision()
-            rev.author = c.author
-            # needed because request is nested
-            # multidict which is read only
-            params = dict(request.params)
-            c.fs = fs.bind(record, data=params or None, session=model.Session)
-            try:
-                self._update(c.fs, id, record.id)
-            except ValidationException, error:
-                fs = error.args[0]
-                c.form = self._render_edit_form(fs)
-                return render('group/edit.html')
-            # do not use groupname from id as may have changed
-            c.groupname = c.fs.name.value
-            c.grouptitle = c.fs.title.value
-            group = model.Group.get(c.groupname)
-            assert group
-            admins = []
-            user = model.User.by_name(c.user)
-            admins = [user]
-            model.setup_default_user_roles(group, admins)
-            group = model.Group.get(c.groupname)
-            pkgs = [model.Package.by_name(name) for name in request.params.getall('Group-packages-current')]
-            group.packages = pkgs
-            pkgnames = request.params.getall('PackageGroup--package_name')
-            for pkgname in pkgnames:
-                if pkgname:
-                    package = model.Package.by_name(pkgname)
-                    if package and package not in group.packages:
-                        group.packages.append(package)
-            for extension in self.extensions:
-                extension.create(group)
-            model.repo.commit_and_remove()
-            h.redirect_to(action='read', id=c.groupname)
-
-        if request.params:
-            data = forms.edit_group_dict(ckan.forms.get_group_dict(), request.params)
-            fs = fs.bind(data=data, session=model.Session)
-        c.form = self._render_edit_form(fs)
+        self._setup_template_variables(context)
+        c.form = render(self.group_form, extra_vars=vars)
         return render('group/new.html')
 
-    def edit(self, id=None): # allow id=None to allow posting
-        c.error = ''
-        group = model.Group.get(id)
-        if group is None:
-            abort(404, '404 Not Found')
+    def edit(self, id, data=None, errors=None, error_summary=None):
+        context = {'model': model, 'session': model.Session,
+                   'user': c.user or c.author, 'extras_as_string': True,
+                   'save': 'save' in request.params,
+                   'schema': self._form_to_db_schema(),
+                   'id': id}
+
+        if context['save'] and not data:
+            return self._save_edit(id, context)
+
+        try:
+            old_data = get.group_show(context)
+            c.grouptitle = old_data.get('title')
+            c.groupname = old_data.get('name')
+            schema = self._db_to_form_schema()
+            if schema:
+                old_data, errors = validate(old_data, schema)
+
+            data = data or old_data
+
+        except NotAuthorized:
+            abort(401, _('Unauthorized to read group %s') % '')
+
+        group = context.get("group")
+
         am_authz = self.authorizer.am_authorized(c, model.Action.EDIT, group)
         if not am_authz:
-            abort(401, _('User %r not authorized to edit %r') % (c.user, id))
-            
-        auth_for_change_state = self.authorizer.am_authorized(c, model.Action.CHANGE_STATE, group)
-        
-        if not 'save' in request.params:
-            c.group = group
-            c.groupname = group.name
-            c.grouptitle = group.title
-            
-            fs = forms.get_group_fieldset(is_admin=auth_for_change_state).bind(c.group)
-            c.form = self._render_edit_form(fs)
-            return render('group/edit.html')
-        else:
-            rev = model.repo.new_revision()
-            rev.author = c.author
-            # id is the name (pre-edited state)
-            c.groupname = id
-            # needed because request is nested
-            # multidict which is read only
-            params = dict(request.params)
-            fs = ckan.forms.get_group_fieldset(is_admin=auth_for_change_state)
-            c.fs = fs.bind(group, data=params or None)
-            try:
-                self._update(c.fs, id, group.id)
-                # do not use groupname from id as may have changed
-                c.groupname = c.fs.name.value
-                c.grouptitle = c.fs.title.value
-            except ValidationException, error:
-                fs = error.args[0]
-                c.form = self._render_edit_form(fs)
-                return render('group/edit.html')
-            pkgs = [model.Package.by_name(name) for name in request.params.getall('Group-packages-current')]
-            group.packages = pkgs
-            pkgnames = request.params.getall('PackageGroup--package_name')
-            for pkgname in pkgnames:
-                if pkgname:
-                    package = model.Package.by_name(pkgname)
-                    if package and package not in group.packages:
-                        group.packages.append(package)
-            for extension in self.extensions: 
-                extension.edit(group)
-            model.repo.commit_and_remove()
-            h.redirect_to(action='read', id=c.groupname)
+            abort(401, _('User %r not authorized to edit %s') % (c.user, id))
+
+        errors = errors or {}
+        vars = {'data': data, 'errors': errors, 'error_summary': error_summary}
+
+        self._setup_template_variables(context)
+        c.form = render(self.group_form, extra_vars=vars)
+        return render('group/edit.html')
+
+    def _save_new(self, context):
+        try:
+            data_dict = clean_dict(unflatten(
+                tuplize_dict(parse_params(request.params))))
+            context['message'] = data_dict.get('log_message', '')
+            group = create.group_create(data_dict, context)
+            h.redirect_to(controller='group', action='read', id=group['name'])
+        except NotAuthorized:
+            abort(401, _('Unauthorized to read group %s') % '')
+        except NotFound, e:
+            abort(404, _('Package not found'))
+        except DataError:
+            abort(400, _(u'Integrity Error'))
+        except ValidationError, e:
+            errors = e.error_dict
+            error_summary = e.error_summary
+            return self.new(data_dict, errors, error_summary)
+
+    def _save_edit(self, id, context):
+        try:
+            data_dict = clean_dict(unflatten(
+                tuplize_dict(parse_params(request.params))))
+            context['message'] = data_dict.get('log_message', '')
+            group = update.group_update(data_dict, context)
+            h.redirect_to(controller='group', action='read', id=group['name'])
+        except NotAuthorized:
+            abort(401, _('Unauthorized to read group %s') % id)
+        except NotFound, e:
+            abort(404, _('Package not found'))
+        except DataError:
+            abort(400, _(u'Integrity Error'))
+        except ValidationError, e:
+            errors = e.error_dict
+            error_summary = e.error_summary
+            return self.edit(id, data_dict, errors, error_summary)
 
     def authz(self, id):
         c.group = model.Group.get(id)
@@ -215,7 +234,7 @@
                 # new_roles = ckan.forms.new_roles_fs.bind(model.GroupRole, data=params or None)
                 # new_roles.sync()
                 for extension in self.extensions:
-                    extensions.authz_add_role(newgrouprole)
+                    extension.authz_add_role(newgrouprole)
                 model.Session.commit()
                 model.Session.remove()
                 c.message = _(u'Added role \'%s\' for authorization group \'%s\'') % (


--- /dev/null	Thu Jan 01 00:00:00 1970 +0000
+++ b/ckan/controllers/group_formalchemy.py	Thu May 19 16:14:22 2011 +0100
@@ -0,0 +1,125 @@
+
+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.lib.navl.dictization_functions import DataError, unflatten
+from ckan.logic import NotFound, NotAuthorized, ValidationError
+from ckan.logic.schema import group_form_schema
+from ckan.logic import tuplize_dict, clean_dict
+from ckan.authz import Authorizer
+import ckan.forms as forms
+import ckan.forms
+from ckan.lib.base import ValidationException
+from ckan.controllers.group import GroupController
+
+log = logging.getLogger(__name__)
+
+class GroupFormalchemyController(GroupController):
+
+    def new(self):
+        record = model.Group
+        c.error = ''
+        
+        auth_for_create = self.authorizer.am_authorized(c, model.Action.GROUP_CREATE, model.System())
+        if not auth_for_create:
+            abort(401, _('Unauthorized to create a group'))
+        
+        is_admin = self.authorizer.is_sysadmin(c.user)
+        fs = ckan.forms.get_group_fieldset(is_admin=is_admin)
+
+        if request.params.has_key('save'):
+            rev = model.repo.new_revision()
+            rev.author = c.author
+            # needed because request is nested
+            # multidict which is read only
+            params = dict(request.params)
+            c.fs = fs.bind(record, data=params or None, session=model.Session)
+            try:
+                self._update(c.fs, id, record.id)
+            except ValidationException, error:
+                fs = error.args[0]
+                c.form = self._render_edit_form(fs)
+                return render('group/edit.html')
+            # do not use groupname from id as may have changed
+            c.groupname = c.fs.name.value
+            c.grouptitle = c.fs.title.value
+            group = model.Group.get(c.groupname)
+            assert group
+            admins = []
+            user = model.User.by_name(c.user)
+            admins = [user]
+            model.setup_default_user_roles(group, admins)
+            group = model.Group.get(c.groupname)
+            pkgs = [model.Package.by_name(name) for name in request.params.getall('Group-packages-current')]
+            group.packages = pkgs
+            pkgnames = request.params.getall('PackageGroup--package_name')
+            for pkgname in pkgnames:
+                if pkgname:
+                    package = model.Package.by_name(pkgname)
+                    if package and package not in group.packages:
+                        group.packages.append(package)
+            for extension in self.extensions:
+                extension.create(group)
+            model.repo.commit_and_remove()
+            h.redirect_to(action='read', id=c.groupname)
+
+        if request.params:
+            data = forms.edit_group_dict(ckan.forms.get_group_dict(), request.params)
+            fs = fs.bind(data=data, session=model.Session)
+        c.form = self._render_edit_form(fs)
+        return render('group/new.html')
+
+    def edit(self, id=None): # allow id=None to allow posting
+        c.error = ''
+        group = model.Group.get(id)
+        if group is None:
+            abort(404, '404 Not Found')
+        am_authz = self.authorizer.am_authorized(c, model.Action.EDIT, group)
+        if not am_authz:
+            abort(401, _('User %r not authorized to edit %r') % (c.user, id))
+            
+        auth_for_change_state = self.authorizer.am_authorized(c, model.Action.CHANGE_STATE, group)
+        
+        if not 'save' in request.params:
+            c.group = group
+            c.groupname = group.name
+            c.grouptitle = group.title
+            
+            fs = forms.get_group_fieldset(is_admin=auth_for_change_state).bind(c.group)
+            c.form = self._render_edit_form(fs)
+            return render('group/edit.html')
+        else:
+            rev = model.repo.new_revision()
+            rev.author = c.author
+            # id is the name (pre-edited state)
+            c.groupname = id
+            # needed because request is nested
+            # multidict which is read only
+            params = dict(request.params)
+            fs = ckan.forms.get_group_fieldset(is_admin=auth_for_change_state)
+            c.fs = fs.bind(group, data=params or None)
+            try:
+                self._update(c.fs, id, group.id)
+                # do not use groupname from id as may have changed
+                c.groupname = c.fs.name.value
+                c.grouptitle = c.fs.title.value
+            except ValidationException, error:
+                fs = error.args[0]
+                c.form = self._render_edit_form(fs)
+                return render('group/edit.html')
+            pkgs = [model.Package.by_name(name) for name in request.params.getall('Group-packages-current')]
+            group.packages = pkgs
+            pkgnames = request.params.getall('PackageGroup--package_name')
+            for pkgname in pkgnames:
+                if pkgname:
+                    package = model.Package.by_name(pkgname)
+                    if package and package not in group.packages:
+                        group.packages.append(package)
+            for extension in self.extensions: 
+                extension.edit(group)
+            model.repo.commit_and_remove()
+            h.redirect_to(action='read', id=c.groupname)


--- a/ckan/controllers/group_logic.py	Wed May 18 22:36:48 2011 +0100
+++ /dev/null	Thu Jan 01 00:00:00 1970 +0000
@@ -1,113 +0,0 @@
-
-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.lib.navl.dictization_functions import DataError, unflatten
-from ckan.logic import NotFound, NotAuthorized, ValidationError
-from ckan.logic.schema import group_form_schema
-from ckan.logic import tuplize_dict, clean_dict
-from ckan.authz import Authorizer
-
-log = logging.getLogger(__name__)
-
-class GroupLogicController(BaseController):
-
-    def new(self, data=None, errors=None, error_summary=None):
-        context = {'model': model, 'session': model.Session,
-                   'user': c.user or c.author, 'extras_as_string': True,
-                   'schema': group_form_schema(),
-                   'save': 'save' in request.params}
-
-        auth_for_create = Authorizer().am_authorized(c, model.Action.GROUP_CREATE, model.System())
-        if not auth_for_create:
-            abort(401, _('Unauthorized to create a group'))
-
-        if context['save'] and not data:
-            return self._save_new(context)
-        
-        data = data or {}
-        errors = errors or {}
-        error_summary = error_summary or {}
-        vars = {'data': data, 'errors': errors, 'error_summary': error_summary}
-
-        self._setup_template_variables(context)
-        c.form = render('group/new_group_form.html', extra_vars=vars)
-        return render('group/new.html')
-
-    def edit(self, id, data=None, errors=None, error_summary=None):
-        context = {'model': model, 'session': model.Session,
-                   'user': c.user or c.author, 'extras_as_string': True,
-                   'save': 'save' in request.params,
-                   'schema': group_form_schema(),
-                   'id': id}
-
-        if context['save'] and not data:
-            return self._save_edit(id, context)
-
-        try:
-            old_data = get.group_show(context)
-            c.grouptitle = old_data.get('title')
-            c.groupname = old_data.get('name')
-            data = data or old_data
-        except NotAuthorized:
-            abort(401, _('Unauthorized to read group %s') % '')
-
-        group = context.get("group")
-
-        am_authz = self.authorizer.am_authorized(c, model.Action.EDIT, group)
-        if not am_authz:
-            abort(401, _('User %r not authorized to edit %s') % (c.user, id))
-
-        errors = errors or {}
-        vars = {'data': data, 'errors': errors, 'error_summary': error_summary}
-
-        self._setup_template_variables(context)
-        c.form = render('group/new_group_form.html', extra_vars=vars)
-        return render('group/edit.html')
-
-    def _save_new(self, context):
-        try:
-            data_dict = clean_dict(unflatten(tuplize_dict(dict(request.params))))
-            context['message'] = data_dict.get('log_message', '')
-            group = create.group_create(data_dict, context)
-            h.redirect_to(controller='group', action='read', id=group['name'])
-        except NotAuthorized:
-            abort(401, _('Unauthorized to read group %s') % '')
-        except NotFound, e:
-            abort(404, _('Package not found'))
-        except DataError:
-            abort(400, _(u'Integrity Error'))
-        except ValidationError, e:
-            errors = e.error_dict
-            error_summary = e.error_summary
-            return self.new(data_dict, errors, error_summary)
-
-    def _save_edit(self, id, context):
-        try:
-            data_dict = clean_dict(unflatten(tuplize_dict(dict(request.params))))
-            context['message'] = data_dict.get('log_message', '')
-            group = update.group_update(data_dict, context)
-            h.redirect_to(controller='group', action='read', id=group['name'])
-        except NotAuthorized:
-            abort(401, _('Unauthorized to read group %s') % id)
-        except NotFound, e:
-            abort(404, _('Package not found'))
-        except DataError:
-            abort(400, _(u'Integrity Error'))
-        except ValidationError, e:
-            errors = e.error_dict
-            error_summary = e.error_summary
-            return self.edit(id, data_dict, errors, error_summary)
-
-    def _setup_template_variables(self, context):
-        c.is_sysadmin = Authorizer().is_sysadmin(c.user)
-
-        ## This is messy as auths take domain object not data_dict
-        group = context.get('group') or c.pkg
-        if group:
-            c.auth_for_change_state = Authorizer().am_authorized(
-                c, model.Action.CHANGE_STATE, group)


--- a/ckan/controllers/package.py	Wed May 18 22:36:48 2011 +0100
+++ b/ckan/controllers/package.py	Thu May 19 16:14:22 2011 +0100
@@ -9,17 +9,26 @@
 from pylons.i18n import get_lang, _
 from autoneg.accept import negotiate
 
-from ckan.lib.base import *
-from ckan.lib.search import query_for, QueryOptions, SearchError
+import ckan.logic.action.create as create
+import ckan.logic.action.update as update
+import ckan.logic.action.get as get
+from ckan.logic.schema import package_form_schema
+from ckan.lib.base import request, c, BaseController, model, abort, h, g, render
+from ckan.lib.base import etag_cache, response, redirect, gettext
+from ckan.authz import Authorizer
+from ckan.lib.search import query_for, SearchError
 from ckan.lib.cache import proxy_cache
 from ckan.lib.package_saver import PackageSaver, ValidationException
+from ckan.lib.navl.dictization_functions import DataError, unflatten, validate
+from ckan.logic import NotFound, NotAuthorized, ValidationError
+from ckan.logic import tuplize_dict, clean_dict, parse_params
 from ckan.plugins import PluginImplementations, IPackageController
 import ckan.forms
 import ckan.authz
 import ckan.rating
 import ckan.misc
 
-logger = logging.getLogger('ckan.controllers')
+log = logging.getLogger('ckan.controllers')
 
 def search_url(params):
     url = h.url_for(controller='package', action='search')
@@ -37,6 +46,47 @@
     ]
 
 class PackageController(BaseController):
+
+    ## hooks for subclasses 
+    package_form = 'package/new_package_form.html'
+
+    def _form_to_db_schema(self):
+        return package_form_schema()
+
+    def _db_to_form_schema(self):
+        '''This is an interface to manipulate data from the database
+        into a format suitable for the form (optional)'''
+
+    def _check_data_dict(self, data_dict):
+        '''Check if the return data is correct, mostly for checking out if
+        spammers are submitting only part of the form'''
+
+        surplus_keys_schema = ['__extras', '__junk', 'state', 'groups',
+                               'extras_validation', 'save', 'preview',
+                               'return_to']
+
+        schema_keys = package_form_schema().keys()
+        keys_in_schema = set(schema_keys) - set(surplus_keys_schema)
+
+        if keys_in_schema - set(data_dict.keys()):
+            log.info('incorrect form fields posted')
+            raise DataError(data_dict)
+
+    def _setup_template_variables(self, context):
+        c.groups = get.group_list_availible(context)
+        c.groups_authz = get.group_list_authz(context)
+        c.licences = [('', '')] + model.Package.get_license_options()
+        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)
+
+    ## end hooks
+
     authorizer = ckan.authz.Authorizer()
     extensions = PluginImplementations(IPackageController)
 
@@ -240,153 +290,115 @@
         c.pkg_revisions = c.pkg.all_related_revisions
         return render('package/history.html')
 
-    def new(self):
-        c.error = ''
-        api_url = config.get('ckan.api_url', '/').rstrip('/')
-        c.package_create_slug_api_url = api_url+h.url_for(controller='api', action='create_slug')
-        is_admin = self.authorizer.is_sysadmin(c.user)
-        # Check access control for user to create a package.
-        auth_for_create = self.authorizer.am_authorized(c, model.Action.PACKAGE_CREATE, model.System())
+    def new(self, data=None, errors=None, error_summary=None):
+        context = {'model': model, 'session': model.Session,
+                   'user': c.user or c.author, 'extras_as_string': True,
+                   'preview': 'preview' in request.params,
+                   'save': 'save' in request.params,
+                   'schema': self._form_to_db_schema()}
+
+        auth_for_create = Authorizer().am_authorized(c, model.Action.PACKAGE_CREATE, model.System())
         if not auth_for_create:
             abort(401, _('Unauthorized to create a package'))
-        # Get the name of the package form.
-        try:
-            fs = self._get_package_fieldset(is_admin=is_admin)
-        except ValueError, e:
-            abort(400, e)
-        if 'save' in request.params or 'preview' in request.params:
-            if not request.params.has_key('log_message'):
-                abort(400, ('Missing parameter: log_message'))
-            log_message = request.params['log_message']
-        record = model.Package
-        if request.params.has_key('save'):
-            fs = fs.bind(record, data=dict(request.params) or None, session=model.Session)
-            try:
-                PackageSaver().commit_pkg(fs, log_message, c.author, client=c)
-                pkgname = fs.name.value
 
-                pkg = model.Package.by_name(pkgname)
-                admins = []
-                if c.user:
-                    user = model.User.by_name(c.user)
-                    if user:
-                        admins = [user]
-                model.setup_default_user_roles(pkg, admins)
-                for item in self.extensions:
-                    item.create(pkg)
-                model.repo.commit_and_remove()
+        if (context['save'] or context['preview']) and not data:
+            return self._save_new(context)
 
-                self._form_save_redirect(pkgname, 'new')
-            except ValidationException, error:
-                fs = error.args[0]
-                c.form = self._render_edit_form(fs, request.params,
-                        clear_session=True)
-                return render('package/new.html')
-            except KeyError, error:
-                abort(400, ('Missing parameter: %s' % error.args).encode('utf8'))
+        data = data or {}
+        errors = errors or {}
+        error_summary = error_summary or {}
+        vars = {'data': data, 'errors': errors, 'error_summary': error_summary}
 
-        # use request params even when starting to allow posting from "outside"
-        # (e.g. bookmarklet)
-        if 'preview' in request.params or 'name' in request.params or 'url' in request.params:
-            if 'name' not in request.params and 'url' in request.params:
-                url = request.params.get('url')
-                domain = urlparse.urlparse(url)[1]
-                if domain.startswith('www.'):
-                    domain = domain[4:]
-            # ensure all fields specified in params (formalchemy needs this on bind)
-            data = ckan.forms.add_to_package_dict(ckan.forms.get_package_dict(fs=fs), request.params)
-            fs = fs.bind(model.Package, data=data, session=model.Session)
-        else:
-            fs = fs.bind(session=model.Session)
-        #if 'preview' in request.params:
-        #    c.preview = ' '
-        c.form = self._render_edit_form(fs, request.params, clear_session=True)
-        if 'preview' in request.params:
-            c.is_preview = True
-            try:
-                PackageSaver().render_preview(fs,
-                                              log_message=log_message,
-                                              author=c.author, client=c)
-                c.preview = h.literal(render('package/read_core.html'))
-            except ValidationException, error:
-                fs = error.args[0]
-                c.form = self._render_edit_form(fs, request.params,
-                        clear_session=True)
-                return render('package/new.html')
+        self._setup_template_variables(context)
+        c.form = render(self.package_form, extra_vars=vars)
         return render('package/new.html')
 
-    def edit(self, id=None): # allow id=None to allow posting
-        # TODO: refactor to avoid duplication between here and new
-        c.error = ''
-        c.pkg = pkg = model.Package.get(id)
-        if pkg is None:
-            abort(404, '404 Not Found')
-        model.Session().autoflush = False
-        am_authz = self.authorizer.am_authorized(c, model.Action.EDIT, pkg)
+
+    def edit(self, id, data=None, errors=None, error_summary=None):
+        context = {'model': model, 'session': model.Session,
+                   'user': c.user or c.author, 'extras_as_string': True,
+                   'preview': 'preview' in request.params,
+                   'save': 'save' in request.params,
+                   'id': id,
+                   'schema': self._form_to_db_schema()}
+
+        if (context['save'] or context['preview']) and not data:
+            return self._save_edit(id, context)
+
+        try:
+            old_data = get.package_show(context)
+            schema = self._db_to_form_schema()
+            if schema:
+                old_data, errors = validate(old_data, schema)
+            data = data or old_data
+        except NotAuthorized:
+            abort(401, _('Unauthorized to read package %s') % '')
+
+        c.pkg = context.get("package")
+
+        am_authz = self.authorizer.am_authorized(c, model.Action.EDIT, c.pkg)
         if not am_authz:
             abort(401, _('User %r not authorized to edit %s') % (c.user, id))
 
-        auth_for_change_state = self.authorizer.am_authorized(c, model.Action.CHANGE_STATE, pkg)
+        errors = errors or {}
+        vars = {'data': data, 'errors': errors, 'error_summary': error_summary}
+
+        self._setup_template_variables(context)
+        c.form = render(self.package_form, extra_vars=vars)
+        return render('package/edit.html')
+
+    def _save_new(self, context):
         try:
-            fs = self._get_package_fieldset(is_admin=auth_for_change_state)
-        except ValueError, e:
-            abort(400, e)
-        if 'save' in request.params or 'preview' in request.params:
-            if not request.params.has_key('log_message'):
-                abort(400, ('Missing parameter: log_message'))
-            log_message = request.params['log_message']
+            data_dict = clean_dict(unflatten(
+                tuplize_dict(parse_params(request.params))))
+            self._check_data_dict(data_dict)
+            context['message'] = data_dict.get('log_message', '')
+            pkg = create.package_create(data_dict, context)
 
-        if not 'save' in request.params and not 'preview' in request.params:
-            # edit
-            c.pkgname = pkg.name
-            c.pkgtitle = pkg.title
-            if pkg.license_id:
-                self._adjust_license_id_options(pkg, fs)
-            fs = fs.bind(pkg)
-            c.form = self._render_edit_form(fs, request.params)
-            return render('package/edit.html')
-        elif request.params.has_key('save'):
-            # id is the name (pre-edited state)
-            pkgname = id
-            params = dict(request.params) # needed because request is nested
-                                          # multidict which is read only
-            fs = fs.bind(pkg, data=params or None)
-            try:
-                for item in self.extensions:
-                    item.edit(fs.model)
-                PackageSaver().commit_pkg(fs, log_message, c.author, client=c)
-                # do not use package name from id, as it may have been edited
-                pkgname = fs.name.value
-                self._form_save_redirect(pkgname, 'edit')
-            except ValidationException, error:
-                fs = error.args[0]
-                c.form = self._render_edit_form(fs, request.params,
-                                                clear_session=True)
-                return render('package/edit.html')
-            except KeyError, error:
-                abort(400, 'Missing parameter: %s' % error.args)
-        else: # Must be preview
-            c.is_preview = True
-            c.pkgname = pkg.name
-            c.pkgtitle = pkg.title
-            if pkg.license_id:
-                self._adjust_license_id_options(pkg, fs)
-            fs = fs.bind(pkg, data=dict(request.params))
-            try:
-                PackageSaver().render_preview(fs,
-                                              log_message=log_message,
-                                              author=c.author, client=c)
-                c.pkgname = fs.name.value
-                c.pkgtitle = fs.title.value
-                read_core_html = render('package/read_core.html') #utf8 format
-                c.preview = h.literal(read_core_html)
-                c.form = self._render_edit_form(fs, request.params)
-            except ValidationException, error:
-                fs = error.args[0]
-                c.form = self._render_edit_form(fs, request.params,
-                        clear_session=True)
-                return render('package/edit.html')
-            return render('package/edit.html') # uses c.form and c.preview
+            if context['preview']:
+                PackageSaver().render_package(context['package'])
+                c.is_preview = True
+                c.preview = render('package/read_core.html')
+                return self.new(data_dict)
+
+            self._form_save_redirect(pkg['name'], 'new')
+        except NotAuthorized:
+            abort(401, _('Unauthorized to read package %s') % '')
+        except NotFound, e:
+            abort(404, _('Package not found'))
+        except DataError:
+            abort(400, _(u'Integrity Error'))
+        except ValidationError, e:
+            errors = e.error_dict
+            error_summary = e.error_summary
+            return self.new(data_dict, errors, error_summary)
+
+    def _save_edit(self, id, context):
+        try:
+            data_dict = clean_dict(unflatten(
+                tuplize_dict(parse_params(request.params))))
+            self._check_data_dict(data_dict)
+            context['message'] = data_dict.get('log_message', '')
+            pkg = update.package_update(data_dict, context)
+            c.pkg = context['package']
+
+            if context['preview']:
+                c.is_preview = True
+                PackageSaver().render_package(context['package'])
+                c.preview = render('package/read_core.html')
+                return self.edit(id, data_dict)
+
+            self._form_save_redirect(pkg['name'], 'edit')
+        except NotAuthorized:
+            abort(401, _('Unauthorized to read package %s') % id)
+        except NotFound, e:
+            abort(404, _('Package not found'))
+        except DataError:
+            abort(400, _(u'Integrity Error'))
+        except ValidationError, e:
+            errors = e.error_dict
+            error_summary = e.error_summary
+            return self.edit(id, data_dict, errors, error_summary)
 
     def _form_save_redirect(self, pkgname, action):
         '''This redirects the user to the CKAN package/read page,
@@ -401,7 +413,7 @@
         if url:
             url = url.replace('<NAME>', pkgname)
         else:
-            url = h.url_for(action='read', id=pkgname)
+            url = h.url_for(controller='package', action='read', id=pkgname)
         redirect(url)        
         
     def _adjust_license_id_options(self, pkg, fs):


--- /dev/null	Thu Jan 01 00:00:00 1970 +0000
+++ b/ckan/controllers/package_formalchemy.py	Thu May 19 16:14:22 2011 +0100
@@ -0,0 +1,159 @@
+import logging
+
+from ckan.lib.base import BaseController, render, c, model, abort, request
+from ckan.lib.base import  config, h, ValidationException
+from ckan.lib.package_saver import PackageSaver
+from ckan.controllers.package import PackageController
+from pylons.i18n import get_lang, _
+
+log = logging.getLogger(__name__)
+
+class PackageFormalchemyController(PackageController):
+
+    def new(self):
+        c.error = ''
+        api_url = config.get('ckan.api_url', '/').rstrip('/')
+        c.package_create_slug_api_url = api_url+h.url_for(controller='api', action='create_slug')
+        is_admin = self.authorizer.is_sysadmin(c.user)
+        # Check access control for user to create a package.
+        auth_for_create = self.authorizer.am_authorized(c, model.Action.PACKAGE_CREATE, model.System())
+        if not auth_for_create:
+            abort(401, _('Unauthorized to create a package'))
+        # Get the name of the package form.
+        try:
+            fs = self._get_package_fieldset(is_admin=is_admin)
+        except ValueError, e:
+            abort(400, e)
+        if 'save' in request.params or 'preview' in request.params:
+            if not request.params.has_key('log_message'):
+                abort(400, ('Missing parameter: log_message'))
+            log_message = request.params['log_message']
+        record = model.Package
+        if request.params.has_key('save'):
+            fs = fs.bind(record, data=dict(request.params) or None, session=model.Session)
+            try:
+                PackageSaver().commit_pkg(fs, log_message, c.author, client=c)
+                pkgname = fs.name.value
+
+                pkg = model.Package.by_name(pkgname)
+                admins = []
+                if c.user:
+                    user = model.User.by_name(c.user)
+                    if user:
+                        admins = [user]
+                model.setup_default_user_roles(pkg, admins)
+                for item in self.extensions:
+                    item.create(pkg)
+                model.repo.commit_and_remove()
+
+                self._form_save_redirect(pkgname, 'new')
+            except ValidationException, error:
+                fs = error.args[0]
+                c.form = self._render_edit_form(fs, request.params,
+                        clear_session=True)
+                return render('package/new.html')
+            except KeyError, error:
+                abort(400, ('Missing parameter: %s' % error.args).encode('utf8'))
+
+        # use request params even when starting to allow posting from "outside"
+        # (e.g. bookmarklet)
+        if 'preview' in request.params or 'name' in request.params or 'url' in request.params:
+            if 'name' not in request.params and 'url' in request.params:
+                url = request.params.get('url')
+                domain = urlparse.urlparse(url)[1]
+                if domain.startswith('www.'):
+                    domain = domain[4:]
+            # ensure all fields specified in params (formalchemy needs this on bind)
+            data = ckan.forms.add_to_package_dict(ckan.forms.get_package_dict(fs=fs), request.params)
+            fs = fs.bind(model.Package, data=data, session=model.Session)
+        else:
+            fs = fs.bind(session=model.Session)
+        #if 'preview' in request.params:
+        #    c.preview = ' '
+        c.form = self._render_edit_form(fs, request.params, clear_session=True)
+        if 'preview' in request.params:
+            c.is_preview = True
+            try:
+                PackageSaver().render_preview(fs,
+                                              log_message=log_message,
+                                              author=c.author, client=c)
+                c.preview = h.literal(render('package/read_core.html'))
+            except ValidationException, error:
+                fs = error.args[0]
+                c.form = self._render_edit_form(fs, request.params,
+                        clear_session=True)
+                return render('package/new.html')
+        return render('package/new.html')
+
+    def edit(self, id=None): # allow id=None to allow posting
+        # TODO: refactor to avoid duplication between here and new
+        c.error = ''
+        c.pkg = pkg = model.Package.get(id)
+        if pkg is None:
+            abort(404, '404 Not Found')
+        model.Session().autoflush = False
+        am_authz = self.authorizer.am_authorized(c, model.Action.EDIT, pkg)
+        if not am_authz:
+            abort(401, _('User %r not authorized to edit %s') % (c.user, id))
+
+        auth_for_change_state = self.authorizer.am_authorized(c, model.Action.CHANGE_STATE, pkg)
+        try:
+            fs = self._get_package_fieldset(is_admin=auth_for_change_state)
+        except ValueError, e:
+            abort(400, e)
+        if 'save' in request.params or 'preview' in request.params:
+            if not request.params.has_key('log_message'):
+                abort(400, ('Missing parameter: log_message'))
+            log_message = request.params['log_message']
+
+        if not 'save' in request.params and not 'preview' in request.params:
+            # edit
+            c.pkgname = pkg.name
+            c.pkgtitle = pkg.title
+            if pkg.license_id:
+                self._adjust_license_id_options(pkg, fs)
+            fs = fs.bind(pkg)
+            c.form = self._render_edit_form(fs, request.params)
+            return render('package/edit.html')
+        elif request.params.has_key('save'):
+            # id is the name (pre-edited state)
+            pkgname = id
+            params = dict(request.params) # needed because request is nested
+                                          # multidict which is read only
+            fs = fs.bind(pkg, data=params or None)
+            try:
+                for item in self.extensions:
+                    item.edit(fs.model)
+                PackageSaver().commit_pkg(fs, log_message, c.author, client=c)
+                # do not use package name from id, as it may have been edited
+                pkgname = fs.name.value
+                self._form_save_redirect(pkgname, 'edit')
+            except ValidationException, error:
+                fs = error.args[0]
+                c.form = self._render_edit_form(fs, request.params,
+                                                clear_session=True)
+                return render('package/edit.html')
+            except KeyError, error:
+                abort(400, 'Missing parameter: %s' % error.args)
+        else: # Must be preview
+            c.is_preview = True
+            c.pkgname = pkg.name
+            c.pkgtitle = pkg.title
+            if pkg.license_id:
+                self._adjust_license_id_options(pkg, fs)
+            fs = fs.bind(pkg, data=dict(request.params))
+            try:
+                PackageSaver().render_preview(fs,
+                                              log_message=log_message,
+                                              author=c.author, client=c)
+                c.pkgname = fs.name.value
+                c.pkgtitle = fs.title.value
+                read_core_html = render('package/read_core.html') #utf8 format
+                c.preview = h.literal(read_core_html)
+                c.form = self._render_edit_form(fs, request.params)
+            except ValidationException, error:
+                fs = error.args[0]
+                c.form = self._render_edit_form(fs, request.params,
+                        clear_session=True)
+                return render('package/edit.html')
+            return render('package/edit.html') # uses c.form and c.preview


--- a/ckan/controllers/package_logic.py	Wed May 18 22:36:48 2011 +0100
+++ /dev/null	Thu Jan 01 00:00:00 1970 +0000
@@ -1,165 +0,0 @@
-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.lib.navl.dictization_functions import DataError, flatten_dict, unflatten
-from ckan.logic import NotFound, NotAuthorized, ValidationError
-from ckan.logic import tuplize_dict, untuplize_dict, clean_dict
-from ckan.logic.schema import package_form_schema
-from ckan.lib.package_saver import PackageSaver
-from ckan.authz import Authorizer
-
-log = logging.getLogger(__name__)
-
-class PackageLogicController(BaseController):
-
-    def new(self, data=None, errors=None, error_summary=None):
-        context = {'model': model, 'session': model.Session,
-                   'user': c.user or c.author, 'extras_as_string': True,
-                   'preview': 'preview' in request.params,
-                   'save': 'save' in request.params,
-                   'schema': package_form_schema()}
-
-        auth_for_create = Authorizer().am_authorized(c, model.Action.PACKAGE_CREATE, model.System())
-        if not auth_for_create:
-            abort(401, _('Unauthorized to create a package'))
-
-        if (context['save'] or context['preview']) and not data:
-            return self._save_new(context)
-
-        data = data or {}
-        errors = errors or {}
-        error_summary = error_summary or {}
-        vars = {'data': data, 'errors': errors, 'error_summary': error_summary}
-
-        self._setup_template_variables(context)
-        c.form = render('package/new_package_form.html', extra_vars=vars)
-        return render('package/new.html')
-
-    def edit(self, id, data=None, errors=None, error_summary=None):
-        context = {'model': model, 'session': model.Session,
-                   'user': c.user or c.author, 'extras_as_string': True,
-                   'preview': 'preview' in request.params,
-                   'save': 'save' in request.params,
-                   'schema': package_form_schema(),
-                   'id': id}
-
-        if (context['save'] or context['preview']) and not data:
-            return self._save_edit(id, context)
-
-        try:
-            old_data = get.package_show(context)
-            data = data or old_data
-        except NotAuthorized:
-            abort(401, _('Unauthorized to read package %s') % '')
-
-        c.pkg = context.get("package")
-
-        am_authz = self.authorizer.am_authorized(c, model.Action.EDIT, c.pkg)
-        if not am_authz:
-            abort(401, _('User %r not authorized to edit %s') % (c.user, id))
-
-        errors = errors or {}
-        vars = {'data': data, 'errors': errors, 'error_summary': error_summary}
-
-        self._setup_template_variables(context)
-        c.form = render('package/new_package_form.html', extra_vars=vars)
-        return render('package/edit.html')
-
-    def _save_new(self, context):
-        try:
-            data_dict = clean_dict(unflatten(tuplize_dict(dict(request.params))))
-            self._check_data_dict(data_dict)
-            context['message'] = data_dict.get('log_message', '')
-            pkg = create.package_create(data_dict, context)
-
-            if context['preview']:
-                PackageSaver().render_package(context['package'])
-                c.is_preview = True
-                c.preview = render('package/read_core.html')
-                return self.new(data_dict)
-
-            self._form_save_redirect(pkg['name'], 'new')
-        except NotAuthorized:
-            abort(401, _('Unauthorized to read package %s') % '')
-        except NotFound, e:
-            abort(404, _('Package not found'))
-        except DataError:
-            abort(400, _(u'Integrity Error'))
-        except ValidationError, e:
-            errors = e.error_dict
-            error_summary = e.error_summary
-            return self.new(data_dict, errors, error_summary)
-
-    def _save_edit(self, id, context):
-        try:
-            data_dict = clean_dict(unflatten(tuplize_dict(dict(request.params))))
-            self._check_data_dict(data_dict)
-            context['message'] = data_dict.get('log_message', '')
-            pkg = update.package_update(data_dict, context)
-            c.pkg = context['package']
-
-            if context['preview']:
-                c.is_preview = True
-                PackageSaver().render_package(context['package'])
-                c.preview = render('package/read_core.html')
-                return self.edit(id, data_dict)
-
-            self._form_save_redirect(pkg['name'], 'edit')
-        except NotAuthorized:
-            abort(401, _('Unauthorized to read package %s') % id)
-        except NotFound, e:
-            abort(404, _('Package not found'))
-        except DataError:
-            abort(400, _(u'Integrity Error'))
-        except ValidationError, e:
-            errors = e.error_dict
-            error_summary = e.error_summary
-            return self.edit(id, data_dict, errors, error_summary)
-
-    def _check_data_dict(self, data_dict):
-        '''Check if the return data is correct'''
-
-        surplus_keys_schema = ['__extras', '__junk', 'state', 'groups',
-                               'extras_validation', 'save', 'preview',
-                               'return_to']
-
-        schema_keys = package_form_schema().keys()
-        keys_in_schema = set(schema_keys) - set(surplus_keys_schema)
-
-        if keys_in_schema - set(data_dict.keys()):
-            log.info('incorrect form fields posted')
-            raise DataError(data_dict)
-
-    def _setup_template_variables(self, context):
-        c.groups = get.group_list_availible(context)
-        c.groups_authz = get.group_list_authz(context)
-        c.licences = [('', '')] + model.Package.get_license_options()
-        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_save_redirect(self, pkgname, action):
-        '''This redirects the user to the CKAN package/read page,
-        unless there is request parameter giving an alternate location,
-        perhaps an external website.
-        @param pkgname - Name of the package just edited
-        @param action - What the action of the edit was
-        '''
-        assert action in ('new', 'edit')
-        url = request.params.get('return_to') or \
-              config.get('package_%s_return_url' % action)
-        if url:
-            url = url.replace('<NAME>', pkgname)
-        else:
-            url = h.url_for(controller='package', action='read', id=pkgname)
-        redirect(url)        


--- a/ckan/tests/functional/test_group.py	Wed May 18 22:36:48 2011 +0100
+++ b/ckan/tests/functional/test_group.py	Thu May 19 16:14:22 2011 +0100
@@ -142,9 +142,13 @@
         
     def test_2_edit(self):
         offset = url_for(controller='group', action='edit', id=self.groupname)
+        print offset
         res = self.app.get(offset, status=200, extra_environ={'REMOTE_USER': 'russianfan'})
         assert 'Edit Group: %s' % self.groupname in res, res
 
+        print res
+        
+
         form = res.forms['group-edit']
         group = model.Group.by_name(self.groupname)
         titlefn = 'title'


--- a/doc/forms.rst	Wed May 18 22:36:48 2011 +0100
+++ b/doc/forms.rst	Thu May 19 16:14:22 2011 +0100
@@ -1,13 +1,84 @@
-Forms
-=====
+Forms Using Templates
+=====================
 
 The forms used to edit Packages and Groups in CKAN can be customised. This makes it easier to help users input data, helping them choose from sensible options or to use different data formats. This document sets out to show how this is achieved, without getting embroilled in the main CKAN code.
 
-Each form is defined in a python script and then 'plugged-in' to the CKAN code.
-The form script uses the FormBuilder to simply select from the standard field types, or even define some more field types. You can easily specify the content of drop-downs, hint text and the order of fields which are displayed.
-
 Note that this section deals with the form used to *edit* packages and groups, not the way they are displayed. Display is done in the CKAN templates.
 
+Location of related code
+------------------------
+ 
+ * ckan/controllers/package.py
+ * ckan/templates/package/new_package_form.html
+
+Building a package form
+-----------------------
+
+Basics
+^^^^^^
+
+You will firstly need to make a new controller in your extension.  This should subclass PackageController like::
+
+ from ckan.controllers.package import PackageController
+ class PackageNew(PackageController):
+     package_form = 'custom_package_form.html'
+
+The package_form variable in the subclass will be used as the new form template.
+
+It is recommended that you copy the package form (named new_package_form.html) and make modifications to it. However, it is possible to start from scratch.
+
+In order for you to get this new controller pointed at correctly, your extension should look like the following::
+
+ class CustomForm(SingletonPlugin):
+
+     implements(IRoutes)
+     implements(IConfigurer)
+
+     def before_map(self, map):
+         map.connect('/package/new', controller='ckanext.extension_name.controllers.PackageNewController:PackageNew', action='new')
+         map.connect('/package/edit/{id}', controller='ckanext.extension_name.controllers.PackageNewController:PackageNew', action='edit')
+         return map
+
+     def after_map(self, map):
+         return map
+
+     def update_config(self, config):
+         configure_template_directory(config, 'templates')
+
+Replace extension_name with the name of your extension. This also assumes that custom_package_form.html is located in templates subdirectory of your extension i.e ckanext/extension_name/templates/custom_package_form.html.
+
+Advanced Use
+^^^^^^^^^^^^
+
+The PackageController has a more hooks to customize how and what data is displayed. These functions can be overridden subclass of PackageController::
+
+  _setup_template_variables(self, context)
+
+This is for setting up new variables for your templates::
+
+  _form_to_db_schema(self)
+
+This defines a navl schema to customize validation and conversion to the database::
+
+  _db_to_form_schema(self)
+
+This defines a navl schema to customize conversion from the database to the form.
+
+An example of the use of these hooks can be best found on in the extension ckanext-dgu.
+
+
+Forms using FormAlchemy (depreciated)
+=====================================
+
+This is now depreciated in order to get this to work your extention must implement IRoutes and have a before_map like the following::
+
+ class Plugin(SingletonPlugin):
+     implements(IRoutes)
+
+     def before_map(self, map):
+        map.connect('/package/new', controller='package_formalchemy', action='new')
+        map.connect('/package/edit/{id}', controller='package_formalchemy', action='edit')
+        return map
 
 Location of related code
 ------------------------
@@ -174,4 +245,4 @@
 
  You can also override the config file setting with a URL parameter in your browser. For example you might browse:
 
- http://eco.ckan.net/package/edit/water-voles?package_form=my_form
\ No newline at end of file
+ http://eco.ckan.net/package/edit/water-voles?package_form=my_form


http://bitbucket.org/okfn/ckan/changeset/dd6116baee68/
changeset:   r3100:dd6116baee68
branch:      feature-1078-refactor-wui-to-use-logic-layer
user:        kindly
date:        2011-05-19 19:17:20
summary:     [forms] correction in docs
affected #:  1 file (1 byte)

--- a/doc/forms.rst	Thu May 19 16:14:22 2011 +0100
+++ b/doc/forms.rst	Thu May 19 18:17:20 2011 +0100
@@ -64,7 +64,7 @@
 
 This defines a navl schema to customize conversion from the database to the form.
 
-An example of the use of these hooks can be best found on in the extension ckanext-dgu.
+A complicated example of the use of these hooks can be found in extension ckanext-dgu.
 
 
 Forms using FormAlchemy (depreciated)


http://bitbucket.org/okfn/ckan/changeset/843b78bae016/
changeset:   r3101:843b78bae016
user:        kindly
date:        2011-05-19 19:48:11
summary:     [merge] 1078 refactor wui to use logic layer
affected #:  27 files (45.8 KB)

--- a/ckan/config/routing.py	Thu May 19 17:45:47 2011 +0100
+++ b/ckan/config/routing.py	Thu May 19 18:48:11 2011 +0100
@@ -165,6 +165,11 @@
     map.redirect("/packages", "/package")
     map.redirect("/packages/{url:.*}", "/package/{url}")
     map.connect('/package', controller='package', action='search')
+
+    ##to get back formalchemy uncomment these lines
+    ##map.connect('/package/new', controller='package_formalchemy', action='new')
+    ##map.connect('/package/edit/{id}', controller='package_formalchemy', action='edit')
+
     map.connect('/package/{action}', controller='package',
         requirements=dict(action='|'.join([
             'list',
@@ -173,7 +178,6 @@
             'search'
             ]))
         )
-    # for backwards compatability
     map.connect('/package', controller='package', action='index')
     map.connect('/package/{action}/{id}', controller='package',
         requirements=dict(action='|'.join([
@@ -186,6 +190,11 @@
     # group
     map.redirect("/groups", "/group")
     map.redirect("/groups/{url:.*}", "/group/{url}")
+
+    ##to get back formalchemy uncomment these lines
+    ##map.connect('/group/new', controller='group_formalchemy', action='new')
+    ##map.connect('/group/edit/{id}', controller='group_formalchemy', action='edit')
+
     map.connect('/group', controller='group', action='index')
     map.connect('/group/list', controller='group', action='list')
     map.connect('/group/new', controller='group', action='new')


--- a/ckan/controllers/api.py	Thu May 19 17:45:47 2011 +0100
+++ b/ckan/controllers/api.py	Thu May 19 18:48:11 2011 +0100
@@ -154,9 +154,9 @@
 
         action_map = {
             'revision': get.revision_show,
-            'group': get.group_show,
+            'group': get.group_show_rest,
             'tag': get.tag_show,
-            'package': get.package_show,
+            'package': get.package_show_rest,
             ('package', 'relationships'): get.package_relationships_list,
         }
 
@@ -174,6 +174,7 @@
             response.status_int = 400
             return gettext('Cannot read entity of this type: %s') % register
         try:
+            
             return self._finish_ok(action(context))
         except NotFound, e:
             extra_msg = e.extra_msg
@@ -188,13 +189,15 @@
 
         action_map = {
             ('package', 'relationships'): create.package_relationship_create,
-             'group': create.group_create,
+             'group': create.group_create_rest,
              'package': create.package_create_rest,
              'rating': create.rating_create,
         }
+
         for type in model.PackageRelationship.get_all_types():
             action_map[('package', type)] = create.package_relationship_create
 
+
         context = {'model': model, 'session': model.Session, 'user': c.user,
                    'id': id, 'id2': id2, 'rel': subregister,
                    'api_version': ver}
@@ -305,6 +308,7 @@
             return self._finish(409, e.error_dict, content_type='json')
 
     def search(self, ver=None, register=None):
+        
         log.debug('search %s params: %r' % (register, request.params))
         if register == 'revision':
             since_time = None


--- a/ckan/controllers/group.py	Thu May 19 17:45:47 2011 +0100
+++ b/ckan/controllers/group.py	Thu May 19 18:48:11 2011 +0100
@@ -1,22 +1,50 @@
 import genshi
 
 from sqlalchemy.orm import eagerload_all
-from ckan.lib.base import *
+from ckan.lib.base import BaseController, c, model, request, render, h
+from ckan.lib.base import ValidationException, abort
 from pylons.i18n import get_lang, _
 import ckan.authz as authz
-from ckan import forms
-from ckan.model import System
+from ckan.authz import Authorizer
 from ckan.lib.helpers import Page
 from ckan.plugins import PluginImplementations, IGroupController
+import ckan.logic.action.create as create
+import ckan.logic.action.update as update
+import ckan.logic.action.get as get
+from ckan.lib.navl.dictization_functions import DataError, unflatten, validate
+from ckan.logic import NotFound, NotAuthorized, ValidationError
+from ckan.logic.schema import group_form_schema
+from ckan.logic import tuplize_dict, clean_dict, parse_params
+import ckan.forms
 
 class GroupController(BaseController):
-    
+
+    ## hooks for subclasses 
+    group_form = 'group/new_group_form.html'
+
+    def _form_to_db_schema(self):
+        return group_form_schema()
+
+    def _db_to_form_schema(self):
+        '''This is an interface to manipulate data from the database
+        into a format suitable for the form (optional)'''
+
+    def _setup_template_variables(self, context):
+        c.is_sysadmin = Authorizer().is_sysadmin(c.user)
+
+        ## This is messy as auths take domain object not data_dict
+        group = context.get('group') or c.pkg
+        if group:
+            c.auth_for_change_state = Authorizer().am_authorized(
+                c, model.Action.CHANGE_STATE, group)
+
+    ## end hooks
+
     def __init__(self):
         BaseController.__init__(self)
         self.extensions = PluginImplementations(IGroupController)
     
     def index(self):
-        from ckan.lib.helpers import Page
         
         if not self.authorizer.am_authorized(c, model.Action.SITE_READ, model.System):
             abort(401, _('Not authorized to see this page'))
@@ -32,6 +60,7 @@
         )
         return render('group/index.html')
 
+
     def read(self, id):
         c.group = model.Group.get(id)
         if c.group is None:
@@ -56,109 +85,99 @@
             extension.read(c.group)
         return render('group/read.html')
 
-    def new(self):
-        record = model.Group
-        c.error = ''
-        
-        auth_for_create = self.authorizer.am_authorized(c, model.Action.GROUP_CREATE, model.System())
+    def new(self, data=None, errors=None, error_summary=None):
+        context = {'model': model, 'session': model.Session,
+                   'user': c.user or c.author, 'extras_as_string': True,
+                   'schema': self._form_to_db_schema(),
+                   'save': 'save' in request.params}
+
+        auth_for_create = Authorizer().am_authorized(c, model.Action.GROUP_CREATE, model.System())
         if not auth_for_create:
             abort(401, _('Unauthorized to create a group'))
+
+        if context['save'] and not data:
+            return self._save_new(context)
         
-        is_admin = self.authorizer.is_sysadmin(c.user)
-        fs = ckan.forms.get_group_fieldset(is_admin=is_admin)
+        data = data or {}
+        errors = errors or {}
+        error_summary = error_summary or {}
+        vars = {'data': data, 'errors': errors, 'error_summary': error_summary}
 
-        if request.params.has_key('save'):
-            rev = model.repo.new_revision()
-            rev.author = c.author
-            # needed because request is nested
-            # multidict which is read only
-            params = dict(request.params)
-            c.fs = fs.bind(record, data=params or None, session=model.Session)
-            try:
-                self._update(c.fs, id, record.id)
-            except ValidationException, error:
-                fs = error.args[0]
-                c.form = self._render_edit_form(fs)
-                return render('group/edit.html')
-            # do not use groupname from id as may have changed
-            c.groupname = c.fs.name.value
-            c.grouptitle = c.fs.title.value
-            group = model.Group.get(c.groupname)
-            assert group
-            admins = []
-            user = model.User.by_name(c.user)
-            admins = [user]
-            model.setup_default_user_roles(group, admins)
-            group = model.Group.get(c.groupname)
-            pkgs = [model.Package.by_name(name) for name in request.params.getall('Group-packages-current')]
-            group.packages = pkgs
-            pkgnames = request.params.getall('PackageGroup--package_name')
-            for pkgname in pkgnames:
-                if pkgname:
-                    package = model.Package.by_name(pkgname)
-                    if package and package not in group.packages:
-                        group.packages.append(package)
-            for extension in self.extensions:
-                extension.create(group)
-            model.repo.commit_and_remove()
-            h.redirect_to(action='read', id=c.groupname)
-
-        if request.params:
-            data = forms.edit_group_dict(ckan.forms.get_group_dict(), request.params)
-            fs = fs.bind(data=data, session=model.Session)
-        c.form = self._render_edit_form(fs)
+        self._setup_template_variables(context)
+        c.form = render(self.group_form, extra_vars=vars)
         return render('group/new.html')
 
-    def edit(self, id=None): # allow id=None to allow posting
-        c.error = ''
-        group = model.Group.get(id)
-        if group is None:
-            abort(404, '404 Not Found')
+    def edit(self, id, data=None, errors=None, error_summary=None):
+        context = {'model': model, 'session': model.Session,
+                   'user': c.user or c.author, 'extras_as_string': True,
+                   'save': 'save' in request.params,
+                   'schema': self._form_to_db_schema(),
+                   'id': id}
+
+        if context['save'] and not data:
+            return self._save_edit(id, context)
+
+        try:
+            old_data = get.group_show(context)
+            c.grouptitle = old_data.get('title')
+            c.groupname = old_data.get('name')
+            schema = self._db_to_form_schema()
+            if schema:
+                old_data, errors = validate(old_data, schema)
+
+            data = data or old_data
+
+        except NotAuthorized:
+            abort(401, _('Unauthorized to read group %s') % '')
+
+        group = context.get("group")
+
         am_authz = self.authorizer.am_authorized(c, model.Action.EDIT, group)
         if not am_authz:
-            abort(401, _('User %r not authorized to edit %r') % (c.user, id))
-            
-        auth_for_change_state = self.authorizer.am_authorized(c, model.Action.CHANGE_STATE, group)
-        
-        if not 'save' in request.params:
-            c.group = group
-            c.groupname = group.name
-            c.grouptitle = group.title
-            
-            fs = forms.get_group_fieldset(is_admin=auth_for_change_state).bind(c.group)
-            c.form = self._render_edit_form(fs)
-            return render('group/edit.html')
-        else:
-            rev = model.repo.new_revision()
-            rev.author = c.author
-            # id is the name (pre-edited state)
-            c.groupname = id
-            # needed because request is nested
-            # multidict which is read only
-            params = dict(request.params)
-            fs = ckan.forms.get_group_fieldset(is_admin=auth_for_change_state)
-            c.fs = fs.bind(group, data=params or None)
-            try:
-                self._update(c.fs, id, group.id)
-                # do not use groupname from id as may have changed
-                c.groupname = c.fs.name.value
-                c.grouptitle = c.fs.title.value
-            except ValidationException, error:
-                fs = error.args[0]
-                c.form = self._render_edit_form(fs)
-                return render('group/edit.html')
-            pkgs = [model.Package.by_name(name) for name in request.params.getall('Group-packages-current')]
-            group.packages = pkgs
-            pkgnames = request.params.getall('PackageGroup--package_name')
-            for pkgname in pkgnames:
-                if pkgname:
-                    package = model.Package.by_name(pkgname)
-                    if package and package not in group.packages:
-                        group.packages.append(package)
-            for extension in self.extensions: 
-                extension.edit(group)
-            model.repo.commit_and_remove()
-            h.redirect_to(action='read', id=c.groupname)
+            abort(401, _('User %r not authorized to edit %s') % (c.user, id))
+
+        errors = errors or {}
+        vars = {'data': data, 'errors': errors, 'error_summary': error_summary}
+
+        self._setup_template_variables(context)
+        c.form = render(self.group_form, extra_vars=vars)
+        return render('group/edit.html')
+
+    def _save_new(self, context):
+        try:
+            data_dict = clean_dict(unflatten(
+                tuplize_dict(parse_params(request.params))))
+            context['message'] = data_dict.get('log_message', '')
+            group = create.group_create(data_dict, context)
+            h.redirect_to(controller='group', action='read', id=group['name'])
+        except NotAuthorized:
+            abort(401, _('Unauthorized to read group %s') % '')
+        except NotFound, e:
+            abort(404, _('Package not found'))
+        except DataError:
+            abort(400, _(u'Integrity Error'))
+        except ValidationError, e:
+            errors = e.error_dict
+            error_summary = e.error_summary
+            return self.new(data_dict, errors, error_summary)
+
+    def _save_edit(self, id, context):
+        try:
+            data_dict = clean_dict(unflatten(
+                tuplize_dict(parse_params(request.params))))
+            context['message'] = data_dict.get('log_message', '')
+            group = update.group_update(data_dict, context)
+            h.redirect_to(controller='group', action='read', id=group['name'])
+        except NotAuthorized:
+            abort(401, _('Unauthorized to read group %s') % id)
+        except NotFound, e:
+            abort(404, _('Package not found'))
+        except DataError:
+            abort(400, _(u'Integrity Error'))
+        except ValidationError, e:
+            errors = e.error_dict
+            error_summary = e.error_summary
+            return self.edit(id, data_dict, errors, error_summary)
 
     def authz(self, id):
         c.group = model.Group.get(id)
@@ -215,7 +234,7 @@
                 # new_roles = ckan.forms.new_roles_fs.bind(model.GroupRole, data=params or None)
                 # new_roles.sync()
                 for extension in self.extensions:
-                    extensions.authz_add_role(newgrouprole)
+                    extension.authz_add_role(newgrouprole)
                 model.Session.commit()
                 model.Session.remove()
                 c.message = _(u'Added role \'%s\' for authorization group \'%s\'') % (


--- /dev/null	Thu Jan 01 00:00:00 1970 +0000
+++ b/ckan/controllers/group_formalchemy.py	Thu May 19 18:48:11 2011 +0100
@@ -0,0 +1,125 @@
+
+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.lib.navl.dictization_functions import DataError, unflatten
+from ckan.logic import NotFound, NotAuthorized, ValidationError
+from ckan.logic.schema import group_form_schema
+from ckan.logic import tuplize_dict, clean_dict
+from ckan.authz import Authorizer
+import ckan.forms as forms
+import ckan.forms
+from ckan.lib.base import ValidationException
+from ckan.controllers.group import GroupController
+
+log = logging.getLogger(__name__)
+
+class GroupFormalchemyController(GroupController):
+
+    def new(self):
+        record = model.Group
+        c.error = ''
+        
+        auth_for_create = self.authorizer.am_authorized(c, model.Action.GROUP_CREATE, model.System())
+        if not auth_for_create:
+            abort(401, _('Unauthorized to create a group'))
+        
+        is_admin = self.authorizer.is_sysadmin(c.user)
+        fs = ckan.forms.get_group_fieldset(is_admin=is_admin)
+
+        if request.params.has_key('save'):
+            rev = model.repo.new_revision()
+            rev.author = c.author
+            # needed because request is nested
+            # multidict which is read only
+            params = dict(request.params)
+            c.fs = fs.bind(record, data=params or None, session=model.Session)
+            try:
+                self._update(c.fs, id, record.id)
+            except ValidationException, error:
+                fs = error.args[0]
+                c.form = self._render_edit_form(fs)
+                return render('group/edit.html')
+            # do not use groupname from id as may have changed
+            c.groupname = c.fs.name.value
+            c.grouptitle = c.fs.title.value
+            group = model.Group.get(c.groupname)
+            assert group
+            admins = []
+            user = model.User.by_name(c.user)
+            admins = [user]
+            model.setup_default_user_roles(group, admins)
+            group = model.Group.get(c.groupname)
+            pkgs = [model.Package.by_name(name) for name in request.params.getall('Group-packages-current')]
+            group.packages = pkgs
+            pkgnames = request.params.getall('PackageGroup--package_name')
+            for pkgname in pkgnames:
+                if pkgname:
+                    package = model.Package.by_name(pkgname)
+                    if package and package not in group.packages:
+                        group.packages.append(package)
+            for extension in self.extensions:
+                extension.create(group)
+            model.repo.commit_and_remove()
+            h.redirect_to(action='read', id=c.groupname)
+
+        if request.params:
+            data = forms.edit_group_dict(ckan.forms.get_group_dict(), request.params)
+            fs = fs.bind(data=data, session=model.Session)
+        c.form = self._render_edit_form(fs)
+        return render('group/new.html')
+
+    def edit(self, id=None): # allow id=None to allow posting
+        c.error = ''
+        group = model.Group.get(id)
+        if group is None:
+            abort(404, '404 Not Found')
+        am_authz = self.authorizer.am_authorized(c, model.Action.EDIT, group)
+        if not am_authz:
+            abort(401, _('User %r not authorized to edit %r') % (c.user, id))
+            
+        auth_for_change_state = self.authorizer.am_authorized(c, model.Action.CHANGE_STATE, group)
+        
+        if not 'save' in request.params:
+            c.group = group
+            c.groupname = group.name
+            c.grouptitle = group.title
+            
+            fs = forms.get_group_fieldset(is_admin=auth_for_change_state).bind(c.group)
+            c.form = self._render_edit_form(fs)
+            return render('group/edit.html')
+        else:
+            rev = model.repo.new_revision()
+            rev.author = c.author
+            # id is the name (pre-edited state)
+            c.groupname = id
+            # needed because request is nested
+            # multidict which is read only
+            params = dict(request.params)
+            fs = ckan.forms.get_group_fieldset(is_admin=auth_for_change_state)
+            c.fs = fs.bind(group, data=params or None)
+            try:
+                self._update(c.fs, id, group.id)
+                # do not use groupname from id as may have changed
+                c.groupname = c.fs.name.value
+                c.grouptitle = c.fs.title.value
+            except ValidationException, error:
+                fs = error.args[0]
+                c.form = self._render_edit_form(fs)
+                return render('group/edit.html')
+            pkgs = [model.Package.by_name(name) for name in request.params.getall('Group-packages-current')]
+            group.packages = pkgs
+            pkgnames = request.params.getall('PackageGroup--package_name')
+            for pkgname in pkgnames:
+                if pkgname:
+                    package = model.Package.by_name(pkgname)
+                    if package and package not in group.packages:
+                        group.packages.append(package)
+            for extension in self.extensions: 
+                extension.edit(group)
+            model.repo.commit_and_remove()
+            h.redirect_to(action='read', id=c.groupname)


--- a/ckan/controllers/package.py	Thu May 19 17:45:47 2011 +0100
+++ b/ckan/controllers/package.py	Thu May 19 18:48:11 2011 +0100
@@ -9,17 +9,26 @@
 from pylons.i18n import get_lang, _
 from autoneg.accept import negotiate
 
-from ckan.lib.base import *
-from ckan.lib.search import query_for, QueryOptions, SearchError
+import ckan.logic.action.create as create
+import ckan.logic.action.update as update
+import ckan.logic.action.get as get
+from ckan.logic.schema import package_form_schema
+from ckan.lib.base import request, c, BaseController, model, abort, h, g, render
+from ckan.lib.base import etag_cache, response, redirect, gettext
+from ckan.authz import Authorizer
+from ckan.lib.search import query_for, SearchError
 from ckan.lib.cache import proxy_cache
 from ckan.lib.package_saver import PackageSaver, ValidationException
+from ckan.lib.navl.dictization_functions import DataError, unflatten, validate
+from ckan.logic import NotFound, NotAuthorized, ValidationError
+from ckan.logic import tuplize_dict, clean_dict, parse_params
 from ckan.plugins import PluginImplementations, IPackageController
 import ckan.forms
 import ckan.authz
 import ckan.rating
 import ckan.misc
 
-logger = logging.getLogger('ckan.controllers')
+log = logging.getLogger('ckan.controllers')
 
 def search_url(params):
     url = h.url_for(controller='package', action='search')
@@ -37,6 +46,47 @@
     ]
 
 class PackageController(BaseController):
+
+    ## hooks for subclasses 
+    package_form = 'package/new_package_form.html'
+
+    def _form_to_db_schema(self):
+        return package_form_schema()
+
+    def _db_to_form_schema(self):
+        '''This is an interface to manipulate data from the database
+        into a format suitable for the form (optional)'''
+
+    def _check_data_dict(self, data_dict):
+        '''Check if the return data is correct, mostly for checking out if
+        spammers are submitting only part of the form'''
+
+        surplus_keys_schema = ['__extras', '__junk', 'state', 'groups',
+                               'extras_validation', 'save', 'preview',
+                               'return_to']
+
+        schema_keys = package_form_schema().keys()
+        keys_in_schema = set(schema_keys) - set(surplus_keys_schema)
+
+        if keys_in_schema - set(data_dict.keys()):
+            log.info('incorrect form fields posted')
+            raise DataError(data_dict)
+
+    def _setup_template_variables(self, context):
+        c.groups = get.group_list_availible(context)
+        c.groups_authz = get.group_list_authz(context)
+        c.licences = [('', '')] + model.Package.get_license_options()
+        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)
+
+    ## end hooks
+
     authorizer = ckan.authz.Authorizer()
     extensions = PluginImplementations(IPackageController)
 
@@ -240,153 +290,115 @@
         c.pkg_revisions = c.pkg.all_related_revisions
         return render('package/history.html')
 
-    def new(self):
-        c.error = ''
-        api_url = config.get('ckan.api_url', '/').rstrip('/')
-        c.package_create_slug_api_url = api_url+h.url_for(controller='api', action='create_slug')
-        is_admin = self.authorizer.is_sysadmin(c.user)
-        # Check access control for user to create a package.
-        auth_for_create = self.authorizer.am_authorized(c, model.Action.PACKAGE_CREATE, model.System())
+    def new(self, data=None, errors=None, error_summary=None):
+        context = {'model': model, 'session': model.Session,
+                   'user': c.user or c.author, 'extras_as_string': True,
+                   'preview': 'preview' in request.params,
+                   'save': 'save' in request.params,
+                   'schema': self._form_to_db_schema()}
+
+        auth_for_create = Authorizer().am_authorized(c, model.Action.PACKAGE_CREATE, model.System())
         if not auth_for_create:
             abort(401, _('Unauthorized to create a package'))
-        # Get the name of the package form.
-        try:
-            fs = self._get_package_fieldset(is_admin=is_admin)
-        except ValueError, e:
-            abort(400, e)
-        if 'save' in request.params or 'preview' in request.params:
-            if not request.params.has_key('log_message'):
-                abort(400, ('Missing parameter: log_message'))
-            log_message = request.params['log_message']
-        record = model.Package
-        if request.params.has_key('save'):
-            fs = fs.bind(record, data=dict(request.params) or None, session=model.Session)
-            try:
-                PackageSaver().commit_pkg(fs, log_message, c.author, client=c)
-                pkgname = fs.name.value
 
-                pkg = model.Package.by_name(pkgname)
-                admins = []
-                if c.user:
-                    user = model.User.by_name(c.user)
-                    if user:
-                        admins = [user]
-                model.setup_default_user_roles(pkg, admins)
-                for item in self.extensions:
-                    item.create(pkg)
-                model.repo.commit_and_remove()
+        if (context['save'] or context['preview']) and not data:
+            return self._save_new(context)
 
-                self._form_save_redirect(pkgname, 'new')
-            except ValidationException, error:
-                fs = error.args[0]
-                c.form = self._render_edit_form(fs, request.params,
-                        clear_session=True)
-                return render('package/new.html')
-            except KeyError, error:
-                abort(400, ('Missing parameter: %s' % error.args).encode('utf8'))
+        data = data or {}
+        errors = errors or {}
+        error_summary = error_summary or {}
+        vars = {'data': data, 'errors': errors, 'error_summary': error_summary}
 
-        # use request params even when starting to allow posting from "outside"
-        # (e.g. bookmarklet)
-        if 'preview' in request.params or 'name' in request.params or 'url' in request.params:
-            if 'name' not in request.params and 'url' in request.params:
-                url = request.params.get('url')
-                domain = urlparse.urlparse(url)[1]
-                if domain.startswith('www.'):
-                    domain = domain[4:]
-            # ensure all fields specified in params (formalchemy needs this on bind)
-            data = ckan.forms.add_to_package_dict(ckan.forms.get_package_dict(fs=fs), request.params)
-            fs = fs.bind(model.Package, data=data, session=model.Session)
-        else:
-            fs = fs.bind(session=model.Session)
-        #if 'preview' in request.params:
-        #    c.preview = ' '
-        c.form = self._render_edit_form(fs, request.params, clear_session=True)
-        if 'preview' in request.params:
-            c.is_preview = True
-            try:
-                PackageSaver().render_preview(fs,
-                                              log_message=log_message,
-                                              author=c.author, client=c)
-                c.preview = h.literal(render('package/read_core.html'))
-            except ValidationException, error:
-                fs = error.args[0]
-                c.form = self._render_edit_form(fs, request.params,
-                        clear_session=True)
-                return render('package/new.html')
+        self._setup_template_variables(context)
+        c.form = render(self.package_form, extra_vars=vars)
         return render('package/new.html')
 
-    def edit(self, id=None): # allow id=None to allow posting
-        # TODO: refactor to avoid duplication between here and new
-        c.error = ''
-        c.pkg = pkg = model.Package.get(id)
-        if pkg is None:
-            abort(404, '404 Not Found')
-        model.Session().autoflush = False
-        am_authz = self.authorizer.am_authorized(c, model.Action.EDIT, pkg)
+
+    def edit(self, id, data=None, errors=None, error_summary=None):
+        context = {'model': model, 'session': model.Session,
+                   'user': c.user or c.author, 'extras_as_string': True,
+                   'preview': 'preview' in request.params,
+                   'save': 'save' in request.params,
+                   'id': id,
+                   'schema': self._form_to_db_schema()}
+
+        if (context['save'] or context['preview']) and not data:
+            return self._save_edit(id, context)
+
+        try:
+            old_data = get.package_show(context)
+            schema = self._db_to_form_schema()
+            if schema:
+                old_data, errors = validate(old_data, schema)
+            data = data or old_data
+        except NotAuthorized:
+            abort(401, _('Unauthorized to read package %s') % '')
+
+        c.pkg = context.get("package")
+
+        am_authz = self.authorizer.am_authorized(c, model.Action.EDIT, c.pkg)
         if not am_authz:
             abort(401, _('User %r not authorized to edit %s') % (c.user, id))
 
-        auth_for_change_state = self.authorizer.am_authorized(c, model.Action.CHANGE_STATE, pkg)
+        errors = errors or {}
+        vars = {'data': data, 'errors': errors, 'error_summary': error_summary}
+
+        self._setup_template_variables(context)
+        c.form = render(self.package_form, extra_vars=vars)
+        return render('package/edit.html')
+
+    def _save_new(self, context):
         try:
-            fs = self._get_package_fieldset(is_admin=auth_for_change_state)
-        except ValueError, e:
-            abort(400, e)
-        if 'save' in request.params or 'preview' in request.params:
-            if not request.params.has_key('log_message'):
-                abort(400, ('Missing parameter: log_message'))
-            log_message = request.params['log_message']
+            data_dict = clean_dict(unflatten(
+                tuplize_dict(parse_params(request.params))))
+            self._check_data_dict(data_dict)
+            context['message'] = data_dict.get('log_message', '')
+            pkg = create.package_create(data_dict, context)
 
-        if not 'save' in request.params and not 'preview' in request.params:
-            # edit
-            c.pkgname = pkg.name
-            c.pkgtitle = pkg.title
-            if pkg.license_id:
-                self._adjust_license_id_options(pkg, fs)
-            fs = fs.bind(pkg)
-            c.form = self._render_edit_form(fs, request.params)
-            return render('package/edit.html')
-        elif request.params.has_key('save'):
-            # id is the name (pre-edited state)
-            pkgname = id
-            params = dict(request.params) # needed because request is nested
-                                          # multidict which is read only
-            fs = fs.bind(pkg, data=params or None)
-            try:
-                for item in self.extensions:
-                    item.edit(fs.model)
-                PackageSaver().commit_pkg(fs, log_message, c.author, client=c)
-                # do not use package name from id, as it may have been edited
-                pkgname = fs.name.value
-                self._form_save_redirect(pkgname, 'edit')
-            except ValidationException, error:
-                fs = error.args[0]
-                c.form = self._render_edit_form(fs, request.params,
-                                                clear_session=True)
-                return render('package/edit.html')
-            except KeyError, error:
-                abort(400, 'Missing parameter: %s' % error.args)
-        else: # Must be preview
-            c.is_preview = True
-            c.pkgname = pkg.name
-            c.pkgtitle = pkg.title
-            if pkg.license_id:
-                self._adjust_license_id_options(pkg, fs)
-            fs = fs.bind(pkg, data=dict(request.params))
-            try:
-                PackageSaver().render_preview(fs,
-                                              log_message=log_message,
-                                              author=c.author, client=c)
-                c.pkgname = fs.name.value
-                c.pkgtitle = fs.title.value
-                read_core_html = render('package/read_core.html') #utf8 format
-                c.preview = h.literal(read_core_html)
-                c.form = self._render_edit_form(fs, request.params)
-            except ValidationException, error:
-                fs = error.args[0]
-                c.form = self._render_edit_form(fs, request.params,
-                        clear_session=True)
-                return render('package/edit.html')
-            return render('package/edit.html') # uses c.form and c.preview
+            if context['preview']:
+                PackageSaver().render_package(context['package'])
+                c.is_preview = True
+                c.preview = render('package/read_core.html')
+                return self.new(data_dict)
+
+            self._form_save_redirect(pkg['name'], 'new')
+        except NotAuthorized:
+            abort(401, _('Unauthorized to read package %s') % '')
+        except NotFound, e:
+            abort(404, _('Package not found'))
+        except DataError:
+            abort(400, _(u'Integrity Error'))
+        except ValidationError, e:
+            errors = e.error_dict
+            error_summary = e.error_summary
+            return self.new(data_dict, errors, error_summary)
+
+    def _save_edit(self, id, context):
+        try:
+            data_dict = clean_dict(unflatten(
+                tuplize_dict(parse_params(request.params))))
+            self._check_data_dict(data_dict)
+            context['message'] = data_dict.get('log_message', '')
+            pkg = update.package_update(data_dict, context)
+            c.pkg = context['package']
+
+            if context['preview']:
+                c.is_preview = True
+                PackageSaver().render_package(context['package'])
+                c.preview = render('package/read_core.html')
+                return self.edit(id, data_dict)
+
+            self._form_save_redirect(pkg['name'], 'edit')
+        except NotAuthorized:
+            abort(401, _('Unauthorized to read package %s') % id)
+        except NotFound, e:
+            abort(404, _('Package not found'))
+        except DataError:
+            abort(400, _(u'Integrity Error'))
+        except ValidationError, e:
+            errors = e.error_dict
+            error_summary = e.error_summary
+            return self.edit(id, data_dict, errors, error_summary)
 
     def _form_save_redirect(self, pkgname, action):
         '''This redirects the user to the CKAN package/read page,
@@ -401,7 +413,7 @@
         if url:
             url = url.replace('<NAME>', pkgname)
         else:
-            url = h.url_for(action='read', id=pkgname)
+            url = h.url_for(controller='package', action='read', id=pkgname)
         redirect(url)        
         
     def _adjust_license_id_options(self, pkg, fs):


--- /dev/null	Thu Jan 01 00:00:00 1970 +0000
+++ b/ckan/controllers/package_formalchemy.py	Thu May 19 18:48:11 2011 +0100
@@ -0,0 +1,159 @@
+import logging
+
+from ckan.lib.base import BaseController, render, c, model, abort, request
+from ckan.lib.base import  config, h, ValidationException
+from ckan.lib.package_saver import PackageSaver
+from ckan.controllers.package import PackageController
+from pylons.i18n import get_lang, _
+
+log = logging.getLogger(__name__)
+
+class PackageFormalchemyController(PackageController):
+
+    def new(self):
+        c.error = ''
+        api_url = config.get('ckan.api_url', '/').rstrip('/')
+        c.package_create_slug_api_url = api_url+h.url_for(controller='api', action='create_slug')
+        is_admin = self.authorizer.is_sysadmin(c.user)
+        # Check access control for user to create a package.
+        auth_for_create = self.authorizer.am_authorized(c, model.Action.PACKAGE_CREATE, model.System())
+        if not auth_for_create:
+            abort(401, _('Unauthorized to create a package'))
+        # Get the name of the package form.
+        try:
+            fs = self._get_package_fieldset(is_admin=is_admin)
+        except ValueError, e:
+            abort(400, e)
+        if 'save' in request.params or 'preview' in request.params:
+            if not request.params.has_key('log_message'):
+                abort(400, ('Missing parameter: log_message'))
+            log_message = request.params['log_message']
+        record = model.Package
+        if request.params.has_key('save'):
+            fs = fs.bind(record, data=dict(request.params) or None, session=model.Session)
+            try:
+                PackageSaver().commit_pkg(fs, log_message, c.author, client=c)
+                pkgname = fs.name.value
+
+                pkg = model.Package.by_name(pkgname)
+                admins = []
+                if c.user:
+                    user = model.User.by_name(c.user)
+                    if user:
+                        admins = [user]
+                model.setup_default_user_roles(pkg, admins)
+                for item in self.extensions:
+                    item.create(pkg)
+                model.repo.commit_and_remove()
+
+                self._form_save_redirect(pkgname, 'new')
+            except ValidationException, error:
+                fs = error.args[0]
+                c.form = self._render_edit_form(fs, request.params,
+                        clear_session=True)
+                return render('package/new.html')
+            except KeyError, error:
+                abort(400, ('Missing parameter: %s' % error.args).encode('utf8'))
+
+        # use request params even when starting to allow posting from "outside"
+        # (e.g. bookmarklet)
+        if 'preview' in request.params or 'name' in request.params or 'url' in request.params:
+            if 'name' not in request.params and 'url' in request.params:
+                url = request.params.get('url')
+                domain = urlparse.urlparse(url)[1]
+                if domain.startswith('www.'):
+                    domain = domain[4:]
+            # ensure all fields specified in params (formalchemy needs this on bind)
+            data = ckan.forms.add_to_package_dict(ckan.forms.get_package_dict(fs=fs), request.params)
+            fs = fs.bind(model.Package, data=data, session=model.Session)
+        else:
+            fs = fs.bind(session=model.Session)
+        #if 'preview' in request.params:
+        #    c.preview = ' '
+        c.form = self._render_edit_form(fs, request.params, clear_session=True)
+        if 'preview' in request.params:
+            c.is_preview = True
+            try:
+                PackageSaver().render_preview(fs,
+                                              log_message=log_message,
+                                              author=c.author, client=c)
+                c.preview = h.literal(render('package/read_core.html'))
+            except ValidationException, error:
+                fs = error.args[0]
+                c.form = self._render_edit_form(fs, request.params,
+                        clear_session=True)
+                return render('package/new.html')
+        return render('package/new.html')
+
+    def edit(self, id=None): # allow id=None to allow posting
+        # TODO: refactor to avoid duplication between here and new
+        c.error = ''
+        c.pkg = pkg = model.Package.get(id)
+        if pkg is None:
+            abort(404, '404 Not Found')
+        model.Session().autoflush = False
+        am_authz = self.authorizer.am_authorized(c, model.Action.EDIT, pkg)
+        if not am_authz:
+            abort(401, _('User %r not authorized to edit %s') % (c.user, id))
+
+        auth_for_change_state = self.authorizer.am_authorized(c, model.Action.CHANGE_STATE, pkg)
+        try:
+            fs = self._get_package_fieldset(is_admin=auth_for_change_state)
+        except ValueError, e:
+            abort(400, e)
+        if 'save' in request.params or 'preview' in request.params:
+            if not request.params.has_key('log_message'):
+                abort(400, ('Missing parameter: log_message'))
+            log_message = request.params['log_message']
+
+        if not 'save' in request.params and not 'preview' in request.params:
+            # edit
+            c.pkgname = pkg.name
+            c.pkgtitle = pkg.title
+            if pkg.license_id:
+                self._adjust_license_id_options(pkg, fs)
+            fs = fs.bind(pkg)
+            c.form = self._render_edit_form(fs, request.params)
+            return render('package/edit.html')
+        elif request.params.has_key('save'):
+            # id is the name (pre-edited state)
+            pkgname = id
+            params = dict(request.params) # needed because request is nested
+                                          # multidict which is read only
+            fs = fs.bind(pkg, data=params or None)
+            try:
+                for item in self.extensions:
+                    item.edit(fs.model)
+                PackageSaver().commit_pkg(fs, log_message, c.author, client=c)
+                # do not use package name from id, as it may have been edited
+                pkgname = fs.name.value
+                self._form_save_redirect(pkgname, 'edit')
+            except ValidationException, error:
+                fs = error.args[0]
+                c.form = self._render_edit_form(fs, request.params,
+                                                clear_session=True)
+                return render('package/edit.html')
+            except KeyError, error:
+                abort(400, 'Missing parameter: %s' % error.args)
+        else: # Must be preview
+            c.is_preview = True
+            c.pkgname = pkg.name
+            c.pkgtitle = pkg.title
+            if pkg.license_id:
+                self._adjust_license_id_options(pkg, fs)
+            fs = fs.bind(pkg, data=dict(request.params))
+            try:
+                PackageSaver().render_preview(fs,
+                                              log_message=log_message,
+                                              author=c.author, client=c)
+                c.pkgname = fs.name.value
+                c.pkgtitle = fs.title.value
+                read_core_html = render('package/read_core.html') #utf8 format
+                c.preview = h.literal(read_core_html)
+                c.form = self._render_edit_form(fs, request.params)
+            except ValidationException, error:
+                fs = error.args[0]
+                c.form = self._render_edit_form(fs, request.params,
+                        clear_session=True)
+                return render('package/edit.html')
+            return render('package/edit.html') # uses c.form and c.preview


--- a/ckan/lib/dictization/model_dictize.py	Thu May 19 17:45:47 2011 +0100
+++ b/ckan/lib/dictization/model_dictize.py	Thu May 19 18:48:11 2011 +0100
@@ -30,7 +30,13 @@
     result_list = []
     for name, extra in extras_dict.iteritems():
         dictized = table_dictize(extra, context)
-        dictized["value"] = json.dumps(dictized["value"])
+        if not extra.state == 'active':
+            continue
+        value = dictized["value"]
+        ## This is to make sure the frontend does not show a plain string
+        ## as json with brackets.
+        if not(context.get("extras_as_string") and isinstance(value, basestring)):
+            dictized["value"] = json.dumps(value)
         result_list.append(dictized)
 
     return sorted(result_list, key=lambda x: x["key"])
@@ -65,8 +71,8 @@
 
     result_dict = table_dictize(group, context)
 
-    result_dict["extras"] = obj_dict_dictize(
-        group._extras, context, lambda x: x["key"])
+    result_dict["extras"] = extras_dict_dictize(
+        group._extras, context)
 
     result_dict["packages"] = obj_list_dictize(
         group.packages, context)


--- a/ckan/lib/dictization/model_save.py	Thu May 19 17:45:47 2011 +0100
+++ b/ckan/lib/dictization/model_save.py	Thu May 19 18:48:11 2011 +0100
@@ -45,28 +45,23 @@
 
     return obj_list
 
-def package_extras_save(extras_dicts, pkg, context):
+def extras_save(extras_dicts, context):
 
     model = context["model"]
     session = context["session"]
+    extras_as_string = context.get("extras_as_string", False)
 
     result_dict = {}
     for extra_dict in extras_dicts:
-        result_dict[extra_dict["key"]] = json.loads(extra_dict["value"])
+        if extra_dict.get("deleted"):
+            continue
+        if extras_as_string:
+            result_dict[extra_dict["key"]] = extra_dict["value"]
+        else:
+            result_dict[extra_dict["key"]] = json.loads(extra_dict["value"])
 
     return result_dict
 
-def group_extras_save(extras_dicts, pkg, context):
-
-    model = context["model"]
-    session = context["session"]
-
-    obj_dict = {}
-    for extra_dict in extras_dicts:
-        obj = table_dict_save(extra_dict, model.GroupExtra, context)
-        obj_dict[extra_dict["key"]] = obj
-
-    return obj_dict
 
 def tag_list_save(tag_dicts, context):
 
@@ -78,7 +73,7 @@
         obj = table_dict_save(table_dict, model.Tag, context)
         tag_list.append(obj)
 
-    return tag_list
+    return list(set(tag_list))
 
 def group_list_save(group_dicts, context):
 
@@ -115,6 +110,7 @@
 
     model = context["model"]
     package = context.get("package")
+    allow_partial_update = context.get("allow_partial_update", False)
     if package:
         pkg_dict["id"] = package.id 
     Package = model.Package
@@ -126,22 +122,22 @@
         pkg.resources[:] = resources
 
     tags = tag_list_save(pkg_dict.get("tags", []), context)
-    if tags:
+    if tags or not allow_partial_update:
         pkg.tags[:] = tags
 
     groups = group_list_save(pkg_dict.get("groups", []), context)
-    if groups:
+    if groups or not allow_partial_update:
         pkg.groups[:] = groups
 
     subjects = pkg_dict.get("relationships_as_subject", [])
-    if subjects:
+    if subjects or not allow_partial_update:
         pkg.relationships_as_subject[:] = relationship_list_save(subjects, context)
     objects = pkg_dict.get("relationships_as_object", [])
-    if objects:
+    if objects or not allow_partial_update:
         pkg.relationships_as_object[:] = relationship_list_save(objects, context)
 
-    extras = package_extras_save(pkg_dict.get("extras", {}), pkg, context)
-    if extras:
+    extras = extras_save(pkg_dict.get("extras", {}), context)
+    if extras or not allow_partial_update:
         old_extras = set(pkg.extras.keys())
         new_extras = set(extras.keys())
         for key in old_extras - new_extras:
@@ -157,18 +153,22 @@
     model = context["model"]
     session = context["session"]
     group = context.get("group")
+    allow_partial_update = context.get("allow_partial_update", False)
+    
     Group = model.Group
     Package = model.Package
     if group:
         group_dict["id"] = group.id 
 
     group = table_dict_save(group_dict, Group, context)
-
-    extras = group_extras_save(group_dict.get("extras", []), group, context)
-
-    group._extras.clear()
-    for key, value in extras.iteritems():
-        group._extras[key] = value
+    extras = extras_save(group_dict.get("extras", {}), context)
+    if extras or not allow_partial_update:
+        old_extras = set(group.extras.keys())
+        new_extras = set(extras.keys())
+        for key in old_extras - new_extras:
+            del group.extras[key]
+        for key in new_extras:
+            group.extras[key] = extras[key] 
 
     package_dicts = group_dict.get("packages", [])
 
@@ -180,10 +180,12 @@
         if id:
             pkg = session.query(Package).get(id)
         if not pkg:
-            pkg = session.query(Package).filter_by(name=package["name"]).one()
-        packages.append(pkg)
+            pkg = session.query(Package).filter_by(name=package["name"]).first()
+        if pkg:
+            packages.append(pkg)
 
-    group.packages[:] = packages
+    if packages or not allow_partial_update:
+        group.packages[:] = packages
 
     return group
 
@@ -212,7 +214,6 @@
                 if extras_value is not None:
                     new_value.append({"key": extras_key,
                                       "value": json.dumps(extras_value)})
-
         dictized[key] = new_value
 
     groups = dictized.pop('groups', None)


--- a/ckan/lib/navl/dictization_functions.py	Thu May 19 17:45:47 2011 +0100
+++ b/ckan/lib/navl/dictization_functions.py	Thu May 19 18:48:11 2011 +0100
@@ -19,6 +19,8 @@
         raise Invalid(fe.api._stdtrans('Missing value'))
     def __hex__(self):
         raise Invalid(fe.api._stdtrans('Missing value'))
+    def __nonzero__(self):
+        return False
 
 missing = Missing()
 
@@ -113,6 +115,15 @@
     for key, value in new_data.items():
         if key in full_schema:
             continue
+
+        ## check if any thing naugthy is placed against subschemas
+        initial_tuple = key[::2]
+        if initial_tuple in [initial_key[:len(initial_tuple)] 
+                             for initial_key in flattented_schema]:
+            if data[key] <> []:
+                raise DataError('Only lists of dicts can be placed against'
+                                'subschema %s' % key)
+                
         if key[:-1] in key_combinations:
             extras_key = key[:-1] + ('__extras',)
             extras = new_data.get(extras_key, {})
@@ -166,6 +177,9 @@
     try:
         converter(key, converted_data, errors, context)
         return
+    except Invalid, e:
+        errors[key].append(e.error)
+        return
     except TypeError, e:
         ## hack to make sure the type error was caused by the wrong
         ## number of arguements given.
@@ -222,9 +236,7 @@
 
     context = context or {}
     assert isinstance(data, dict)
-    flattened = flatten_dict(data)
-    converted_data, errors = _validate(flattened, schema, context)
-    converted_data = unflatten(converted_data)
+    converted_data, errors = _validate(data, schema, context)
 
     for key, value in errors.items():
         if not value:
@@ -311,7 +323,7 @@
 
     for key, value in data.iteritems():
         new_key = old_key + [key]
-        if isinstance(value, list):
+        if isinstance(value, list) and value and isinstance(value[0], dict):
             flattened = flatten_list(value, flattened, new_key)
         else:
             flattened[tuple(new_key)] = value


--- a/ckan/lib/navl/validators.py	Thu May 19 17:45:47 2011 +0100
+++ b/ckan/lib/navl/validators.py	Thu May 19 18:48:11 2011 +0100
@@ -74,7 +74,15 @@
 
     value = data.get(key)
 
-    if not value or value is missing:
+    if value is missing or value is None:
+        data.pop(key, None)
+        raise StopOnError
+
+def ignore_empty(key, data, errors, context):
+
+    value = data.get(key)
+
+    if value is missing or not value:
         data.pop(key, None)
         raise StopOnError
 


--- a/ckan/logic/__init__.py	Thu May 19 17:45:47 2011 +0100
+++ b/ckan/logic/__init__.py	Thu May 19 18:48:11 2011 +0100
@@ -12,12 +12,59 @@
     pass
 
 class ValidationError(ActionError):
-    def __init__(self, error_dict, extra_msg=None):
+    def __init__(self, error_dict, error_summary=None, extra_msg=None):
         self.error_dict = error_dict
+        self.error_summary = error_summary
         self.extra_msg = extra_msg
 
 log = logging.getLogger(__name__)
 
+def parse_params(params):
+    parsed = {}
+    for key in params:
+        value = params.getall(key)
+        if not value:
+            value = ''
+        if len(value) == 1:
+            value = value[0]
+        parsed[key] = value
+    return parsed
+
+
+def clean_dict(data_dict):
+    for key, value in data_dict.items():
+        if not isinstance(value, list):
+            continue
+        for inner_dict in value[:]:
+            if isinstance(inner_dict, basestring):
+                break
+            if not any(inner_dict.values()):
+                value.remove(inner_dict)
+            else:
+                clean_dict(inner_dict)
+    return data_dict
+
+def tuplize_dict(data_dict):
+    ''' gets a dict with keys of the form 'table__0__key' and converts them
+    to a tuple like ('table', 0, 'key')'''
+
+    tuplized_dict = {}
+    for key, value in data_dict.iteritems():
+        key_list = key.split('__')
+        for num, key in enumerate(key_list):
+            if num % 2 == 1:
+                key_list[num] = int(key)
+        tuplized_dict[tuple(key_list)] = value
+    return tuplized_dict
+
+def untuplize_dict(tuplized_dict):
+
+    data_dict = {}
+    for key, value in tuplized_dict.iteritems():
+        new_key = '__'.join([str(item) for item in key])
+        data_dict[new_key] = value
+    return data_dict
+
 def check_access(entity, action, context):
     model = context["model"]
     user = context.get("user")


--- a/ckan/logic/action/create.py	Thu May 19 17:45:47 2011 +0100
+++ b/ckan/logic/action/create.py	Thu May 19 18:48:11 2011 +0100
@@ -6,7 +6,11 @@
                           IPackageController)
 from ckan.logic import NotFound, check_access, NotAuthorized, ValidationError
 from ckan.lib.base import _
-from ckan.lib.dictization.model_dictize import package_to_api1, package_to_api2
+from ckan.lib.dictization.model_dictize import (package_to_api1,
+                                                package_to_api2,
+                                                group_to_api1,
+                                                group_to_api2)
+
 from ckan.lib.dictization.model_save import (group_api_to_dict,
                                              group_dict_save,
                                              package_api_to_dict,
@@ -19,46 +23,55 @@
 from ckan.logic.schema import default_create_package_schema, default_resource_schema
 
 from ckan.logic.schema import default_group_schema
-from ckan.lib.navl.dictization_functions import validate
-from ckan.logic.action.update import _update_package_relationship
+from ckan.lib.navl.dictization_functions import validate 
+from ckan.logic.action.update import (_update_package_relationship,
+                                      package_error_summary,
+                                      group_error_summary,
+                                      check_group_auth)
 log = logging.getLogger(__name__)
 
-
 def package_create(data_dict, context):
 
     model = context['model']
     user = context['user']
+    preview = context.get('preview', False)
     schema = context.get('schema') or default_create_package_schema()
+    model.Session.remove()
 
     check_access(model.System(), model.Action.PACKAGE_CREATE, context)
+    check_group_auth(data_dict, context)
 
-    data, errors = validate(data_dict,
-                            schema,
-                            context)
+    data, errors = validate(data_dict, schema, context)
 
     if errors:
-        raise ValidationError(errors)
+        model.Session.rollback()
+        raise ValidationError(errors, package_error_summary(errors))
 
-    rev = model.repo.new_revision()
-    rev.author = user
-    rev.message = _(u'REST API: Create object %s') % data["name"]
+    if not preview:
+        rev = model.repo.new_revision()
+        rev.author = user
+        if 'message' in context:
+            rev.message = context['message']
+        else:
+            rev.message = _(u'REST API: Create object %s') % data.get("name")
 
     pkg = package_dict_save(data, context)
-
+    admins = []
     if user:
         admins = [model.User.by_name(user.decode('utf8'))]
-    else:
-        admins = []
-    model.setup_default_user_roles(pkg, admins)
-    for item in PluginImplementations(IPackageController):
-        item.create(pkg)
-    model.repo.commit()        
-    ## need to let rest api create
+
+    if not preview:
+        model.setup_default_user_roles(pkg, admins)
+        for item in PluginImplementations(IPackageController):
+            item.create(pkg)
+        model.repo.commit()        
+
+    ## need to let rest api create and preview
     context["package"] = pkg
     ## this is added so that the rest controller can make a new location 
     context["id"] = pkg.id
     log.debug('Created object %s' % str(pkg.name))
-    return package_dictize(pkg, context)
+    return package_dictize(pkg, context) 
 
 def resource_create(data_dict, context):
     model = context['model']
@@ -111,22 +124,23 @@
 def group_create(data_dict, context):
     model = context['model']
     user = context['user']
+    schema = context.get('schema') or default_group_schema()
 
     check_access(model.System(), model.Action.GROUP_CREATE, context)
 
-    context = {'model': model, 'session': model.Session}
-    dictized = group_api_to_dict(data_dict, context)
-
-    data, errors = validate(dictized,
-                            default_group_schema(),
-                            context)
+    data, errors = validate(data_dict, schema, context)
 
     if errors:
-        raise ValidationError(errors)
+        model.Session.rollback()
+        raise ValidationError(errors, group_error_summary(errors))
 
     rev = model.repo.new_revision()
     rev.author = user
-    rev.message = _(u'REST API: Create object %s') % data['name']
+
+    if 'message' in context:
+        rev.message = context['message']
+    else:
+        rev.message = _(u'REST API: Create object %s') % data.get("name")
 
     group = group_dict_save(data, context)
 
@@ -138,6 +152,7 @@
     for item in PluginImplementations(IGroupController):
         item.create(group)
     model.repo.commit()        
+    context["group"] = group
     context["id"] = group.id
     log.debug('Created object %s' % str(group.name))
     return group_dictize(group, context)
@@ -186,7 +201,7 @@
     dictized_package = package_api_to_dict(data_dict, context)
     dictized_after = package_create(dictized_package, context) 
 
-    pkg = context["package"]
+    pkg = context['package']
 
     if api == '1':
         package_dict = package_to_api1(pkg, context)
@@ -195,3 +210,19 @@
 
     return package_dict
 
+def group_create_rest(data_dict, context):
+
+    api = context.get('api_version') or '1'
+
+    dictized_group = group_api_to_dict(data_dict, context)
+    dictized_after = group_create(dictized_group, context) 
+
+    group = context['group']
+
+    if api == '1':
+        group_dict = group_to_api1(group, context)
+    else:
+        group_dict = group_to_api2(group, context)
+
+    return group_dict
+


--- a/ckan/logic/action/get.py	Thu May 19 17:45:47 2011 +0100
+++ b/ckan/logic/action/get.py	Thu May 19 18:48:11 2011 +0100
@@ -5,7 +5,10 @@
 import ckan.authz
 
 from ckan.lib.dictization.model_dictize import group_to_api1, group_to_api2
-from ckan.lib.dictization.model_dictize import package_to_api1, package_to_api2
+from ckan.lib.dictization.model_dictize import (package_to_api1,
+                                                package_to_api2,
+                                                package_dictize,
+                                                group_dictize)
 
 
 def package_list(context):
@@ -39,7 +42,6 @@
     return revision_dicts
 
 def group_list(context):
-
     model = context["model"]
     user = context["user"]
     api = context.get('api_version') or '1'
@@ -49,6 +51,28 @@
     groups = query.all() 
     return [getattr(p, ref_group_by) for p in groups]
 
+def group_list_authz(context):
+    model = context['model']
+    user = context['user']
+    pkg = context.get('package')
+
+    query = ckan.authz.Authorizer().authorized_query(user, model.Group, model.Action.EDIT)
+    groups = set(query.all())
+    return set([group.id for group in groups])
+
+def group_list_availible(context):
+    model = context['model']
+    user = context['user']
+    pkg = context.get('package')
+
+    query = ckan.authz.Authorizer().authorized_query(user, model.Group, model.Action.EDIT)
+    groups = set(query.all())
+
+    if pkg:
+        groups = groups - set(pkg.groups)
+
+    return [(group.id, group.name) for group in groups]
+
 def licence_list(context):
     model = context["model"]
     license_register = model.Package.get_license_register()
@@ -105,15 +129,14 @@
     id = context['id']
 
     pkg = model.Package.get(id)
-    
+
+    context['package'] = pkg
+
     if pkg is None:
         raise NotFound
     check_access(pkg, model.Action.READ, context)
 
-    if api == '1':
-        package_dict = package_to_api1(pkg, context)
-    else:
-        package_dict = package_to_api2(pkg, context)
+    package_dict = package_dictize(pkg, context)
 
     for item in PluginImplementations(IPackageController):
         item.read(pkg)
@@ -139,20 +162,21 @@
     id = context['id']
     api = context.get('api_version') or '1'
 
+
     group = model.Group.get(id)
+    context['group'] = group
+
     if group is None:
         raise NotFound
+    check_access(group, model.Action.READ, context)
 
-    check_access(group, model.Action.READ, context)
+    group_dict = group_dictize(group, context)
 
     for item in PluginImplementations(IGroupController):
         item.read(group)
-    if api == '2':
-        _dict = group_to_api2(group, context)
-    else:
-        _dict = group_to_api1(group, context)
-    #TODO check it's not none
-    return _dict
+
+    return group_dict
+
 
 def tag_show(context):
     model = context['model']
@@ -167,3 +191,31 @@
     return package_list 
 
 
+def package_show_rest(context):
+
+    package_show(context)
+
+    api = context.get('api_version') or '1'
+
+    pkg = context['package']
+
+    if api == '1':
+        package_dict = package_to_api1(pkg, context)
+    else:
+        package_dict = package_to_api2(pkg, context)
+
+    return package_dict
+
+def group_show_rest(context):
+
+    group_show(context)
+    api = context.get('api_version') or '1'
+    group = context['group']
+
+    if api == '2':
+        group_dict = group_to_api2(group, context)
+    else:
+        group_dict = group_to_api1(group, context)
+
+    return group_dict
+


--- a/ckan/logic/action/update.py	Thu May 19 17:45:47 2011 +0100
+++ b/ckan/logic/action/update.py	Thu May 19 18:48:11 2011 +0100
@@ -1,4 +1,5 @@
 import logging
+import re
 
 import ckan.authz
 from ckan.plugins import PluginImplementations, IGroupController, IPackageController
@@ -14,14 +15,66 @@
 from ckan.lib.navl.dictization_functions import validate
 log = logging.getLogger(__name__)
 
+def prettify(field_name):
+    field_name = re.sub('(?<!\w)[Uu]rl(?!\w)', 'URL', field_name.replace('_', ' ').capitalize())
+    return _(field_name.replace('_', ' '))
 
+def package_error_summary(error_dict):
+
+    error_summary = {}
+    for key, error in error_dict.iteritems():
+        if key == 'resources':
+            error_summary[_('Resources')] = _('Package resource(s) incomplete')
+        elif key == 'extras':
+            error_summary[_('Extras')] = _('Missing Value')
+        elif key == 'extras_validation':
+            error_summary[_('Extras')] = error[0]
+        else:
+            error_summary[_(prettify(key))] = error[0]
+    return error_summary
+
+def group_error_summary(error_dict):
+
+    error_summary = {}
+    for key, error in error_dict.iteritems():
+        if key == 'extras':
+            error_summary[_('Extras')] = _('Missing Value')
+        elif key == 'extras_validation':
+            error_summary[_('Extras')] = error[0]
+        else:
+            error_summary[_(prettify(key))] = error[0]
+    return error_summary
+
+def check_group_auth(data_dict, context):
+    model = context['model']
+    pkg = context.get("package")
+
+    ## hack as api does not allow groups
+    if context.get("allow_partial_update"):
+        return
+    
+    group_dicts = data_dict.get("groups", [])
+    groups = set()
+    for group_dict in group_dicts:
+        grp = model.Group.get(group_dict['id'])
+        if grp is None:
+            raise NotFound(_('Group was not found.'))
+        groups.add(grp)
+
+    if pkg:
+        groups = groups - set(pkg.groups)
+
+    for group in groups:
+        check_access(group, model.Action.EDIT, context)
 
 def package_update(data_dict, context):
 
     model = context['model']
     user = context['user']
     id = context["id"]
+    preview = context.get('preview', False)
     schema = context.get('schema') or default_update_package_schema()
+    model.Session.remove()
 
     pkg = model.Package.get(id)
     context["package"] = pkg
@@ -30,22 +83,29 @@
         raise NotFound(_('Package was not found.'))
 
     check_access(pkg, model.Action.EDIT, context)
+    check_group_auth(data_dict, context)
 
-    data, errors = validate(data_dict,
-                            default_update_package_schema(),
-                            context)
+    data, errors = validate(data_dict, schema, context)
+
     if errors:
-        raise ValidationError(errors)
+        model.Session.rollback()
+        raise ValidationError(errors, package_error_summary(errors))
 
-    rev = model.repo.new_revision()
-    rev.author = user
-    rev.message = _(u'REST API: Update object %s') % pkg.name
+    if not preview:
+        rev = model.repo.new_revision()
+        rev.author = user
+        if 'message' in context:
+            rev.message = context['message']
+        else:
+            rev.message = _(u'REST API: Create object %s') % data.get("name")
 
     pkg = package_dict_save(data, context)
-    for item in PluginImplementations(IPackageController):
-        item.edit(pkg)
 
-    model.repo.commit()        
+    if not preview:
+        for item in PluginImplementations(IPackageController):
+            item.edit(pkg)
+        model.repo.commit()        
+
     return package_dictize(pkg, context)
 
 
@@ -97,15 +157,12 @@
     return _update_package_relationship(entity, comment, context)
 
 
-def group_update_rest(data_dict, context):
-
-    dictized = group_api_to_dict(data_dict, context)
-    return group_update(dictized, context)
 
 def group_update(data_dict, context):
 
     model = context['model']
     user = context['user']
+    schema = context.get('schema') or default_update_group_schema()
     id = context['id']
 
     group = model.Group.get(id)
@@ -115,17 +172,20 @@
 
     check_access(group, model.Action.EDIT, context)
 
-    data, errors = validate(data_dict,
-                            default_update_group_schema(),
-                            context)
+    data, errors = validate(data_dict, schema, context)
     if errors:
-        raise ValidationError(errors)
+        model.Session.rollback()
+        raise ValidationError(errors, group_error_summary(errors))
 
     rev = model.repo.new_revision()
     rev.author = user
     
+    if 'message' in context:
+        rev.message = context['message']
+    else:
+        rev.message = _(u'REST API: Create object %s') % data.get("name")
+
     group = group_dict_save(data, context)
-    rev.message = _(u'REST API: Update object %s') % group.name
 
     for item in PluginImplementations(IGroupController):
         item.edit(group)
@@ -144,6 +204,17 @@
     id = context["id"]
     pkg = model.Package.get(id)
     context["package"] = pkg
+    context["allow_partial_update"] = True
     dictized_package = package_api_to_dict(data_dict, context)
     return package_update(dictized_package, context)
 
+def group_update_rest(data_dict, context):
+
+    model = context['model']
+    id = context["id"]
+    group = model.Group.get(id)
+    context["group"] = group
+    context["allow_partial_update"] = True
+    dictized_package = group_api_to_dict(data_dict, context)
+    return group_update(dictized_package, context)
+


--- a/ckan/logic/schema.py	Thu May 19 17:45:47 2011 +0100
+++ b/ckan/logic/schema.py	Thu May 19 18:48:11 2011 +0100
@@ -3,20 +3,24 @@
                                       not_empty,
                                       empty,
                                       ignore,
-                                      both_not_empty,
                                       if_empty_same_as,
-                                      not_missing
+                                      not_missing,
+                                      ignore_empty
                                      )
 from ckan.logic.validators import (package_id_not_changed,
-                                             package_id_exists,
-                                             package_id_or_name_exists,
-                                             extras_unicode_convert,
-                                             name_validator,
-                                             package_name_validator,
-                                             group_name_validator,
-                                             tag_length_validator,
-                                             tag_name_validator,
-                                             tag_not_uppercase)
+                                   package_id_exists,
+                                   package_id_or_name_exists,
+                                   extras_unicode_convert,
+                                   name_validator,
+                                   package_name_validator,
+                                   group_name_validator,
+                                   tag_length_validator,
+                                   tag_name_validator,
+                                   tag_string_convert,
+                                   duplicate_extras_key,
+                                   ignore_not_admin,
+                                   no_http,
+                                   tag_not_uppercase)
 from formencode.validators import OneOf
 import ckan.model
 
@@ -24,7 +28,7 @@
 def default_resource_schema():
 
     schema = {
-        'id': [ignore_missing, unicode],
+        'id': [ignore_empty, unicode],
         'revistion_id': [ignore_missing, unicode],
         'resource_group_id': [ignore],
         'package_id': [ignore],
@@ -54,7 +58,7 @@
 
     schema = {
         'id': [ignore_missing, unicode, package_id_exists],
-        'revision_id': [ignore_missing, unicode],
+        'revision_id': [ignore],
         'name': [not_empty, unicode, name_validator, package_name_validator],
         'title': [if_empty_same_as("name"), unicode],
         'author': [ignore_missing, unicode],
@@ -65,7 +69,7 @@
         'notes': [ignore_missing, unicode],
         'url': [ignore_missing, unicode],#, URL(add_http=False)],
         'version': [ignore_missing, unicode],
-        'state': [ignore],
+        'state': [ignore_not_admin, ignore_missing],
         '__extras': [ignore],
         '__junk': [empty],
         'resources': default_resource_schema(),
@@ -74,10 +78,10 @@
         'relationships_as_object': default_relationship_schema(),
         'relationships_as_subject': default_relationship_schema(),
         'groups': {
+            'id': [ignore_missing, unicode],
             '__extras': [ignore],
         }
     }
-
     return schema
 
 def default_create_package_schema():
@@ -96,18 +100,41 @@
 
     return schema
 
+def package_form_schema():
+
+    schema = default_package_schema()
+    ##new
+    schema['log_message'] = [unicode, no_http]
+    schema['groups'] = {
+            'id': [not_empty, unicode],
+            '__extras': [empty],
+    }
+    schema['tag_string'] = [ignore_missing, tag_string_convert]
+    schema['extras_validation'] = [duplicate_extras_key, ignore]
+    schema['save'] = [ignore]
+    schema['preview'] = [ignore]
+    schema['return_to'] = [ignore]
+
+    ##changes
+    schema.pop("id")
+    schema.pop('tags')
+    schema.pop('relationships_as_object')
+    schema.pop('revision_id')
+    schema.pop('relationships_as_subject')
+    return schema
 
 def default_group_schema():
 
     schema = {
         'id': [ignore_missing, unicode],
-        'revision_id': [ignore_missing, unicode],
+        'revision_id': [ignore],
         'name': [not_empty, unicode, name_validator, group_name_validator],
         'title': [ignore_missing, unicode],
         'description': [ignore_missing, unicode],
         'state': [ignore],
         'created': [ignore],
         'extras': default_extras_schema(),
+        '__extras': [ignore],
         'packages': {
             "id": [not_empty, unicode, package_id_or_name_exists],
             "__extras": [ignore]
@@ -115,11 +142,22 @@
     }
     return schema
 
+def group_form_schema():
+    schema = default_group_schema()
+    #schema['extras_validation'] = [duplicate_extras_key, ignore]
+    schema['packages'] = {
+        "name": [not_empty, unicode],
+        "__extras": [ignore]
+    }
+    return schema
+
+
 def default_update_group_schema():
     schema = default_group_schema()
     schema["name"] = [ignore_missing, group_name_validator, unicode]
     return schema
 
+
 def default_extras_schema():
 
     schema = {
@@ -127,6 +165,7 @@
         'key': [not_empty, unicode],
         'value': [not_missing, unicode],
         'state': [ignore],
+        'deleted': [ignore_missing],
     }
     return schema
 


--- a/ckan/logic/validators.py	Thu May 19 17:45:47 2011 +0100
+++ b/ckan/logic/validators.py	Thu May 19 18:48:11 2011 +0100
@@ -1,6 +1,7 @@
 import re
 from pylons.i18n import _, ungettext, N_, gettext
-from ckan.lib.navl.dictization_functions import Invalid, missing
+from ckan.lib.navl.dictization_functions import Invalid, missing, unflatten
+from ckan.authz import Authorizer
 
 def package_id_not_changed(value, context):
 
@@ -10,6 +11,15 @@
                         'This key is read-only') % (package.id, value))
     return value
 
+def no_http(value, context):
+
+    model = context['model']
+    session = context['session']
+
+    if 'http:' in value:
+        raise Invalid(_('No links are allowed in the log_message.'))
+    return value
+
 def package_id_exists(value, context):
 
     model = context['model']
@@ -78,6 +88,20 @@
     if result:
         errors[key].append(_('Package name already exists in database'))
 
+def duplicate_extras_key(key, data, errors, context):
+
+    unflattened = unflatten(data)
+    extras = unflattened.get('extras', [])
+    extras_keys = []
+    for extra in extras:
+        if not extra.get('deleted'):
+            extras_keys.append(extra['key'])
+    
+    for extra_key in set(extras_keys):
+        extras_keys.remove(extra_key)
+    if extras_keys:
+        errors[key].append(_('Duplicate key "%s"') % extras_keys[0])
+    
 def group_name_validator(key, data, errors, context):
     model = context['model']
     session = context['session']
@@ -116,3 +140,31 @@
     if tagname_uppercase.search(value):
         raise Invalid(_('Tag "%s" must not be uppercase' % (value)))
     return value
+
+def tag_string_convert(key, data, errors, context):
+
+    value = data[key]
+
+    tags = value.split()
+    for num, tag in enumerate(tags):
+        data[('tags', num, 'name')] = tag.lower()
+
+    for tag in tags:
+        tag_length_validator(tag, context)
+        tag_name_validator(tag, context)
+
+def ignore_not_admin(key, data, errors, context):
+
+    model = context['model']
+    user = context.get('user')
+
+    if user and Authorizer.is_sysadmin(user):
+        return
+
+    pkg = context.get('package')
+    if (user and pkg and 
+        Authorizer().is_authorized(user, model.Action.CHANGE_STATE, pkg)):
+        return
+
+    data.pop(key)
+


--- a/ckan/public/scripts/autocompleter.js	Thu May 19 17:45:47 2011 +0100
+++ b/ckan/public/scripts/autocompleter.js	Thu May 19 18:48:11 2011 +0100
@@ -2,33 +2,27 @@
 
 (function () {
   
-  function extractDataAttributes() {
-    var el = $(this);
-    $.each(this.attributes, function () {
-      var m = this.name.match(/data\-(\S+)/);
-      if (m) { el.data(m[1], this.value); }
-    });
-  }
-  
   function processResult(e, item) {
-		console.log(item);
-    $(this).val('')
-           .parent('dd').before(
-      '<input type="hidden" name="PackageGroup--package_name" value="' + item[1] + '">' +
+    var input_box = $(this)
+    input_box.val('')
+    var parent_dd = input_box.parent('dd')
+    var old_name = input_box.attr('name')
+    var field_name_regex = /^(\S+)__(\d+)__(\S+)$/;
+    var split = old_name.match(field_name_regex)
+
+    var new_name = split[1] + '__' + (parseInt(split[2]) + 1) + '__' + split[3]
+
+    input_box.attr('name', new_name)
+    input_box.attr('id', new_name)
+
+    parent_dd.before(
+      '<input type="hidden" name="' + old_name + '" value="' + item[1] + '">' +
       '<dd>' + item[0] + '</dd>'
     );
   }
   
   $(document).ready(function () {
-    $('input.autocomplete').each(function () {
-      extractDataAttributes.apply(this);
-      
-      var url = $(this).data('autocomplete-url');
-      
-      if (url) {
-        $(this).autocomplete(url, {})
+    $('input.autocomplete').autocomplete('/package/autocomplete', {})
                .result(processResult);
-      }
-    });
   });
 })(jQuery);


--- a/ckan/public/scripts/flexitable.js	Thu May 19 17:45:47 2011 +0100
+++ b/ckan/public/scripts/flexitable.js	Thu May 19 18:48:11 2011 +0100
@@ -6,7 +6,7 @@
 
 (function ($) {
 
-  var fieldNameRegex = /^(\S+)-(\d+)-(\S+)$/;
+  var fieldNameRegex = /^(\S+)__(\d+)__(\S+)$/;
 
   var controlsHtml = '<td><div class="controls">' +
                        '<a class="moveUp"   title="Move this row up" href="#moveUp">Move up</a>' +
@@ -24,8 +24,8 @@
   function setRowNumber(tr, num) {
     $(tr).find('input').each(function () {
       $(this).attr({
-        id:   $(this).attr('id').replace(fieldNameRegex, "$1-" + num + "-$3"),
-        name: $(this).attr('name').replace(fieldNameRegex, "$1-" + num + "-$3")
+        id:   $(this).attr('id').replace(fieldNameRegex, "$1__" + num + "__$3"),
+        name: $(this).attr('name').replace(fieldNameRegex, "$1__" + num + "__$3")
       });
     });
   }


--- /dev/null	Thu Jan 01 00:00:00 1970 +0000
+++ b/ckan/templates/group/new_group_form.html	Thu May 19 18:48:11 2011 +0100
@@ -0,0 +1,90 @@
+<form id="group-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">
+
+<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>
+</ul>
+</div>
+
+<fieldset>
+  <legend>Details</legend>
+  <dl>
+    <dt><label class="field_opt" for="name">Name *</label></dt>
+    <dd><input id="name" name="name" type="text" value="${data.get('name', '')}"/></dd>
+    <dd class="instructions basic"><br/><strong>Unique identifier</strong> for group.<br/>2+ chars, lowercase, using only 'a-z0-9' and '-_'</dd>
+    <dd class="field_error" py:if="errors.get('name', '')">${errors.get('name', '')}</dd>
+
+    <dt><label class="field_opt" for="title">Title</label></dt>
+    <dd><input id="title" name="title" type="text" value="${data.get('title', '')}"/></dd>
+    <dd class="field_error" py:if="errors.get('title', '')">${errors.get('title', '')}</dd>
+
+    <dt><label class="field_opt" for="title">Description</label></dt>
+    <dd><textarea cols="60" id="description" name="description" rows="15">${data.get('description', '')}</textarea></dd>
+
+    <dt py:if="c.is_sysadmin or c.auth_for_change_state"><label class="field_opt" for="state">State</label></dt>
+    <dd 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>
+  <legend>Extras</legend>
+  <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>
+    </py:for>
+
+    <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>
+    </py:for>
+    </py:with>
+  </dl>
+</fieldset>
+
+<fieldset>
+  <legend>Packages</legend>
+  <dl py:if="data.get('packages')">
+    <py:for each="num, package in enumerate(data.get('packages'))">
+      <dt><input checked="checked" id="packages__${num}__name" name="packages__${num}__name" type="checkbox" value="${package['name']}"/></dt>
+      <dd>
+        <label for="packages__${num}__name">${package['name']}</label>
+      </dd>
+    </py:for>
+  </dl>
+  <p py:if="not data.get('packages')">There are no packages currently in this group.</p>
+</fieldset>
+
+<fieldset>
+  <legend>
+    Add packages
+  </legend>
+  <dl>
+    <dt><label class="field_opt" for="packages__${len(data.get('packages', []))}__name">Package</label></dt>
+    <dd><input class="autocomplete" id="packages__${len(data.get('packages', []))}__name" name="packages__${len(data.get('packages', []))}__name" type="text" /></dd>
+  </dl>
+</fieldset>
+
+  <br />
+  <input id="save" name="save" type="submit" value="Save" />
+</form>


--- a/ckan/templates/package/new.html	Thu May 19 17:45:47 2011 +0100
+++ b/ckan/templates/package/new.html	Thu May 19 18:48:11 2011 +0100
@@ -21,6 +21,8 @@
     <!-- Tagcomplete --><script type="text/javascript" src="${g.site_url}/scripts/tagcomplete.js"></script><link rel="stylesheet" href="${g.site_url}/css/tagcomplete.css" /> 
+
+    <xi:include href="new_package_form.js"/></py:def><div py:match="content">


--- /dev/null	Thu Jan 01 00:00:00 1970 +0000
+++ b/ckan/templates/package/new_package_form.html	Thu May 19 18:48:11 2011 +0100
@@ -0,0 +1,203 @@
+<form id="package-edit" class="package_create_form ckan" 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">
+
+
+<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>
+</ul>
+</div>
+
+<fieldset>
+  <legend> Basic information</legend>
+  <dl>
+    <dt><label class="field_opt" for="title">Title</label></dt>
+    <dd><input id="title" name="title" type="text" value="${data.get('title', '')}"/></dd>
+    <dd class="instructions basic">A short descriptive title for the data set.</dd>
+    <dd class="instructions further">It should not be a description though - save that for the Notes field. Do not give a trailing full stop.</dd>
+    <dd class="field_error" py:if="errors.get('title', '')">${errors.get('title', '')}</dd>
+
+    <dt><label class="field_req" for="name">Name *</label></dt>
+    <dd><input id="name" maxlength="100" name="name" type="text" value="${data.get('name', '')}" /></dd>
+    <dd class="instructions basic">A unique identifier for the package.</dd>
+    <dd class="instructions further">It should be broadly humanly readable, in the spirit of Semantic Web URIs. Only use an acronym if it is widely recognised. Renaming is possible but discouraged.</dd>
+    <dd class="hints">2+ characters, lowercase, using only 'a-z0-9' and '-_'</dd>
+    <dd class="field_error" py:if="errors.get('name', '')">${errors.get('name', '')}</dd>
+
+    <dt><label class="field_opt" for="url">URL</label></dt>
+    <dd><input id="url" name="url" type="text" value="${data.get('url', '')}"/></dd>
+    <dd class="instructions basic">The URL for the web page describing the data (not the data itself).</dd>
+    <dd class="hints">e.g. http://www.example.com/growth-figures.html</dd>
+    <dd class="field_error" py:if="errors.get('url', '')">${errors.get('url', '')}</dd>
+
+    <dt><label class="field_opt" for="notes">Notes</label></dt>
+    <dd><textarea cols="60" id="notes" name="notes" rows="15">${data.get('notes', '')}</textarea></dd>
+    <dd class="instructions basic">The main description of the dataset</dd>
+    <dd class="instructions further">It is often displayed with the package title. In particular, it should start with a short sentence that describes the data set succinctly, because the first few words alone may be used in some views of the data sets.</dd>
+    <dd class="hints">You can use <a href="http://daringfireball.net/projects/markdown/syntax">Markdown formatting</a> here.</dd>
+
+    <dt><label class="field_opt" for="license_id">Licence</label></dt>
+    <dd>
+      <select id="license_id" name="license_id">
+        <py:for each="licence_desc, licence_id in c.licences">
+          <option value="${licence_id}" py:attrs="{'selected': 'selected' if data.get('license_id', '') == licence_id else None}" >${licence_desc}</option>
+        </py:for>
+      </select>
+    </dd>
+    <dd class="instructions basic">The licence under which the dataset is released.</dd>
+
+    <dt><label class="field_opt" for="tags">Tags</label></dt>
+    <dd>
+      <input class="long tagComplete" data-tagcomplete-queryparam="incomplete" 
+               data-tagcomplete-url="/api/2/util/tag/autocomplete" id="tag_string" name="tag_string" size="60" type="text" 
+               value="${data.get('tag_string') or ' '.join([tag['name'] for tag in data.get('tags', [])])}" />
+    </dd>
+    <dd class="instructions basic">Terms that may link this dataset to similar ones. For more information on conventions, see <a href="http://wiki.okfn.org/ckan/doc/faq#TagConventions">this wiki page</a>.</dd>
+    <dd class="hints">e.g. pollution rivers water-quality</dd>
+    <dd class="field_error" py:if="errors.get('tag_string', '')">${errors.get('tag_string', '')}</dd>
+  </dl>
+</fieldset>
+
+<fieldset>
+  <legend>Resources</legend>
+  <table class="flexitable">
+    <thead>
+      <tr>
+        <th class="field_req resource-url">URL*</th>
+        <th class="field_opt resource-format">Format</th>
+        <th class="field_opt resource-description">Description</th>
+        <th class="field_opt resource-hash">Hash</th>
+      </tr>
+    </thead>
+    <tbody>
+      <py:for each="num, res in enumerate(data.get('resources', []) + [{}])">
+      <tr>
+        <py:for each="col in c.resource_columns">
+        <td class="resource-${col}">
+          <input name="resources__${num}__${col}" type="text" value="${res.get(col, '')}" class="${'medium-width' if col=='description' else 'short'}" />
+        </td>
+        </py:for>
+        <td class="resource-id"><input name="resources__${num}__id" type="hidden" value="${res.get('id', '')}" /></td>
+      </tr>
+      </py:for>
+    </tbody>
+  </table>
+
+  <div class="instructions basic">The files containing the data or address of the APIs for accessing it.</div>
+  <div class="instructions further"><br />These can be repeated as required. For example if the data is being supplied in multiple formats, or split into different areas or time periods, each file is a different 'resource' which should be described differently. They will all appear on the dataset page on CKAN together.<br /><br /><b>URL:</b> This is the Internet link directly to the data - by selecting this link in a web browser, the user will immediately download the full data set. Note that datasets are not hosted on this site, but by the publisher of the data. Alternatively the URL can point to an API server such as a SPARQL endpoint or JSON-P service.<br /><b>Format:</b> This should give the file format in which the data is supplied. <br /><b>Description</b> Any information you want to add to describe the resource.<br /></div>
+  <div class="hints">Format choices: CSV | RDF | XML | XBRL | SDMX | HTML+RDFa | Other as appropriate</div>
+  <div class="field_error" py:if="errors.get('resources', '')">Package resource(s) incomplete.</div>
+</fieldset>
+
+<fieldset>
+  <legend>Groups</legend>
+  <dl>
+    <py:for each="num, group in enumerate(data.get('groups', []))">
+      <dt>
+      <input type="${'checkbox' if group['id'] in c.groups_authz else 'hidden'}" name="groups__${num}__id" checked="checked" value="${group['id']}" />
+      </dt>     
+      <dd><label for="groups__${num}__checked">${group['name']}</label></dd>
+    </py:for>
+
+    <dt>Group</dt>
+    <dd py:if="c.groups"> 
+      <select id="groups__${len(data.get('groups', []))}__id" name="groups__${len(data.get('groups', []))}__id">
+        <option selected="selected" value="">(None)</option>
+        <py:for each="group_id, group_name in c.groups">
+          <option value="${group_id}" py:if="group_id in c.groups_authz">${group_name}</option>
+        </py:for>
+      </select>
+    </dd> 
+    <dd py:if="not c.groups">Cannot add any groups.</dd>
+  </dl>
+</fieldset>
+<fieldset>
+  <legend>Detail</legend>
+  <dl>
+    <dt><label class="field_opt" for="author">Author</label></dt>
+    <dd><input id="author" name="author" type="text" value="${data.get('author', '')}" /></dd>
+    <dd class="instructions basic">The name of the main contact, for enquiries about this particular dataset, using the e-mail address in the following field.</dd>
+
+    <dt><label class="field_opt" for="author_email">Author email</label></dt>
+    <dd><input id="author_email" name="author_email" type="text" value="${data.get('author_email', '')}" /></dd>
+
+    <dt><label class="field_opt" for="maintainer">Maintainer</label></dt>
+    <dd><input id="maintainer" name="maintainer" type="text" value="${data.get('maintainer', '')}" /></dd>
+    <dd class="instructions basic">If there is another important contact person (in addition to the person in the Author field) then provide details here.</dd>
+
+    <dt><label class="field_opt" for="maintainer_email">Maintainer email</label></dt>
+    <dd><input id="maintainer_email" name="maintainer_email" type="text" value="${data.get('maintainer_email', '')}" /></dd>
+
+    <dt><label class="field_opt" for="version">Version</label></dt>
+    <dd><input id="version" maxlength="100" name="version" type="text" value="${data.get('version', '')}" /></dd>
+    <dd class="instructions basic">A number representing the version (if applicable)</dd>
+    <dd class="hints">e.g. 1.2.0</dd>
+
+    <dt py:if="c.is_sysadmin or c.auth_for_change_state"><label class="field_opt" for="state">State</label></dt>
+    <dd 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>
+  <legend>Extras</legend>
+  <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>
+    </py:for>
+
+    <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>
+    </py:for>
+    </py:with>
+  </dl>
+</fieldset>
+<hr />
+
+<label for="log_message">Edit summary (briefly describe the changes you have made)</label>
+<textarea id="log_message" name="log_message" class="short wide">${data.get('log_message', '')}</textarea>
+
+<div class="ckan-logged-in" style="display: none;">
+  <p>Author: ${c.author}</p>
+</div>
+<div class="ckan-logged-out">
+  <label>Author: ${c.author}</label>
+  <p class="hints">
+  Since you have not signed in this will just be your IP address.
+  <a href="${h.url_for(controller='user', action='login', id=None)}" target="_blank">Click here to sign in</a> before saving (opens in new window).
+  </p>
+</div>
+
+<div class="submit">
+  <input name="preview" type="submit" value="Preview" />
+  <input id="save" name="save" type="submit" value="Save" />
+</div>
+
+<p class="hints">
+<strong>Important:</strong> By submitting content, you agree to release your contributions
+  under the open license specified on the <a href="/license">license page</a>. Please <strong>refrain</strong> from editing this page if you are <strong>not</strong> happy to do this.
+</p>
+
+
+</form>


--- /dev/null	Thu Jan 01 00:00:00 1970 +0000
+++ b/ckan/templates/package/new_package_form.js	Thu May 19 18:48:11 2011 +0100
@@ -0,0 +1,110 @@
+
+<script type="text/javascript">
+//<![CDATA[
+(function($){
+    $.fn.ajaxCreateSlug = function(name, url) {
+        var title = this;
+        var updater = {
+            init: function(title, name) {
+                // Add a new element where the validity of the package name can be displayed
+                this.name_field = name;
+                this.title_field = title;
+                this.name_field.parent().append('<div id="package_name_valid_msg"></div>');
+                this.title_field.blur(this.title_change_handler())
+                this.title_field.keyup(this.title_change_handler())
+                this.name_field.keyup(this.name_change_handler());
+                this.name_field.blur(this.name_blur_handler());
+                this.url = url;
+            },
+            title_change_handler: function() {
+                var self = this;
+                return function() {
+                    if (!self.name_changed && self.title_field.val().replace(/^\s+|\s+$/g, '')) {
+                        self.update(self.title_field.val(), function(data) {self.name_field.val(data.name)});
+                    }
+                }
+            },
+            name_blur_handler: function() {
+                var self = this;
+                return function() {
+                    // Reset if the name is emptied
+                    if (!self.name_field.val().replace(/^\s+|\s+$/g, '')){
+                        self.name_changed = false;
+                        $('#package_name_valid_msg').html('');
+                    } else {
+                        self.update(self.name_field.val(), function(data) {
+                            self.name_field.val(data.name)
+                        });
+                    }
+                };
+            },
+            name_change_handler: function() {
+                var self = this;
+                return function() {
+                    // Reset if the name is emptied
+                    if (!self.name_field.val().replace(/^\s+|\s+$/g, '')){
+                        self.name_changed = false;
+                        $('#package_name_valid_msg').html('');
+                    } else {
+                        self.name_changed = true;
+                        self.update(self.name_field.val(), function(data) {
+                            if (self.name_field.val().length >= data.name) {
+                                self.name_field.val(data.name);
+                            }
+                        });
+                    }
+                };
+            },
+            // Keep a variable where we can store whether the name field has been
+            // directly modified by the user or not. If it has, we should no longer
+            // fetch updates.
+            name_changed: false,
+            // Create a function for fetching the value and updating the result
+            perform_update: function(value, on_success){
+                var self = this;
+                $.ajax({
+                    url: self.url,
+                    data: 'title=' + value,
+                    dataType: 'jsonp',
+                    type: 'get',
+                    jsonpCallback: 'callback',
+                    success: function (data) {
+                        if (on_success) {
+                            on_success(data);
+                        }
+                        var valid_msg = $('#package_name_valid_msg');
+                        if (data.valid) {
+                            valid_msg.html('<span style="font-weight: bold; color: #0c0">This package name is available!</span>');
+                        } else {
+                            valid_msg.html('<span style="font-weight: bold; color: #c00">This package name is already used, please use a different name</span>');
+                        }
+                    }
+                });
+            },
+            // We only want to perform the update if there hasn't been a change for say 200ms
+            timer: null,
+            update: function(value, on_success) {
+                var self = this;
+                if (this.timer) {
+                    clearTimeout(this.timer)
+                };
+                this.timer = setTimeout(function () {
+                    self.perform_update(value, on_success)
+                }, 200);
+            }
+        }
+        updater.init(title, $(name), url);
+        return title;
+    };
+})( jQuery );
+$(document).ready(function() {
+    $('#title').ajaxCreateSlug('#name', '/api/2/util/package/create_slug');
+});
+
+$(document).ready(function () {
+    if (!$('#preview').length) {
+        $("#title").focus();
+    }
+});
+//]]>
+</script>


--- a/ckan/tests/functional/test_authz.py	Thu May 19 17:45:47 2011 +0100
+++ b/ckan/tests/functional/test_authz.py	Thu May 19 18:48:11 2011 +0100
@@ -379,7 +379,7 @@
         res = self.app.get(offset, extra_environ={'REMOTE_USER': user.name.encode('utf8')})
         assert 'New - Data Packages' in res
         fv = res.forms['package-edit']
-        prefix = 'Package--'
+        prefix = ''
         fv[prefix + 'name'] = u'annakarenina'
         res = fv.submit('save', extra_environ={'REMOTE_USER': user.name.encode('utf8')})
 


--- a/ckan/tests/functional/test_group.py	Thu May 19 17:45:47 2011 +0100
+++ b/ckan/tests/functional/test_group.py	Thu May 19 18:48:11 2011 +0100
@@ -142,13 +142,17 @@
         
     def test_2_edit(self):
         offset = url_for(controller='group', action='edit', id=self.groupname)
+        print offset
         res = self.app.get(offset, status=200, extra_environ={'REMOTE_USER': 'russianfan'})
         assert 'Edit Group: %s' % self.groupname in res, res
 
+        print res
+        
+
         form = res.forms['group-edit']
         group = model.Group.by_name(self.groupname)
-        titlefn = 'Group-%s-title' % group.id
-        descfn = 'Group-%s-description' % group.id
+        titlefn = 'title'
+        descfn = 'description'
         newtitle = 'xxxxxxx'
         newdesc = '''### Lots of stuff here
 
@@ -158,7 +162,7 @@
         form[titlefn] = newtitle
         form[descfn] = newdesc
         pkg = model.Package.by_name(self.packagename)
-        form['PackageGroup--package_name'] = pkg.name
+        form['packages__2__name'] = pkg.name
 
         
         res = form.submit('save', status=302, extra_environ={'REMOTE_USER': 'russianfan'})
@@ -201,7 +205,7 @@
         res = self.app.get(offset, status=200, extra_environ={'REMOTE_USER': 'russianfan'})
         form = res.forms['group-edit']
         group = model.Group.by_name(self.groupname)
-        form['Group-%s-title' % group.id] = "huhuhu"
+        form['title'] = "huhuhu"
         res = form.submit('save', status=302, extra_environ={'REMOTE_USER': 'russianfan'})
         assert plugin.calls['edit'] == 1, plugin.calls
         plugins.unload(plugin)
@@ -233,7 +237,7 @@
         assert res.request.url.startswith('/user/login')
 
     def test_2_new(self):
-        prefix = 'Group--'
+        prefix = ''
         group_name = u'testgroup'
         group_title = u'Test Title'
         group_description = u'A Description'
@@ -246,14 +250,14 @@
         assert fv[prefix+'name'].value == '', fv.fields
         assert fv[prefix+'title'].value == ''
         assert fv[prefix+'description'].value == ''
-        assert fv['PackageGroup--package_name'].value == '', fv['PackageGroup--package_name'].value
+        assert fv['packages__0__name'].value == '', fv['PackageGroup--package_name'].value
 
         # Edit form
         fv[prefix+'name'] = group_name
         fv[prefix+'title'] = group_title
         fv[prefix+'description'] = group_description
         pkg = model.Package.by_name(self.packagename)
-        fv['PackageGroup--package_name'] = pkg.name
+        fv['packages__0__name'] = pkg.name
         res = fv.submit('save', status=302, extra_environ={'REMOTE_USER': 'russianfan'})
         res = res.follow()
         assert '%s' % group_title in res, res
@@ -267,7 +271,7 @@
         assert group.packages == [pkg]
 
     def test_3_new_duplicate(self):
-        prefix = 'Group--'
+        prefix = ''
 
         # Create group
         group_name = u'testgrp1'
@@ -302,8 +306,8 @@
         offset = url_for(controller='group', action='new')
         res = self.app.get(offset, status=200, extra_environ={'REMOTE_USER': 'russianfan'})
         form = res.forms['group-edit']
-        form['Group--name'] = "hahaha"
-        form['Group--title'] = "huhuhu"
+        form['name'] = "hahaha"
+        form['title'] = "huhuhu"
         res = form.submit('save', status=302, extra_environ={'REMOTE_USER': 'russianfan'})
         assert plugin.calls['create'] == 1, plugin.calls
         plugins.unload(plugin)


--- a/ckan/tests/functional/test_package.py	Thu May 19 17:45:47 2011 +0100
+++ b/ckan/tests/functional/test_package.py	Thu May 19 18:48:11 2011 +0100
@@ -89,23 +89,19 @@
         if params.has_key('state'):
             assert 'State: %s' % params['state'] in main_div.replace('</strong>', ''), main_div_str
         if isinstance(params['extras'], dict):
-            extras = params['extras'].items()
+            extras = []
+            for key, value in params['extras'].items():
+                extras.append((key, value, False))
         elif isinstance(params['extras'], (list, tuple)):
             extras = params['extras']
         else:
             raise NotImplementedError
-        for key, value in extras:
-            key_in_html_body = self.escape_for_html_body(key)
-            value_in_html_body = self.escape_for_html_body(value)
-            self.check_named_element(main_div, 'tr', key_in_html_body, value_in_html_body)
-        if params.has_key('deleted_extras'):
-            if isinstance(params['deleted_extras'], dict):
-                deleted_extras = params['deleted_extras'].items()
-            elif isinstance(params['deleted_extras'], (list, tuple)):
-                deleted_extras = params['deleted_extras']
+        for key, value, deleted in extras:
+            if not deleted:
+                key_in_html_body = self.escape_for_html_body(key)
+                value_in_html_body = self.escape_for_html_body(value)
+                self.check_named_element(main_div, 'tr', key_in_html_body, value_in_html_body)
             else:
-                raise NotImplementedError
-            for key, value in params['deleted_extras']:
                 self.check_named_element(main_div, 'tr', '!' + key)
                 self.check_named_element(main_div, 'tr', '!' + value)
 
@@ -130,12 +126,12 @@
             assert str(params['state']) in preview, preview
         else:
             assert 'state' not in preview
-        for key, value in params['extras']:
-            key_html = self.escape_for_html_body(key)
-            value_html = self.escape_for_html_body(value)
-            self.check_named_element(preview, 'tr', key_html, value_html)
-        if params.has_key('deleted_extras'):
-            for key, value in params['deleted_extras']:
+        for key, value, deleted in params['extras']:
+            if not deleted:
+                key_html = self.escape_for_html_body(key)
+                value_html = self.escape_for_html_body(value)
+                self.check_named_element(preview, 'tr', key_html, value_html)
+            else:
                 key_html = self.escape_for_html_body(key)
                 value_html = self.escape_for_html_body(value)
                 self.check_named_element(preview, 'tr', '!' + key_html)
@@ -174,14 +170,14 @@
                 if key == 'license':
                     key = 'license_id'
                 params[key] = value
-        prefix = 'Package-%s-' % params['id']
+        prefix = ''
         main_res = self.main_div(res)
         self.check_tag(main_res, prefix+'name', params['name'])
         self.check_tag(main_res, prefix+'title', params['title'])
         self.check_tag(main_res, prefix+'version', params['version'])
         self.check_tag(main_res, prefix+'url', params['url'])
         for res_index, res_field, expected_value in self._get_resource_values(params['resources']):
-            self.check_tag(main_res, '%sresources-%i-%s' % (prefix, res_index, res_field), expected_value)
+            self.check_tag(main_res, '%sresources__%i__%s' % (prefix, res_index, res_field), expected_value)
         self.check_tag_and_data(main_res, prefix+'notes', params['notes'])
         self.check_tag_and_data(main_res, 'selected', params['license_id'])
         if isinstance(params['tags'], (str, unicode)):
@@ -189,20 +185,25 @@
         else:
             tags = params['tags']
         for tag in tags:
-            self.check_tag(main_res, prefix+'tags', tag)
+            self.check_tag(main_res, prefix+'tag_string', tag)
         if params.has_key('state'):
             self.check_tag_and_data(main_res, 'selected', str(params['state']))
         if isinstance(params['extras'], dict):
-            extras = params['extras'].items()
+            extras = []
+            for key, value in params['extras'].items():
+                extras.append((key, value, False))
         else:
             extras = params['extras']
-        for key, value in extras:
+        for num, (key, value, deleted) in enumerate(sorted(extras)):
             key_in_html_body = self.escape_for_html_body(key)
             value_in_html_body = self.escape_for_html_body(value)
             key_escaped = genshi_escape(key)
             value_escaped = genshi_escape(value)
-            self.check_tag_and_data(main_res, 'Package-%s-extras-%s' % (params['id'], key_escaped), key_in_html_body.capitalize())
-            self.check_tag(main_res, 'Package-%s-extras-%s' % (params['id'], key_escaped), value_escaped)
+            self.check_tag(main_res, 'extras__%s__key' % num, key_in_html_body)
+            self.check_tag(main_res, 'extras__%s__value' % num, value_escaped)
+            if deleted:
+                self.check_tag(main_res, 'extras__%s__deleted' % num, 'checked')
+
         assert params['log_message'] in main_res, main_res
     
     def _check_redirect(self, return_url_param, expected_redirect,
@@ -233,7 +234,7 @@
             res = self.app.get(offset)
             assert 'Packages -' in res
             fv = res.forms['package-edit']
-            prefix = 'Package-%s-' % pkg_id
+            prefix = ''
             fv[prefix + 'name'] = new_name
             res = fv.submit('preview')
             assert not 'Error' in res, res
@@ -474,7 +475,7 @@
             new_name = u'new-name'
             new_title = u'New Title'
             fv = self.res.forms['package-edit']
-            prefix = 'Package-%s-' % self.pkgid
+            prefix = ''
             fv[prefix + 'name'] = new_name
             fv[prefix + 'title'] = new_title
             res = fv.submit('save')
@@ -503,11 +504,11 @@
             newlicense_id = u'cc-by'
             newversion = u'0.9b'
             fv = self.res.forms['package-edit']
-            prefix = 'Package-%s-' % self.pkgid
+            prefix = ''
             fv[prefix + 'name'] = new_name
             fv[prefix + 'title'] =  new_title
             fv[prefix + 'url'] =  newurl
-            fv[prefix + 'resources-0-url'] =  new_download_url
+            fv[prefix + 'resources__0__url'] =  new_download_url
             fv[prefix + 'license_id'] =  newlicense_id
             fv[prefix + 'version'] = newversion
             res = fv.submit('save')
@@ -537,7 +538,7 @@
             new_name = u'new-name'
             new_title = u'A Short Description of this Package'
             fv = self.res.forms['package-edit']
-            prefix = 'Package-%s-' % self.pkgid
+            prefix = ''
             fv[prefix + 'name'] = new_name
             fv[prefix + 'title'] =  new_title
             res = fv.submit('save')
@@ -563,8 +564,8 @@
         newtags = newtagnames
         tagvalues = ' '.join(newtags)
         fv = self.res.forms['package-edit']
-        prefix = 'Package-%s-' % self.pkgid
-        fv[prefix + 'tags'] =  tagvalues
+        prefix = ''
+        fv[prefix + 'tag_string'] = tagvalues
         exp_log_message = 'test_edit_2: making some changes'
         fv['log_message'] =  exp_log_message
         res = fv.submit('save')
@@ -593,7 +594,7 @@
 
 '''
         fv = self.res.forms['package-edit']
-        prefix = 'Package-%s-' % self.pkgid
+        prefix = ''
         fv[prefix + 'url'] =  newurl
         fv[prefix + 'notes'] =  newnotes
         res = fv.submit('preview')
@@ -611,10 +612,9 @@
         res = fv.submit('save', status=400)
 
         fv = self.res.forms['package-edit']
-        prefix = 'Package-%s-' % self.pkgid
+        prefix = ''
         del fv.fields[prefix + 'license_id']
-        res = fv.submit('save', status=400)     
-
+        res = fv.submit('save', status=400)
 
     def test_redirect_after_edit_using_param(self):
         return_url = 'http://random.site.com/package/<NAME>?test=param'
@@ -686,42 +686,42 @@
             state = model.State.ACTIVE
             tags = (u'tag1', u'tag2', u'tag3')
             tags_txt = u' '.join(tags)
-            extra_changed = 'key1', self.value1 + ' CHANGED'
-            extra_new = 'newkey', 'newvalue'
+            extra_changed = 'key1', self.value1 + ' CHANGED', False
+            extra_new = 'newkey', 'newvalue', False
             log_message = 'This is a comment'
             assert not model.Package.by_name(name)
             fv = res.forms['package-edit']
-            prefix = 'Package-%s-' % pkg.id
+            prefix = ''
             fv[prefix+'name'] = name
             fv[prefix+'title'] = title
             fv[prefix+'version'] = version
             fv[prefix+'url'] = url
             for res_index, resource in enumerate(resources):
                 for field_index, res_field in enumerate(model.Resource.get_columns()):
-                    fv[prefix+'resources-%s-%s' % (res_index, res_field)] = resource[field_index]
+                    fv[prefix+'resources__%s__%s' % (res_index, res_field)] = resource[field_index]
             fv[prefix+'notes'] = notes
             fv[prefix+'license_id'] = license_id
-            fv[prefix+'tags'] = tags_txt
+            fv[prefix+'tag_string'] = tags_txt
             fv[prefix+'state'] = state
-            fv[prefix+'extras-%s' % extra_changed[0]] = extra_changed[1].encode('utf8')
-            fv[prefix+'extras-newfield0-key'] = extra_new[0].encode('utf8')
-            fv[prefix+'extras-newfield0-value'] = extra_new[1].encode('utf8')
-            fv[prefix+'extras-key3-checkbox'] = True
+            fv[prefix+'extras__0__value'] = extra_changed[1].encode('utf8')
+            fv[prefix+'extras__3__key'] = extra_new[0].encode('utf8')
+            fv[prefix+'extras__3__value'] = extra_new[1].encode('utf8')
+            fv[prefix+'extras__2__deleted'] = True
             fv['log_message'] = log_message
             res = fv.submit('preview', extra_environ={'REMOTE_USER':'testadmin'})
             assert not 'Error' in res, res
 
             # Check preview is correct
-            current_extras = (('key2', extras['key2']),
-                              extra_changed,
-                              extra_new)
-            deleted_extras = [('key3', extras['key3'])]
+            extras = (('key2', extras['key2'], False),
+                       extra_changed,
+                       extra_new,
+                       ('key3', extras['key3'], True))
+
             self._check_preview(res, name=name, title=title, version=version,
                                 url=url,
                                 download_url='',
                                 resources=resources, notes=notes, license_id=license_id,
-                                tags=tags, extras=current_extras,
-                                deleted_extras=deleted_extras,
+                                tags=tags, extras=extras,
                                 state=state)
 
             # Check form is correctly filled
@@ -729,8 +729,7 @@
                                              title=title, version=version,
                                              url=url, resources=resources,
                                              notes=notes, license_id=license_id,
-                                             tags=tags, extras=current_extras,
-                                             deleted_extras=deleted_extras,
+                                             tags=tags, extras=extras,
                                              log_message=log_message,
                                              state=state)
 
@@ -746,8 +745,7 @@
                                      resources=resources, notes=notes,
                                      license_id=license_id, 
                                      tags=tags,
-                                     extras=current_extras,
-                                     deleted_extras=deleted_extras,
+                                     extras=extras,
                                      state=state,
                                      )
 
@@ -768,9 +766,10 @@
             expected_tagnames.sort()
             assert saved_tagnames == expected_tagnames
             assert pkg.state == state
-            assert len(pkg.extras) == len(current_extras)
-            for key, value in current_extras:
-                assert pkg.extras[key] == value
+            assert len(pkg.extras) == len([extra for extra in extras if not extra[-1]])
+            for key, value, deleted in extras:
+                if not deleted:
+                    assert pkg.extras[key] == value
 
             # for some reason environ['REMOTE_ADDR'] is undefined
             rev = model.Revision.youngest(model.Session)
@@ -785,7 +784,7 @@
 
     def test_edit_bad_log_message(self):
         fv = self.res.forms['package-edit']
-        prefix = 'Package-%s-' % self.pkgid
+        prefix = ''
         fv['log_message'] = u'Free enlargements: http://drugs.com/' # spam
         res = fv.submit('preview')
         assert 'Error' in res, res
@@ -799,7 +798,7 @@
 
     def test_edit_bad_name(self):
         fv = self.res.forms['package-edit']
-        prefix = 'Package-%s-' % self.pkgid
+        prefix = ''
         fv[prefix + 'name'] = u'a' # invalid name
         res = fv.submit('preview')
         assert 'Error' in res, res
@@ -823,7 +822,7 @@
             new_name = u'new-name'
             new_title = u'New Title'
             fv = res.forms['package-edit']
-            prefix = 'Package-%s-' % self.pkgid
+            prefix = ''
             fv[prefix + 'name'] = new_name
             fv[prefix + 'title'] = new_title
             res = fv.submit('save')
@@ -840,16 +839,16 @@
             assert len(pkg.groups) == 0
             offset = url_for(controller='package', action='edit', id=pkg.name)
             res = self.app.get(offset)
-            prefix = 'Package-%s-' % pkg.id
+            prefix = ''
             fv = res.forms['package-edit']
-            name = prefix + 'groups-new'
+            name = prefix + 'groups__0__id'
             # XXX the following assertion fails since upgrade to
             # sqlalchemy 0.6.5; apparently outer joins are handled
             # differently in such a way that
             # ckan.lib.base._get_user_editable_groups (which calls 
             # ckan.authz.authorized_query) now returns groups when it
             # shouldn't.                             
-            assert not name in fv.fields.keys()
+            assert not name in fv.fields.keys(), fv.fields.keys()
             res = fv.submit('save')
             res = res.follow()
             pkg = model.Package.by_name(u'editpkgtest')
@@ -865,11 +864,11 @@
             offset = url_for(controller='package', action='edit', id=pkg.name)
             
             res = self.app.get(offset, extra_environ={'REMOTE_USER':'russianfan'})
-            prefix = 'Package-%s-' % pkg.id
-            field_name = prefix + "groups-%s" % grp.id
-            assert not field_name in res
+            prefix = ''
+            field_name = prefix + "groups__0__id"
+            assert field_name in res
             fv = res.forms['package-edit']
-            fv[prefix + 'groups-new'] = grp.id
+            fv[prefix + 'groups__0__id'] = grp.id
             res = fv.submit('save', extra_environ={'REMOTE_USER':'russianfan'})
             res = res.follow()
             pkg = model.Package.by_name(u'editpkgtest')
@@ -890,8 +889,8 @@
             assert len(pkg.groups) == 1
             offset = url_for(controller='package', action='edit', id=pkg.name)
             res = self.app.get(offset, extra_environ={'REMOTE_USER':'russianfan'})
-            prefix = 'Package-%s-' % pkg.id
-            field_name = prefix + "groups-%s" % grp.id
+            prefix = ''
+            field_name = prefix + "groups__0__id"
             fv = res.forms['package-edit']
             fv[field_name] = False
             res = fv.submit('save', extra_environ={'REMOTE_USER':'russianfan'})
@@ -917,19 +916,19 @@
                 url='http://xxx.org')
         res = self.app.get(offset)
         form = res.forms['package-edit']
-        form['Package--url'].value == 'http://xxx.org/'
-        form['Package--name'].value == 'xxx.org'
+        form['url'].value == 'http://xxx.org/'
+        form['name'].value == 'xxx.org'
 
     def test_new_with_params_2(self):
         offset = url_for(controller='package', action='new',
                 url='http://www.xxx.org')
         res = self.app.get(offset)
         form = res.forms['package-edit']
-        form['Package--name'].value == 'xxx.org'
+        form['name'].value == 'xxx.org'
 
     def test_new_without_resource(self):
         # new package
-        prefix = 'Package--'
+        prefix = ''
         name = u'test_no_res'
         offset = url_for(controller='package', action='new')
         res = self.app.get(offset)
@@ -965,18 +964,18 @@
         res = self.app.get(offset)
         assert 'New - Data Packages' in res
         fv = res.forms['package-edit']
-        prefix = 'Package--'
+        prefix = ''
         fv[prefix + 'name'] = 'annakarenina'
         self.pkg_names.append('annakarenina')
         res = fv.submit('save')
         assert not 'Error' in res, res
 
     def test_new_bad_name(self):
-        offset = url_for(controller='package', action='new')
+        offset = url_for(controller='package', action='new', id=None)
         res = self.app.get(offset)
         assert 'New - Data Packages' in res
         fv = res.forms['package-edit']
-        prefix = 'Package--'
+        prefix = ''
         fv[prefix + 'name'] = u'a' # invalid name
         res = fv.submit('preview')
         assert 'Error' in res, res
@@ -1020,19 +1019,19 @@
         res = self.app.get(offset)
         assert 'New - Data Packages' in res
         fv = res.forms['package-edit']
-        prefix = 'Package--'
+        prefix = ''
         fv[prefix+'name'] = name
         fv[prefix+'title'] = title
         fv[prefix+'version'] = version
         fv[prefix+'url'] = url
-        fv[prefix+'resources-0-url'] = download_url
-        fv[prefix+'resources-0-description'] = u'description escape: & umlaut: \xfc quote "'.encode('utf8')
+        fv[prefix+'resources__0__url'] = download_url
+        fv[prefix+'resources__0__description'] = u'description escape: & umlaut: \xfc quote "'.encode('utf8')
         fv[prefix+'notes'] = notes
         fv[prefix+'license_id'] = license_id
-        fv[prefix+'tags'] = tags_txt
-        for i, extra in enumerate(extras.items()):
-            fv[prefix+'extras-newfield%s-key' % i] = extra[0].encode('utf8')
-            fv[prefix+'extras-newfield%s-value' % i] = extra[1].encode('utf8')
+        fv[prefix+'tag_string'] = tags_txt
+        for i, extra in enumerate(sorted(extras.items())):
+            fv[prefix+'extras__%s__key' % i] = extra[0].encode('utf8')
+            fv[prefix+'extras__%s__value' % i] = extra[1].encode('utf8')
         fv['log_message'] = log_message
         res = fv.submit('preview')
         assert not 'Error' in res, res
@@ -1040,11 +1039,14 @@
         # Check preview is correct
         resources = [[download_url, u'', u'description escape: & umlaut: \xfc quote "', u'']]
         resources_escaped = [[download_url, u'', u'description escape: & umlaut: \xfc quote "', u'']]
+
+        extras_list = [(key, value, False) for key, value in sorted(extras.items())]
+
         self._check_preview(res, name=name, title=title, version=version,
                             url=url,
                             resources=resources_escaped, notes=notes,
                             license_id=license_id,
-                            tags=tags, extras=extras.items(),
+                            tags=tags, extras=extras_list,
                             )
 
         # Check form is correctly filled
@@ -1052,11 +1054,9 @@
                                          title=title, version=version,
                                          url=url, resources=[download_url],
                                          notes=notes, license_id=license_id,
-                                         tags=[tag.lower() for tag in tags],
+                                         tags=[tag for tag in tags],
                                          extras=extras,
-#                                         deleted_extras=deleted_extras,
                                          log_message=log_message,
-#                                         state=state
                                          )
         # Submit
         fv = res.forms['package-edit']
@@ -1108,11 +1108,11 @@
         pkgname = u'testpkg'
         pkgtitle = u'mytesttitle'
         assert not model.Package.by_name(pkgname)
-        offset = url_for(controller='package', action='new')
+        offset = url_for(controller='package', action='new', id=None)
         res = self.app.get(offset)
         assert 'New - Data Packages' in res
         fv = res.forms['package-edit']
-        prefix = 'Package--'
+        prefix = ''
         fv[prefix + 'name'] = pkgname
         self.pkg_names.append(pkgname)
         res = fv.submit('save')
@@ -1138,7 +1138,7 @@
         offset = url_for(controller='package', action='new')
         res = self.app.get(offset)
         assert 'New - Data Packages' in res, res
-        prefix = 'Package--'
+        prefix = ''
         fv = res.forms['package-edit']
         fv[prefix + 'name'] = 'anything'
         del fv.fields['log_message']
@@ -1150,12 +1150,12 @@
         assert 'New - Data Packages' in res
         fv = res.forms['package-edit']
         fv[prefix + 'name'] = 'anything'
-        prefix = 'Package--'
+        prefix = ''
         del fv.fields[prefix + 'notes']
         # NOTE Missing dropdowns fields don't cause KeyError in
         # _serialized_value so don't register as an error here like
         # text field tested here.
-        res = fv.submit('save', status=400)     
+        res = fv.submit('save', status=400)
 
     def test_multi_resource_bug(self):
         # ticket:276
@@ -1163,10 +1163,10 @@
         res = self.app.get(offset)
         assert 'New - Data Packages' in res
         fv = res.forms['package-edit']
-        prefix = 'Package--'
+        prefix = ''
         fv[prefix + 'name'] = 'name276'
         resformat = u'xls'    
-        fv[prefix + 'resources-0-format'] = resformat
+        fv[prefix + 'resources__0__format'] = resformat
         res = fv.submit('preview')
 
         res = self.main_div(res)
@@ -1180,7 +1180,7 @@
         res = self.app.get(offset)
         new_name = u'plugged'
         fv = res.forms['package-edit']
-        prefix = 'Package--'
+        prefix = ''
         fv[prefix + 'name'] = new_name
         res = fv.submit('save')
         # get redirected ...
@@ -1209,7 +1209,7 @@
         res = self.app.get(offset)
         assert 'New - Data Packages' in res
         fv = res.forms['package-edit']
-        prefix = 'Package--'
+        prefix = ''
         fv[prefix + 'name'] = self.pkgname
         fv[prefix + 'title'] = self.pkgtitle
         res = fv.submit('preview')
@@ -1225,6 +1225,7 @@
 
 class TestNonActivePackages(TestPackageBase):
 
+
     @classmethod
     def setup_class(self):
         CreateTestData.create()


--- a/ckan/tests/lib/test_dictization_schema.py	Thu May 19 17:45:47 2011 +0100
+++ b/ckan/tests/lib/test_dictization_schema.py	Thu May 19 18:48:11 2011 +0100
@@ -90,6 +90,7 @@
                                  'version': u'0.7a'}, pformat(converted_data)
 
 
+
         assert not errors, errors
 
         data = converted_data
@@ -140,7 +141,6 @@
                                  'packages': sorted([{'id': group.packages[0].id},
                                               {'id': group.packages[1].id,
                                                }], key=lambda x:x["id"]),
-                                 'revision_id': group.revision_id,
                                  'title': u"Dave's books"}
 
 


--- a/doc/forms.rst	Thu May 19 17:45:47 2011 +0100
+++ b/doc/forms.rst	Thu May 19 18:48:11 2011 +0100
@@ -1,13 +1,84 @@
-Forms
-=====
+Forms Using Templates
+=====================
 
 The forms used to edit Packages and Groups in CKAN can be customised. This makes it easier to help users input data, helping them choose from sensible options or to use different data formats. This document sets out to show how this is achieved, without getting embroilled in the main CKAN code.
 
-Each form is defined in a python script and then 'plugged-in' to the CKAN code.
-The form script uses the FormBuilder to simply select from the standard field types, or even define some more field types. You can easily specify the content of drop-downs, hint text and the order of fields which are displayed.
-
 Note that this section deals with the form used to *edit* packages and groups, not the way they are displayed. Display is done in the CKAN templates.
 
+Location of related code
+------------------------
+ 
+ * ckan/controllers/package.py
+ * ckan/templates/package/new_package_form.html
+
+Building a package form
+-----------------------
+
+Basics
+^^^^^^
+
+You will firstly need to make a new controller in your extension.  This should subclass PackageController like::
+
+ from ckan.controllers.package import PackageController
+ class PackageNew(PackageController):
+     package_form = 'custom_package_form.html'
+
+The package_form variable in the subclass will be used as the new form template.
+
+It is recommended that you copy the package form (named new_package_form.html) and make modifications to it. However, it is possible to start from scratch.
+
+In order for you to get this new controller pointed at correctly, your extension should look like the following::
+
+ class CustomForm(SingletonPlugin):
+
+     implements(IRoutes)
+     implements(IConfigurer)
+
+     def before_map(self, map):
+         map.connect('/package/new', controller='ckanext.extension_name.controllers.PackageNewController:PackageNew', action='new')
+         map.connect('/package/edit/{id}', controller='ckanext.extension_name.controllers.PackageNewController:PackageNew', action='edit')
+         return map
+
+     def after_map(self, map):
+         return map
+
+     def update_config(self, config):
+         configure_template_directory(config, 'templates')
+
+Replace extension_name with the name of your extension. This also assumes that custom_package_form.html is located in templates subdirectory of your extension i.e ckanext/extension_name/templates/custom_package_form.html.
+
+Advanced Use
+^^^^^^^^^^^^
+
+The PackageController has a more hooks to customize how and what data is displayed. These functions can be overridden subclass of PackageController::
+
+  _setup_template_variables(self, context)
+
+This is for setting up new variables for your templates::
+
+  _form_to_db_schema(self)
+
+This defines a navl schema to customize validation and conversion to the database::
+
+  _db_to_form_schema(self)
+
+This defines a navl schema to customize conversion from the database to the form.
+
+A complicated example of the use of these hooks can be found in extension ckanext-dgu.
+
+
+Forms using FormAlchemy (depreciated)
+=====================================
+
+This is now depreciated in order to get this to work your extention must implement IRoutes and have a before_map like the following::
+
+ class Plugin(SingletonPlugin):
+     implements(IRoutes)
+
+     def before_map(self, map):
+        map.connect('/package/new', controller='package_formalchemy', action='new')
+        map.connect('/package/edit/{id}', controller='package_formalchemy', action='edit')
+        return map
 
 Location of related code
 ------------------------
@@ -174,4 +245,4 @@
 
  You can also override the config file setting with a URL parameter in your browser. For example you might browse:
 
- http://eco.ckan.net/package/edit/water-voles?package_form=my_form
\ No newline at end of file
+ http://eco.ckan.net/package/edit/water-voles?package_form=my_form

Repository URL: https://bitbucket.org/okfn/ckan/

--

This is a commit notification from bitbucket.org. You are receiving
this because you have the service enabled, addressing the recipient of
this email.




More information about the ckan-changes mailing list