[ckan-changes] commit/ckan: dread: [merge] from feature-1368-ux-fixes.

Bitbucket commits-noreply at bitbucket.org
Mon Oct 17 10:29:36 UTC 2011


1 new changeset in ckan:

http://bitbucket.org/okfn/ckan/changeset/b46fdd2fc4b0/
changeset:   b46fdd2fc4b0
user:        dread
date:        2011-10-17 12:29:24
summary:     [merge] from feature-1368-ux-fixes.
affected #:  42 files (-1 bytes)

--- a/ckan/config/routing.py	Mon Oct 17 10:03:54 2011 +0100
+++ b/ckan/config/routing.py	Mon Oct 17 11:29:24 2011 +0100
@@ -155,7 +155,7 @@
 
     map.connect('/api/2/util/user/autocomplete', controller='api',
         action='user_autocomplete')
-    map.connect('/api/2/util/dataset/create_slug', controller='api', action='create_slug',
+    map.connect('/api/2/util/is_slug_valid', controller='api', action='is_slug_valid',
                 conditions=dict(method=['GET']))
     map.connect('/api/2/util/tag/autocomplete', controller='api', action='tag_autocomplete',
                 conditions=dict(method=['GET']))


--- a/ckan/controllers/api.py	Mon Oct 17 10:03:54 2011 +0100
+++ b/ckan/controllers/api.py	Mon Oct 17 11:29:24 2011 +0100
@@ -9,12 +9,11 @@
 import ckan.rating
 from ckan.lib.search import query_for, QueryOptions, SearchIndexError, SearchError, DEFAULT_OPTIONS, convert_legacy_parameters_to_solr
 from ckan.plugins import PluginImplementations, IGroupController
-from ckan.lib.munge import munge_title_to_name
 from ckan.lib.navl.dictization_functions import DataError
 from ckan.logic import get_action, check_access
 from ckan.logic import NotFound, NotAuthorized, ValidationError
 from ckan.lib.jsonp import jsonpify
-from ckan.forms.common import package_exists
+from ckan.forms.common import package_exists, group_exists
 
 
 log = logging.getLogger(__name__)
@@ -562,17 +561,17 @@
         out = map(convert_to_dict, query.all())
         return out
 
-    def create_slug(self):
-
-        title = request.params.get('title') or ''
-        name = munge_title_to_name(title)
-        if package_exists(name):
-            valid = False
-        else:
-            valid = True
-        #response.content_type = 'application/javascript'
-        response_data = dict(name=name.replace('_', '-'), valid=valid)
-        return self._finish_ok(response_data)
+    def is_slug_valid(self):
+        slug = request.params.get('slug') or ''
+        slugtype = request.params.get('type') or ''
+        if slugtype==u'package':
+            response_data = dict(valid=not bool(package_exists(slug)))
+            return self._finish_ok(response_data)
+        if slugtype==u'group':
+            response_data = dict(valid=not bool(group_exists(slug)))
+            return self._finish_ok(response_data)
+        return self._finish_bad_request(gettext('Bad slug type: %s') % slugtype)
+            
 
     def tag_autocomplete(self):
         q = request.params.get('incomplete', '')


--- a/ckan/controllers/group.py	Mon Oct 17 10:03:54 2011 +0100
+++ b/ckan/controllers/group.py	Mon Oct 17 11:29:24 2011 +0100
@@ -79,6 +79,12 @@
             abort(404, _('Group not found'))
         except NotAuthorized:
             abort(401, _('Unauthorized to read group %s') % id)
+        try:
+            description_formatted = ckan.misc.MarkdownFormat().to_html(group.get('description',''))
+            c.description_formatted = genshi.HTML(description_formatted)
+        except Exception, e:
+            error_msg = "<span class='inline-warning'>%s</span>" % _("Cannot render description")
+            c.description_formatted = genshi.HTML(error_msg)
         
         try:
  
@@ -140,13 +146,14 @@
                 old_data, errors = validate(old_data, schema, context=context)
 
             data = data or old_data
-
         except NotFound:
             abort(404, _('Group not found'))
         except NotAuthorized:
             abort(401, _('Unauthorized to read group %s') % '')
 
         group = context.get("group")
+        c.group = group
+
 
         try:
             check_access('group_update',context)
@@ -208,6 +215,7 @@
             context = {'model':model,'user':c.user or c.author, 'group':group}
             check_access('group_edit_permissions',context)
             c.authz_editable = True
+            c.group = context['group']
         except NotAuthorized:
             c.authz_editable = False
         if not c.authz_editable:


--- a/ckan/controllers/package_formalchemy.py	Mon Oct 17 10:03:54 2011 +0100
+++ b/ckan/controllers/package_formalchemy.py	Mon Oct 17 11:29:24 2011 +0100
@@ -20,8 +20,6 @@
 
     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.
         try:


--- a/ckan/forms/common.py	Mon Oct 17 10:03:54 2011 +0100
+++ b/ckan/forms/common.py	Mon Oct 17 11:29:24 2011 +0100
@@ -39,6 +39,11 @@
         if pkg != field.parent.model:
             raise formalchemy.ValidationError(_('Dataset name already exists in database'))
 
+def group_exists(val):
+    if model.Session.query(model.Group).autoflush(False).filter_by(name=val).count():
+        return True
+    return False
+
 def group_name_validator(val, field=None):
     name_validator(val, field)
     # we disable autoflush here since may get used in dataset preview


--- a/ckan/lib/munge.py	Mon Oct 17 10:03:54 2011 +0100
+++ b/ckan/lib/munge.py	Mon Oct 17 11:29:24 2011 +0100
@@ -7,33 +7,6 @@
 
 from ckan import model
 
-def munge_title_to_name(name):
-    '''Munge a title into a name.
-    '''
-    # remove foreign accents
-    if isinstance(name, unicode):
-        name = substitute_ascii_equivalents(name)
-    # convert spaces and separators
-    name = re.sub('[ .:/]', '-', name)
-    # take out not-allowed characters
-    name = re.sub('[^a-zA-Z0-9-_]', '', name).lower()
-    # remove doubles
-    name = re.sub('--', '-', name)
-    # remove leading or trailing hyphens
-    name = name.strip('-')
-    # if longer than max_length, keep last word if a year
-    max_length = model.PACKAGE_NAME_MAX_LENGTH - 5
-    # (make length less than max, in case we need a few for '_' chars
-    # to de-clash names.)
-    if len(name) > max_length:
-        year_match = re.match('.*?[_-]((?:\d{2,4}[-/])?\d{2,4})$', name)
-        if year_match:
-            year = year_match.groups()[0]
-            name = '%s-%s' % (name[:(max_length-len(year)-1)], year)
-        else:
-            name = name[:max_length]
-    return name
-
 def munge_name(name):
     '''Munges the name field in case it is not to spec.
     '''


--- a/ckan/logic/schema.py	Mon Oct 17 10:03:54 2011 +0100
+++ b/ckan/logic/schema.py	Mon Oct 17 11:29:24 2011 +0100
@@ -40,7 +40,7 @@
         'revistion_id': [ignore_missing, unicode],
         'resource_group_id': [ignore],
         'package_id': [ignore],
-        'url': [not_empty, unicode],#, URL(add_http=False)],
+        'url': [ignore_empty, unicode],#, URL(add_http=False)],
         'description': [ignore_missing, unicode],
         'format': [ignore_missing, unicode],
         'hash': [ignore_missing, unicode],


--- a/ckan/logic/validators.py	Mon Oct 17 10:03:54 2011 +0100
+++ b/ckan/logic/validators.py	Mon Oct 17 11:29:24 2011 +0100
@@ -93,7 +93,7 @@
     if len(val) < 2:
         raise Invalid(_('Name must be at least %s characters long') % 2)
     if not name_match.match(val):
-        raise Invalid(_('Name must be purely lowercase alphanumeric '
+        raise Invalid(_('Url must be purely lowercase alphanumeric '
                         '(ascii) characters and these symbols: -_'))
     return val
 
@@ -111,7 +111,7 @@
         query = query.filter(model.Package.id <> package_id) 
     result = query.first()
     if result:
-        errors[key].append(_('Dataset name already exists in database'))
+        errors[key].append(_('That URL is already in use.'))
 
 def duplicate_extras_key(key, data, errors, context):
 


--- a/ckan/public/css/forms.css	Mon Oct 17 10:03:54 2011 +0100
+++ b/ckan/public/css/forms.css	Mon Oct 17 11:29:24 2011 +0100
@@ -117,37 +117,6 @@
     color: #555;
     font-size: 90%;
   }
-
-form table thead th.form-label {
-    width: 23%;
-    display: none;
-}
-form table thead th.form-value {
-    width: 27%;
-    display: none;
-}
-form table tbody tr td.form-label,
-form table tbody tr td.form-value {
-  font-size: 0.92em;
-  line-height: 1.5em;
-  padding-top: 0;
-  padding-bottom: 4px;
-  white-space: nowrap;
-  background: inherit;
-}
-form table tbody tr td.form-label {
-  text-align: right; 
-  font-weight: bold;
-  padding-right: 4px;
-  vertical-align: middle;
-}
-form table tbody tr td.form-value {
-  padding-left: 4px; 
-  border-left: 1px dashed #aaa;
-}
-form table td.form-value input {
-  width: 100%;
-}
  
 label.has-errors, label.fieldWithErrors {
   font-weight: bold;


--- a/ckan/public/css/style.css	Mon Oct 17 10:03:54 2011 +0100
+++ b/ckan/public/css/style.css	Mon Oct 17 11:29:24 2011 +0100
@@ -183,10 +183,13 @@
 #minornavigation li.current-tab {
   background: #000;
   background-color: #fff;
-  border: 1px solid #aaa;
+  border: 1px solid #777;
+  border-bottom: 1px solid #ccc;
+  border-right: 1px solid #ccc;
      -moz-border-radius: 5px; 
   -webkit-border-radius: 5px; 
           border-radius: 5px; 
+          
 }
 #minornavigation li.current-tab a,
 #minornavigation li.current-tab a:hover,
@@ -791,6 +794,10 @@
   padding: 0 5px 5px 10px;
   width: 32em;
 }
+div.markdown-editor .button-row {
+  padding-right: 40px;
+  text-align: center;
+}
 
 div.markdown-preview {
   background: white;
@@ -807,31 +814,6 @@
 }
 
 
-table.resource-table tbody tr td.resource-expand-link,
-table.resource-table tbody tr td.resource-is-changed {
-  vertical-align: top;
-}
-
-td.resource-expanded {
-  padding: 8px 0 8px 0;
-}
-td.resource-expand-link {
-  padding-top: 6px;
-  text-align: center;
-}
-th.resource-expand-link,
-td.resource-expand-link,
-th.resource-is-changed, 
-td.resource-is-changed {
-  text-align: center;
-  padding: 8px 0 0 0;
-  width: 20px;
-}
-
-div.inner table {
-  margin-bottom: 0;
-}
-
 /* ====================== */
 /* = Add Resources Page = */
 /* ====================== */
@@ -840,7 +822,7 @@
   width: 60%;
 }
 
-button.delete-resource {
+button.done-editing {
   float: right;
 }
 
@@ -859,6 +841,9 @@
 div.resource-add-subpane {
   margin-top: 10px;
 }
+div.resource-add .fileinfo {
+  margin: 7px 0;
+}
 
 
 /* ==================== */
@@ -903,20 +888,31 @@
   display: none;
 }
 
-body.package.new dt.name-label {
-  font-size: 10px;
-}
-
 body.package.new .instructions {
   font-size: 10px;
 }
 
-body.package.new input#name {
-  font-size: 8px;
+a.url-edit {
+  font-weight: normal;
+  margin-left: 10px;
+}
+dd.name-field {
+  padding-top: 0.2em;
+}
+dd.name-field p {
+  margin-bottom: 4px;
+}
 
-  background: #eee;
+body.package.read #sidebar ul.tags, 
+body.package.read #sidebar ul.groups {
+  margin-bottom: 10px;
 }
 
+input.url-input {
+  width: 250px;
+}
+
+
 .success .new-dataset {
   font-size: 150%;
 }
@@ -924,57 +920,42 @@
   font-weight: bold;
 }
 
-/* =============================== */
-/* = Mini-Tabs [Markdown Editor] = */
-/* =============================== */
 
-ul.tabs {
+/* ================== */
+/* = Add Group Page = */
+/* ================== */
+
+body.group.new fieldset#extras,
+body.group.new fieldset#datasets {
+  display: none;
+}
+body.group.new .description-label,
+body.group.new .description-field,
+body.group.new .state-label,
+body.group.new .state-field {
+  display: none;
+}
+
+
+/* ============= */
+/* = Mini-Tabs = */
+/* ============= */
+ul.button-row {
   margin-bottom: 5px;
 }
-
-ul.tabs li {
+ul.button-row li {
   display: inline;
+  margin-right: 10px;
 }
 
-ul.tabs li a {
-  display: inline-block;
-  padding: 2px 8px;
-  margin-right: 10px;
-  font-size: 10px;
-  font-weight: bold;
-  text-decoration: none;
-  color: #666;
-  border: 1px solid #DDD;
-  border-color: #DDD;
-  border-right-color: #BBB;
-  border-bottom-color: #BBB;
-  -webkit-border-radius: 10px;
-  -moz-border-radius: 10px;
-  border-top-left-radius: 10px 10px;
-  border-top-right-radius: 10px 10px;
-  border-bottom-right-radius: 10px 10px;
-  border-bottom-left-radius: 10px 10px;
-}
-
-ul.tabs li a.selected {
-  color: #333;
-  background: white;
-  border-color: #BBB;
-  border-right-color: #DDD;
-  border-bottom-color: #DDD;
-}
 
 
 /* ============================== */
 /* = Controller-specific tweaks = */
 /* ============================== */
 
-body.group.index #minornavigation { 
-  visibility: hidden; 
-}
-
 body.package.search #minornavigation { 
-  visibility: hidden; 
+  display: none;
 }
 body.package.search #menusearch {
   display: none;
@@ -983,7 +964,6 @@
 body.index.home #minornavigation {
   display: none;
 }
-
 body.index.home #sidebar {
   display: none;
 }
@@ -1107,10 +1087,10 @@
 body.admin .actions input {
   margin: 0;
 }
-body.admin.authz form {
+body.authz form {
   margin-bottom: 30px;
 }
-body.admin.authz form button {
+body.authz form button {
   width: 120px;
   float: right;
 }
@@ -1149,6 +1129,7 @@
   -moz-transition: 0.1s linear all;
   transition: 0.1s linear all;
 }
+.pretty-button.depressed,
 .pretty-button:hover {
   background-position: 0 -15px;
   color: #333;
@@ -1216,6 +1197,7 @@
   opacity: 0.65;
   cursor: default;
 }
+.pretty-button.depressed,
 .pretty-button:active {
   -webkit-box-shadow: inset 0 3px 7px rgba(0, 0, 0, 0.1), 0 1px 2px rgba(0, 0, 0, 0.05);
   -moz-box-shadow: inset 0 3px 7px rgba(0, 0, 0, 0.1), 0 1px 2px rgba(0, 0, 0, 0.05);
@@ -1240,3 +1222,83 @@
   color: #000;
 }
 
+/* ============================= */
+/* = Inline resource edit form = */
+/* ============================= */
+
+.resource-table-edit tbody tr td,
+.resource-table-view tbody tr td {
+  /* Constrain structure against overflow */
+  max-width: 200px;
+  overflow: hidden;
+  white-space: nowrap;
+}
+
+.resource-table-edit tr {
+  overflow: hidden;
+}
+th.resource-edit-delete,
+td.resource-edit-delete {
+  /* Override screen.css */
+  padding: 0;
+  width: 40px;
+}
+td.resource-edit-delete img {
+  padding: 8px;
+}
+a.resource-edit-expand {
+  background-image: url('/images/icons/arrow-closed.gif');
+  padding-left: 13px;
+  background-position: left center;
+  background-repeat: no-repeat;
+}
+td.resource-edit {
+  padding: 10px;
+  vertical-align: top;
+}
+.resource-edit-expanded {
+  overflow: hidden;
+  margin: 0;
+}
+.resource-edit-expanded table {
+  /* Override screen.css */
+  margin: 10px 0;
+}
+table.resource-table-edit td.resource-edit tbody td {
+  /* Override alternating background */
+  background: transparent;
+}
+td.resource-edit-delete {
+  vertical-align: top;
+  text-align: center;
+}
+td.resource-edit input {
+  /* Override forms.css */
+  width: 100%;
+}
+th.resource-edit-label {
+    width: 23%;
+    display: none;
+}
+th.resource-edit-value {
+    width: 27%;
+    display: none;
+}
+td.resource-edit-label,
+td.resource-edit-value {
+  font-size: 0.92em;
+  line-height: 1.5em;
+  padding-top: 0;
+  padding-bottom: 4px;
+  background: inherit;
+}
+td.resource-edit-label {
+  text-align: right; 
+  font-weight: bold;
+  padding-right: 4px;
+  vertical-align: middle;
+}
+td.resource-edit-value {
+  padding-left: 4px; 
+  border-left: 1px dashed #aaa;
+}


Binary file ckan/public/images/icons/arrow-closed.gif has changed


Binary file ckan/public/images/icons/arrow-open.gif has changed


Binary file ckan/public/images/icons/delete.png has changed


Binary file ckan/public/images/icons/pencil.png has changed


--- a/ckan/public/scripts/application.js	Mon Oct 17 10:03:54 2011 +0100
+++ b/ckan/public/scripts/application.js	Mon Oct 17 11:29:24 2011 +0100
@@ -7,7 +7,7 @@
     $('input.autocomplete-format').live('keyup', function(){
       CKAN.Utils.setupFormatAutocomplete($(this));
     });
-    CKAN.Utils.setupMarkdownEditor($('.markdown-editor .tabs a, .markdown-editor .markdown-preview'));
+    CKAN.Utils.setupMarkdownEditor($('.markdown-editor'));
     // set up ckan js
     var config = {
       endpoint: '/'
@@ -26,7 +26,17 @@
 
     var isDatasetNew = $('body.package.new').length > 0;
     if (isDatasetNew) {
+      // Set up magic URL slug editor
+      CKAN.Utils.setupUrlEditor('package');
       $('#save').val(CKAN.Strings.addDataset);
+      $("#title").focus();
+    }
+    var isGroupNew = $('body.group.new').length > 0;
+    if (isGroupNew) {
+      // Set up magic URL slug editor
+      CKAN.Utils.setupUrlEditor('group');
+      $('#save').val(CKAN.Strings.addGroup);
+      $("#title").focus();
     }
 
     // Buttons with href-action should navigate when clicked
@@ -37,10 +47,11 @@
     
     var isDatasetEdit = $('body.package.edit').length > 0;
     if (isDatasetEdit) {
+      CKAN.Utils.setupUrlEditor('package',readOnly=true);
       // Selectively enable the upload button
       var storageEnabled = $.inArray('storage',CKAN.plugins)>=0;
       if (storageEnabled) {
-        $('div.resource-add li.upload-file').show();
+        $('li.js-upload-file').show();
       }
 
       // Set up hashtag nagivigation
@@ -48,18 +59,138 @@
 
       var _dataset = new CKAN.Model.Dataset(preload_dataset);
       var $el=$('form#dataset-edit');
-      var view=new CKAN.View.DatasetEdit({
+      var view=new CKAN.View.DatasetEditForm({
         model: _dataset,
         el: $el
       });
       view.render();
     }
+    var isGroupEdit = $('body.group.edit').length > 0;
+    if (isGroupEdit) {
+      CKAN.Utils.setupUrlEditor('group',readOnly=true);
+    }
   });
 }(jQuery));
 
 var CKAN = CKAN || {};
 
 CKAN.Utils = function($, my) {
+
+  my.flashMessage = function(msg, category) {
+    if (!category) {
+      category = 'info';
+    }
+    var messageDiv = $('<div />').html(msg).addClass(category).hide();
+    $('.flash-messages').append(messageDiv);
+    messageDiv.show(1200);
+
+  };
+
+  my.bindInputChanges = function(input, callback) {
+    input.keyup(callback);
+    input.keydown(callback);
+    input.keypress(callback);
+    input.change(callback);
+  };
+
+  my.setupUrlEditor = function(slugType,readOnly) {
+    // Page elements to hook onto
+    var titleInput = $('.js-title');
+    var urlText = $('.js-url-text');
+    var urlSuffix = $('.js-url-suffix');
+    var urlInput = $('.js-url-input');
+    var validMsg = $('.js-url-is-valid');
+
+    var api_url = '/api/2/util/is_slug_valid';
+    // (make length less than max, in case we need a few for '_' chars to de-clash slugs.)
+    var MAX_SLUG_LENGTH = 90;
+
+    var titleChanged = function() {
+      var lastTitle = "";
+      var regexToHyphen = [ new RegExp('[ .:/_]', 'g'), 
+                        new RegExp('[^a-zA-Z0-9-_]', 'g'), 
+                        new RegExp('-+', 'g')];
+      var regexToDelete = [ new RegExp('^-*', 'g'), 
+                        new RegExp('-*$', 'g')];
+
+      var titleToSlug = function(title) {
+        var slug = title;
+        $.each(regexToHyphen, function(idx,regex) { slug = slug.replace(regex, '-'); });
+        $.each(regexToDelete, function(idx,regex) { slug = slug.replace(regex, ''); });
+        slug = slug.toLowerCase();
+
+        if (slug.length<MAX_SLUG_LENGTH) {
+            slug=slug.substring(0,MAX_SLUG_LENGTH);
+        }
+        return slug;
+      };
+
+      // Called when the title changes
+      return function() {
+        var title = titleInput.val();
+        if (title == lastTitle) return;
+        lastTitle = title;
+
+        slug = titleToSlug(title);
+        urlInput.val(slug);
+        urlInput.change();
+      };
+    }();
+
+    var urlChanged = function() {
+      var timer = null;
+
+      var checkSlugValid = function(slug) {
+        $.ajax({
+          url: api_url,
+          data: 'type='+slugType+'&slug=' + slug,
+          dataType: 'jsonp',
+          type: 'get',
+          jsonpCallback: 'callback',
+          success: function (data) {
+            if (data.valid) {
+              validMsg.html('<span style="font-weight: bold; color: #0c0">'+CKAN.Strings.urlIsAvailable+'</span>');
+            } else {
+              validMsg.html('<span style="font-weight: bold; color: #c00">'+CKAN.Strings.urlIsNotAvailable+'</span>');
+            }
+          }
+        });
+      }
+
+      return function() {
+        slug = urlInput.val();
+        urlSuffix.html('<span>'+slug+'</span>');
+        validMsg.html('<span style="color: #777;">'+CKAN.Strings.checking+'</span>');
+        if (timer) clearTimeout(timer);
+        timer = setTimeout(function () {
+          checkSlugValid(slug);
+        }, 200);
+      };
+    }();
+
+    if (readOnly) {
+      slug = urlInput.val();
+      urlSuffix.html('<span>'+slug+'</span>');
+    }
+    else {
+      var editLink = $('.js-url-editlink');
+      editLink.show();
+      // Hook title changes to the input box
+      my.bindInputChanges(titleInput, titleChanged);
+      my.bindInputChanges(urlInput, urlChanged);
+      // Set up the form
+      urlChanged();
+
+      editLink.live('click',function(e) {
+        e.preventDefault();
+        $('.js-url-viewmode').hide();
+        $('.js-url-editmode').show();
+        urlInput.select();
+        urlInput.focus();
+      });
+    }
+  }
+
   // Attach dataset autocompletion to provided elements
   //
   // Requires: jquery-ui autocomplete
@@ -215,20 +346,19 @@
     });
   };
 
-  my.setupMarkdownEditor = function(elements) {
+  my.setupMarkdownEditor = function(markdownEditor) {
     // Markdown editor hooks
-    elements.live('click', function(e) {
+    markdownEditor.find('button, div.markdown-preview').live('click', function(e) {
       e.preventDefault();
-      var $el = $(e.target);
-      var action = $el.attr('action') || 'write';
+      var $target = $(e.target);
       // Extract neighbouring elements
-      var div=$el.closest('.markdown-editor')
-      div.find('.tabs a').removeClass('selected');
-      div.find('.tabs a[action='+action+']').addClass('selected');
-      var textarea = div.find('.markdown-input');
-      var preview = div.find('.markdown-preview');
+      var markdownEditor=$target.closest('.markdown-editor')
+      markdownEditor.find('button').removeClass('depressed');
+      var textarea = markdownEditor.find('.markdown-input');
+      var preview = markdownEditor.find('.markdown-preview');
       // Toggle the preview
-      if (action=='preview') {
+      if ($target.is('.js-markdown-preview')) {
+        $target.addClass('depressed');
         raw_markdown=textarea.val();
         preview.html("<em>"+CKAN.Strings.loading+"<em>");
         $.post("/api/util/markdown", { q: raw_markdown },
@@ -239,6 +369,7 @@
         textarea.hide();
         preview.show();
       } else {
+        markdownEditor.find('.js-markdown-edit').addClass('depressed');
         textarea.show();
         preview.hide();
         textarea.focus();
@@ -274,186 +405,69 @@
     });  
   };
 
-  // Name slug generator for $name element using $title element
-  //
-  // Also does nice things like show errors if name not available etc
-  //
-  // Usage: CKAN.Utils.PackageSlugCreator.create($('#my-title'), $('#my-name'))
-  my.PackageSlugCreator = (function() {
-    // initialize function
-    // 
-    // args: $title and $name input elements
-    function SlugCreator($title, $name) {
-      this.name_field = $name;
-      this.title_field = $title;
-      // 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.
-      this.name_changed = false;
-      // url for slug api (we need api rather than do it ourself because we check if available)
-      this.url = '/api/2/util/dataset/create_slug';
-      // Add a new element where the validity of the dataset name can be displayed
-      this.name_field.parent().append('<div id="dataset_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());
-    }
-
-    SlugCreator.create = function($title, $name) {
-      return new SlugCreator($title, $name);
-    }
-
-    SlugCreator.prototype.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)});
-        }
-      }
-    }
-
-    SlugCreator.prototype.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;
-          $('#dataset_name_valid_msg').html('');
-        } else {
-          self.update(self.name_field.val(), function(data) {
-              self.name_field.val(data.name)
-          });
-        }
-      };
-    }
-
-    SlugCreator.prototype.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;
-          $('#dataset_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);
-            }
-          });
-        }
-      };
-    }
-
-    // Create a function for fetching the value and updating the result
-    SlugCreator.prototype.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 = $('#dataset_name_valid_msg');
-          if (data.valid) {
-            valid_msg.html('<span style="font-weight: bold; color: #0c0">'+CKAN.Strings.datasetNameAvailable+'</span>');
-          } else {
-            valid_msg.html('<span style="font-weight: bold; color: #c00">'+CKAN.Strings.datasetNameNotAvailable+'</span>');
-          }
-        }
-      });
-    }
-
-    // We only want to perform the update if there hasn't been a change for say 200ms
-    var timer = null;
-    SlugCreator.prototype.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);
-    }
-
-    return SlugCreator;
-  })();
-
-
   return my;
 }(jQuery, CKAN.Utils || {});
 
 
-CKAN.View.DatasetEdit = Backbone.View.extend({
+CKAN.View.DatasetEditForm = Backbone.View.extend({
   initialize: function() {
-    _.bindAll(this, 'render');
+    var resources = this.model.get('resources');
+    var $form = this.el;
 
-    var boundToUnload = false;
-    this.el.change(function() {
-      if (!boundToUnload) {
-        boundToUnload = true;
-        window.onbeforeunload = function () { 
-          return CKAN.Strings.youHaveUnsavedChanges; 
-        };
+    var changesMade = function() {
+      var boundToUnload = false;
+      return function() {
+        if (!boundToUnload) {
+          CKAN.Utils.flashMessage(CKAN.Strings.youHaveUnsavedChanges,'notice');
+          boundToUnload = true;
+          window.onbeforeunload = function () { 
+            return CKAN.Strings.youHaveUnsavedChanges; 
+          };
+        }
+      }
+    }();
+
+    $form.find('input').live('change', function(e) {
+      $target = $(e.target);
+      // Entering text in the 'add' box does not represent a change
+      if ($target.closest('.resource-add').length==0) {
+        changesMade();
       }
     });
-    this.el.submit(function() {
+    resources.bind('add', changesMade);
+    resources.bind('remove', changesMade);
+
+    $form.submit(function() {
       // Don't stop us leaving
       window.onbeforeunload = null;
     });
 
-    // Tabbed view for adding resources
-    var $el=this.el.find('.resource-add');
-    this.addView=new CKAN.View.ResourceAdd({
-      collection: this.model.get('resources'),
+    // Table for editing resources
+    var $el = this.el.find('.js-resource-editor');
+    this.resourceList=new CKAN.View.ResourceEditList({
+      collection: resources,
       el: $el
     });
 
-    // Table for editing resources
-    var $el=this.el.find('.resource-table.edit');
-    this.resourceList=new CKAN.View.ResourceEditList({
-      collection: this.model.get('resources'),
+    // Tabbed view for adding resources
+    var $el = this.el.find('.resource-add');
+    this.addView=new CKAN.View.ResourceAddTabs({
+      collection: resources,
       el: $el
     });
 
-    this.render();
-  },
-
-
-  render: function() {
     this.addView.render();
     this.resourceList.render();
   },
-
-  events: {
-  }
-
 });
 
 
 CKAN.View.ResourceEditList = Backbone.View.extend({
   initialize: function() {
-    _.bindAll(this, 'render', 'addRow');
-    this.collection.bind('add', this.addRow);
-  },
-
-  render: function() {
-    var self = this;
-
-    // Have to trash entire content; some stuff was there on page load
-    this.el.find('tbody').empty();
-    this.collection.each(this.addRow);
-
-    if (this.collection.isEmpty()) {
-      $tr = $('<tr />').addClass('table-empty');
-      $tr.html('<td></td><td colspan="4">'+CKAN.Strings.bracketsNone+'</td>');
-      this.el.find('tbody').append($tr);
-    }
+    _.bindAll(this, 'addResource', 'removeResource');
+    this.collection.bind('add', this.addResource);
+    this.collection.bind('remove', this.removeResource);
+    this.collection.each(this.addResource);
   },
 
   nextIndex: function() {
@@ -468,127 +482,96 @@
     return maxId+1;
   },
 
-  addRow: function(resource) {
-    // Strip placeholder row
-    this.el.find('tr.table-empty').remove();
+  addResource: function(resource) {
+    var position = this.nextIndex();
+    // Create a row from the template
+    var $tr = $('<tr />');
+    $tr.html($.tmpl(
+      CKAN.Templates.resourceEntry, 
+      { resource: resource.toTemplateJSON(),
+        num: position
+      }
+    ));
+    $tr.find('.js-resource-edit-expanded').hide();
+    this.el.append($tr);
+    resource.view_tr = $tr;
 
-    // TODO tidy up so the view creates its own elements
-    var $tr = $('<tr />');
+    // == Inner Function: Toggle the expanded options set == //
+    var toggleOpen = function(triggerEvent) {
+      if (triggerEvent) triggerEvent.preventDefault();
+      var animTime = 350;
+      var expandedTable = $tr.find('.js-resource-edit-expanded');
+      var finalHeight = expandedTable.height();
+      var icon = 'closed';
 
-    // Captured by an inner function
-    var self = this;
+      if (expandedTable.is(':visible')) {
+        expandedTable.animate(
+            {height:0},
+            animTime,
+            function() { 
+              expandedTable.height(finalHeight);
+              expandedTable.hide(); 
+            }
+        );
+      }
+      else {
+        expandedTable.show();
+        expandedTable.height(0);
+        // Transition to its true height
+        expandedTable.animate({height:finalHeight}, animTime);
+        $tr.find('.js-resource-edit-name').focus();
+        icon = 'open';
+      }
+      $tr.find('.js-resource-edit-toggle').css("background-image", "url('/images/icons/arrow-"+icon+".gif')");
+    };
 
-    this.el.find('tbody.resource-table').append($tr);
-    var _view = new CKAN.View.ResourceEdit({
-      model: resource,
-      el: $tr,
-      position: this.nextIndex(),
-      deleteResource: function() {
-        // Passing down a capture to remove the resource
-        $tr.remove();
-        
-        self.collection.remove(resource);
-        if (self.collection.isEmpty()) {
-          self.render();
-        }
+    // == Inner Function: Delete the row == //
+    var collection = this.collection;
+    var deleteResource = function(triggerEvent) {
+      if (triggerEvent) triggerEvent.preventDefault();
+      confirmMessage = CKAN.Strings.deleteThisResourceQuestion;
+      resourceName = resource.attributes.name || CKAN.Strings.noNameBrackets;
+      confirmMessage = confirmMessage.replace('%name%', resourceName);
+      if (confirm(confirmMessage)) {
+        collection.remove(resource);
       }
-    });
-    _view.render();
+    };
+
+    // == Inner Functions: Update the name as you type == //
+    var setName = function(newName) { 
+      $link = $tr.find('.js-resource-edit-toggle');
+      newName = newName || ('<em>'+CKAN.Strings.noNameBrackets+'</em>');
+      // Need to structurally modify the DOM to force a re-render of text
+      $link.html('<ema>'+newName+'</span>');
+    };
+    var nameBoxChanged = function(e) {
+      setName($(e.target).val());
+    }
+
+    // Trigger animation
+    if (resource.isNew()) {
+      toggleOpen();
+    }
+
+    var nameBox = $tr.find('input.js-resource-edit-name');
+    CKAN.Utils.bindInputChanges(nameBox,nameBoxChanged);
+
+    $tr.find('.js-resource-edit-toggle').click(toggleOpen);
+    $tr.find('.js-resource-edit-delete').click(deleteResource);
+    // Initialise name
+    setName(resource.attributes.name);
   },
 
-  events: {
-  }
+  removeResource: function(resource) {
+    if (resource.view_tr) {
+      resource.view_tr.remove();
+      delete resource.view_tr;
+    }
+  },
 });
 
-CKAN.View.ResourceEdit = Backbone.View.extend({
-  initialize: function() {
-    _.bindAll(this, 'render', 'toggleExpanded');
-    var self = this;
-    this.model.bind('change', function() { self.hasChanged=true; });
-    this.model.bind('change', this.render());
-    this.position = this.options.position;
 
-    this.expanded = this.model.isNew();
-    this.hasChanged = this.model.isNew();
-    this.animate = this.model.isNew();
-  },
-
-  render: function() {
-    var tmplData = {
-      resource: this.model.toTemplateJSON(),
-      num: this.position
-    };
-    var $newRow = $.tmpl(CKAN.Templates.resourceEntry, tmplData);
-    this.el.html($newRow);
-
-    if (this.expanded) {
-      this.el.find('a.resource-expand-link').hide();
-      this.el.find('.resource-summary').hide();
-      if (this.animate) {
-        this.el.find('.resource-expanded .inner').hide();
-        this.el.find('.resource-expanded .inner').show('slow');
-      }
-    }
-    else {
-      this.el.find('a.resource-collapse-link').hide();
-      this.el.find('.resource-expanded').hide();
-    }
-
-    if (!this.hasChanged) {
-      this.el.find('img.resource-is-changed').hide();
-    }
-    this.animate = false;
-  },
-
-  events: {
-    'click a.resource-expand-link': 'toggleExpanded',
-    'click a.resource-collapse-link': 'toggleExpanded',
-    'click .delete-resource': 'clickDelete'
-  },
-
-  clickDelete: function(e) {
-    e.preventDefault();
-    this.options.deleteResource();
-  },
-
-  saveData: function() {
-    this.model.set(this.getData(), {
-      error: function(model, error) {
-        var msg = CKAN.Strings.failedToSave;
-        msg += JSON.stringify(error);
-        alert(msg);
-      }
-    });
-    return false;
-  },
-
-  getData: function() {
-    var _data = $(this.el).find('input').serializeArray();
-    modelData = {};
-    $.each(_data, function(idx, value) {
-      modelData[value.name.split('__')[2]] = value.value
-    });
-    return modelData;
-  },
-
-  toggleExpanded: function(e) {
-    e.preventDefault();
-
-    this.expanded = !this.expanded;
-    this.animate = true;
-    // Closing the form; update the model fields
-    if (!this.expanded) {
-      this.saveData();
-      // Model might not have changed
-      this.render();
-    } else {
-      this.render();
-    }
-  }
-
-});
-
-CKAN.View.ResourceAdd = Backbone.View.extend({
+CKAN.View.ResourceAddTabs = Backbone.View.extend({
   initialize: function() {
     _.bindAll(this, 'render', 'addNewResource', 'reset');
   },
@@ -597,12 +580,12 @@
   },
 
   events: {
-    'click .action-resource-tab': 'clickAdd',
+    'click button': 'clickButton',
     'click input[name=reset]': 'reset'
   },
 
   reset: function() {
-    this.el.find('.tabs a').removeClass('selected');
+    this.el.find('button').removeClass('depressed');
     if (this.subView != null) {
       this.subView.remove();
       this.subView = null;
@@ -610,40 +593,43 @@
     return false;
   },
 
-  clickAdd: function(e) {
+  clickButton: function(e) {
     e.preventDefault();
+    var $target = $(e.target);
+    
+    if ($target.is('.depressed')) {
+      this.reset();
+    } 
+    else {
+      this.reset();
+      $target.addClass('depressed');
 
-    this.reset();
+      var $subPane = $('<div />').addClass('resource-add-subpane');
+      this.el.append($subPane);
 
-    var action = $(e.target).attr('action');
-    this.el.find('.tabs a').removeClass('selected');
-    this.el.find('.tabs a[action='+action+']').addClass('selected');
+      var tempResource = new CKAN.Model.Resource({});
 
-    var $subPane = $('<div />').addClass('resource-add-subpane');
-    this.el.append($subPane);
-
-    var tempResource = new CKAN.Model.Resource({});
-
-    tempResource.bind('change', this.addNewResource);
-    // Open sub-pane
-    if (action=='upload-file') {
-      this.subView = new CKAN.View.ResourceUpload({
-        el: $subPane,
-        model: tempResource,
-        // TODO: horrible reverse depedency ...
-        client: CKAN.UI.workspace.client
-      });
+      tempResource.bind('change', this.addNewResource);
+      // Open sub-pane
+      if ($target.is('.js-upload-file')) {
+        this.subView = new CKAN.View.ResourceUpload({
+          el: $subPane,
+          model: tempResource,
+          // TODO: horrible reverse depedency ...
+          client: CKAN.UI.workspace.client
+        });
+      }
+      else if ($target.is('.js-link-file') || $target.is('.js-link-api')) {
+        this.subView = new CKAN.View.ResourceAddLink({
+          el: $subPane,
+          model: tempResource,
+          mode: ($target.is('.js-link-file'))? 'file' : 'api',
+          // TODO: horrible reverse depedency ...
+          client: CKAN.UI.workspace.client
+        });
+      }
+      this.subView.render();
     }
-    else if (action=='link-file' || action=='link-api') {
-      this.subView = new CKAN.View.ResourceAddLink({
-        el: $subPane,
-        model: tempResource,
-        mode: (action=='link-file')? 'file' : 'api',
-        // TODO: horrible reverse depedency ...
-        client: CKAN.UI.workspace.client
-      });
-    }
-    this.subView.render();
   },
 
   addNewResource: function(tempResource) {


--- a/ckan/public/scripts/templates.js	Mon Oct 17 10:03:54 2011 +0100
+++ b/ckan/public/scripts/templates.js	Mon Oct 17 11:29:24 2011 +0100
@@ -1,6 +1,6 @@
 
 CKAN.Templates.resourceAddLinkFile = ' \
-  <form class="resource-add" action=""> \
+  <form class="resource-add"> \
     <dl> \
       <dt> \
         <label class="field_opt" for="url"> \
@@ -18,7 +18,7 @@
 ';
 
 CKAN.Templates.resourceAddLinkApi = ' \
-  <form class="resource-add" action=""> \
+  <form class="resource-add"> \
     <dl> \
       <dt> \
         <label class="field_opt" for="url"> \
@@ -50,7 +50,8 @@
       </dt> \
       <dd> \
         <input type="file" name="file" /> \
-        <span class="fileinfo"></span> \
+        <br /> \
+        <div class="fileinfo"></div> \
         <input id="upload" name="upload" type="submit" class="pretty-button primary" value="'+CKAN.Strings.add+'" /> \
         <input id="reset" name="reset" type="reset" class="pretty-button" value="'+CKAN.Strings.cancel+'" /> \
       </dd> \
@@ -64,44 +65,26 @@
 
 
 CKAN.Templates.resourceEntry = ' \
-  <td class="resource-expand-link"> \
-    <a class="resource-expand-link" href="#"><img src="/images/icons/edit-expand.png" /></a> \
-    <a class="resource-collapse-link" href="#"><img src="/images/icons/edit-collapse.png" /></a> \
-  </td> \
-  <td class="resource-summary resource-url"> \
-    ${resource.url} \
-  </td> \
-  <td class="resource-summary resource-name"> \
-    ${resource.name} \
-  </td> \
-  <td class="resource-summary resource-format"> \
-    ${resource.format} \
-  </td> \
-  <td class="resource-expanded" colspan="3"> \
-    <div class="inner"> \
+  <td class="resource-edit"> \
+    <a class="resource-edit-expand js-resource-edit-toggle" href="#">${resource.name}</a>\
+    <div class="resource-edit-expanded js-resource-edit-expanded"> \
     <table> \
-      <thead> \
-      <th class="form-label"></th> \
-      <th class="form-value"></th> \
-      <th class="form-label"></th> \
-      <th class="form-value"></th> \
-      </thead> \
       <tbody> \
       <tr> \
-      <td class="form-label">'+CKAN.Strings.name+'</td> \
-      <td class="form-value" colspan="3"> \
-        <input name="resources__${num}__name" type="text" value="${resource.name}" class="long" /> \
+      <td class="resource-edit-label">'+CKAN.Strings.name+'</td> \
+      <td class="resource-edit-value" colspan="3"> \
+        <input class="js-resource-edit-name" name="resources__${num}__name" type="text" value="${resource.name}" class="long" /> \
       </td> \
       </tr> \
       <tr> \
-      <td class="form-label">'+CKAN.Strings.description+'</td> \
-      <td class="form-value" colspan="3"> \
+      <td class="resource-edit-label">'+CKAN.Strings.description+'</td> \
+      <td class="resource-edit-value" colspan="3"> \
         <input name="resources__${num}__description" type="text" value="${resource.description}" class="long" /> \
       </td> \
       </tr> \
       <tr> \
-      <td class="form-label">'+CKAN.Strings.url+'</td> \
-      <td class="form-value" colspan="3"> \
+      <td class="resource-edit-label">'+CKAN.Strings.url+'</td> \
+      <td class="resource-edit-value" colspan="3"> \
       {{if resource.resource_type=="file.upload"}} \
         ${resource.url} \
         <input name="resources__${num}__url" type="hidden" value="${resource.url}" /> \
@@ -112,59 +95,60 @@
       </td> \
       </tr> \
       <tr> \
-      <td class="form-label">'+CKAN.Strings.format+'</td> \
-      <td class="form-value"> \
+      <td class="resource-edit-label">'+CKAN.Strings.format+'</td> \
+      <td class="resource-edit-value"> \
         <input name="resources__${num}__format" type="text" value="${resource.format}" class="long autocomplete-format" /> \
       </td> \
-      <td class="form-label">'+CKAN.Strings.resourceType+'</td> \
-      <td class="form-value"> \
-      {{if resource.resource_type=="file.upload"}} \
-        ${resource.resource_type} \
-        <input name="resources__${num}__resource_type" type="hidden" value="${resource.resource_type}" /> \
-      {{/if}} \
-      {{if resource.resource_type!="file.upload"}} \
-        <input name="resources__${num}__resource_type" type="text" value="${resource.resource_type}" /> \
-      {{/if}} \
+      <td class="resource-edit-label">'+CKAN.Strings.resourceType+'</td> \
+      <td class="resource-edit-value"> \
+        {{if resource.resource_type=="file.upload"}} \
+          ${resource.resource_type} \
+          <input name="resources__${num}__resource_type" type="hidden" value="${resource.resource_type}" /> \
+        {{/if}} \
+        {{if resource.resource_type!="file.upload"}} \
+          <input name="resources__${num}__resource_type" type="text" value="${resource.resource_type}" /> \
+        {{/if}} \
       </td> \
       </tr> \
       <tr> \
-      <td class="form-label">'+CKAN.Strings.sizeBracketsBytes+'</td> \
-      <td class="form-value"> \
+      <td class="resource-edit-label">'+CKAN.Strings.sizeBracketsBytes+'</td> \
+      <td class="resource-edit-value"> \
         <input name="resources__${num}__size" type="text" value="${resource.size}" class="long" /> \
       </td> \
-      <td class="form-label">'+CKAN.Strings.mimetype+'</td> \
-      <td class="form-value"> \
+      <td class="resource-edit-label">'+CKAN.Strings.mimetype+'</td> \
+      <td class="resource-edit-value"> \
         <input name="resources__${num}__mimetype" type="text" value="${resource.mimetype}" /> \
       </td> \
       </tr> \
       <tr> \
-      <td class="form-label">'+CKAN.Strings.lastModified+'</td> \
-      <td class="form-value"> \
+      <td class="resource-edit-label">'+CKAN.Strings.lastModified+'</td> \
+      <td class="resource-edit-value"> \
         <input name="resources__${num}__last_modified" type="text" value="${resource.last_modified}" /> \
       </td> \
-      <td class="form-label">'+CKAN.Strings.mimetypeInner+'</td> \
-      <td class="form-value"> \
+      <td class="resource-edit-label">'+CKAN.Strings.mimetypeInner+'</td> \
+      <td class="resource-edit-value"> \
         <input name="resources__${num}__mimetype_inner" type="text" value="${resource.mimetype_inner}" /> \
       </td> \
       </tr> \
       <tr> \
-      <td class="form-label">'+CKAN.Strings.hash+'</td> \
-      <td class="form-value"> \
+      <td class="resource-edit-label">'+CKAN.Strings.hash+'</td> \
+      <td class="resource-edit-value" colspan="3"> \
         ${resource.hash || "Unknown"} \
         <input name="resources__${num}__hash" type="hidden" value="${resource.hash}" /> \
       </td> \
-      <td class="form-label">'+CKAN.Strings.id+'</td> \
-      <td class="form-value"> \
+      </tr> \
+      <tr> \
+      <td class="resource-edit-label">'+CKAN.Strings.id+'</td> \
+      <td class="resource-edit-value" colspan="3"> \
         ${resource.id} \
         <input name="resources__${num}__id" type="hidden" value="${resource.id}" /> \
       </td> \
       </tr> \
     </tbody> \
     </table> \
-    <button class="delete-resource pretty-button danger">'+CKAN.Strings.deleteResource+'</button> \
     </div> \
   </td> \
-  <td class="resource-is-changed"> \
-    <img src="/images/icons/add.png" title="'+CKAN.Strings.resourceHasUnsavedChanges+'" class="resource-is-changed" /> \
+  <td class="resource-edit-delete"> \
+    <a class="resource-edit-delete js-resource-edit-delete" href="#"><img src="/images/icons/delete.png" /></a> \
   </td> \
 ';


--- a/ckan/templates/group/authz.html	Mon Oct 17 10:03:54 2011 +0100
+++ b/ckan/templates/group/authz.html	Mon Oct 17 11:29:24 2011 +0100
@@ -2,24 +2,25 @@
   xmlns:xi="http://www.w3.org/2001/XInclude"
   py:strip="">
   
-  <py:def function="page_title">${c.grouptitle or c.groupname} - Authorization - Groups</py:def>
-  <py:def function="page_heading">Authorization: ${c.grouptitle or c.groupname}</py:def>
-
+  <py:def function="page_title">Authorization: ${c.group.display_name}</py:def>
+  <py:def function="page_heading">Authorization: ${c.group.display_name}</py:def>
+  
   <div py:match="content"><h3>Update Existing Roles</h3><form id="theform" method="POST">
       ${authz_form_table('theform', c.roles, c.users, c.user_role_dict)}
-      <button type="submit" name="save">
-        Save
+      <button type="submit" name="save" class="pretty-button primary">
+        Save Changes
       </button>
+      <div class="clear"></div></form><h3>Add Roles for Any User</h3>
-
     <form id="addform" method="POST">
       ${authz_add_table(c.roles)}
-      <button type="submit" name="add"> Add </button>
+      <button type="submit" name="add" class="pretty-button primary">Add Role</button>
+      <div class="clear"></div></form><hr/>
@@ -28,16 +29,16 @@
 
     <form id="authzgroup_form" method="POST">
       ${authz_form_group_table('authzgroup_form', c.roles, c.authz_groups, c.authz_groups_role_dict)}
-      <button type="submit" name="authz_save">
-        Save
-      </button>
+      <button type="submit" name="authz_save" class="pretty-button primary">Save Changes</button>
+      <div class="clear"></div></form><h3>Add Roles for Any Authorization Group</h3><form id="authzgroup_addform" method="POST">
       ${authz_add_group_table(c.roles)}
-      <button type="submit" name="authz_add"> Add </button>
+      <button type="submit" name="authz_add" class="pretty-button primary">Add Role</button>
+      <div class="clear"></div></form></div>


--- a/ckan/templates/group/edit.html	Mon Oct 17 10:03:54 2011 +0100
+++ b/ckan/templates/group/edit.html	Mon Oct 17 11:29:24 2011 +0100
@@ -2,8 +2,8 @@
   xmlns:xi="http://www.w3.org/2001/XInclude"
   py:strip="">
   
-  <py:def function="page_title">${c.grouptitle or c.groupname} - Edit - Groups</py:def>
-  <py:def function="page_heading">Edit: ${c.grouptitle or c.groupname}</py:def>
+  <py:def function="page_title">Edit: ${c.group.display_name}</py:def>
+  <py:def function="page_heading">Edit: ${c.group.display_name}</py:def><div py:match="content">


--- a/ckan/templates/group/history.html	Mon Oct 17 10:03:54 2011 +0100
+++ b/ckan/templates/group/history.html	Mon Oct 17 11:29:24 2011 +0100
@@ -2,22 +2,19 @@
   xmlns:xi="http://www.w3.org/2001/XInclude"
   py:strip="">
   
-  <py:def function="page_title">${c.group_dict['display_name']} - Groups - History</py:def>
+  <py:def function="page_title">History: ${c.group.display_name}</py:def>
+  <py:def function="page_heading">History: ${c.group.display_name}</py:def><div py:match="content" class="group">
-    <h2 class="head">
-      ${c.group_dict['display_name']}
-    </h2>
-
     <h3>
       Revisions
-      <p class="atom-feed-link group-history-link">
+      <!--p class="atom-feed-link group-history-link"><a
           href="${url(controller='group', action='history', id=c.group_dict['name'], format='atom', days=7)}"
           title="${g.site_title} - Group History - ${c.group_dict['name']}"
           >
           Subscribe »</a>
-      </p>
+      </p--></h3><form id="group-revisions" action="diff" method="post"
       xmlns:py="http://genshi.edgewall.org/"
@@ -49,7 +46,7 @@
           </tr></py:for></table>
-      ${h.submit('diff', _('Compare »'))}
+      <input type="submit" name="diff" value="${_('Compare »')}" class="pretty-button primary" /></form></div><!-- content -->
 


--- a/ckan/templates/group/index.html	Mon Oct 17 10:03:54 2011 +0100
+++ b/ckan/templates/group/index.html	Mon Oct 17 11:29:24 2011 +0100
@@ -5,16 +5,19 @@
   
   <py:def function="page_title">Groups of Datasets</py:def><py:def function="page_heading">Groups of Datasets</py:def>
+
+  <py:match path="primarysidebar">
+    <li class="widget-container boxed widget_text">
+      <h3>What Are Groups?</h3>
+      <span i18n:msg="">Whilst tags are great at collecting datasets together, there are occasions when you want to restrict users from editing a collection. A <strong>group</strong> can be set-up to specify which users have permission to add or remove datasets from it.</span>
+    </li>
+  </py:match>
+
   
   <div py:match="content">
-
     ${c.page.pager()}
     ${group_list_from_dict(c.page.items)}
     ${c.page.pager()}
-    
-    <py:choose test="">
-
-    </py:choose></div><xi:include href="layout.html" />


--- a/ckan/templates/group/layout.html	Mon Oct 17 10:03:54 2011 +0100
+++ b/ckan/templates/group/layout.html	Mon Oct 17 11:29:24 2011 +0100
@@ -6,36 +6,29 @@
   py:strip=""
   >
 
-  <py:match path="primarysidebar">
-    <li class="widget-container boxed widget_text">
-      <h3>Groups section</h3>
-      <p i18n:msg="">Whilst tags are great at collecting datasets together, there are occasions when you want to restrict users from editing a collection. A <strong>group</strong> can be set-up to specify which users have permission to add or remove datasets from it.</p>
-      <p>
-	<span class="ckan_logged_in" style="display: none;" i18n:msg="">
-	  To create a new group, please first <a href="${h.url_for(controller='user',action='login', id=None)}">login</a>.
-	</span>
-	<span class="ckan_logged_out">
-	  <a href="${h.url_for(controller='group',action='new', id=None)}">Create a new group</a>
-	</span>
-      </p>
-    </li>
-  </py:match>
-
-  <py:match path="minornavigation" py:if="c.group">
-  <ul class="tabbed">
-    <li py:attrs="{'class':'current-tab'} if c.action=='read' else {}">${h.subnav_link(c, h.icon('group') + _('View'), controller='group', action='read', id=c.group.name)}</li>
-    <li py:attrs="{'class':'current-tab'} if c.action=='edit' else {}" py:if="h.check_access('group_update',{'id':c.group.id})">
-      ${h.subnav_link(c, h.icon('group_edit') + _('Edit'), controller='group', action='edit', id=c.group.name)}
-    </li>
-    <li py:attrs="{'class':'current-tab'} if c.action=='history' else {}">${h.subnav_link(c, h.icon('page_white_stack') + _('History'), controller='group', action='history', id=c.group.name)}</li>
-    <li py:attrs="{'class':'current-tab'} if c.action=='authz' else {}" py:if="h.check_access('group_edit_permissions',{'id':c.group.id})">
-      ${h.subnav_link(c, h.icon('lock') + _('Authorization'), controller='group', action='authz', id=c.group.name)}
-    </li>
-    <li class="action">
-    ${h.subnav_link(c, h.icon('atom_feed') + _('Subscribe'),
-    controller='group', action='history', id=c.group.name, format='atom', days=7)}
-    </li>
-  </ul>
+  <py:match path="minornavigation">
+    <ul py:if="c.group" class="tabbed">
+      <li py:attrs="{'class':'current-tab'} if c.action=='read' else {}">${h.subnav_link(c, h.icon('group') + _('View'), controller='group', action='read', id=c.group.name)}</li>
+      <li py:attrs="{'class':'current-tab'} if c.action=='edit' else {}" py:if="h.check_access('group_update',{'id':c.group.id})">
+        ${h.subnav_link(c, h.icon('group_edit') + _('Edit'), controller='group', action='edit', id=c.group.name)}
+      </li>
+      <li py:attrs="{'class':'current-tab'} if c.action=='history' else {}">${h.subnav_link(c, h.icon('page_white_stack') + _('History'), controller='group', action='history', id=c.group.name)}</li>
+      <li py:attrs="{'class':'current-tab'} if c.action=='authz' else {}" py:if="h.check_access('group_edit_permissions',{'id':c.group.id})">
+        ${h.subnav_link(c, h.icon('lock') + _('Authorization'), controller='group', action='authz', id=c.group.name)}
+      </li>
+      <!-- li class="action">
+      ${h.subnav_link(c, h.icon('atom_feed') + _('Subscribe'),
+      controller='group', action='history', id=c.group.name, format='atom', days=7)}
+      </li-->
+    </ul>
+    <ul py:if="not c.group" class="tabbed">
+      <li py:attrs="{'class':'current-tab'} if c.action=='index' else {}">
+        ${h.subnav_link(c, h.icon('group') + _('List Groups'), controller='group', action='index')}
+      </li>
+      <li py:attrs="{'class':'current-tab'} if c.action=='new' else {}">
+        ${h.subnav_link(c, h.icon('group_add') + _('Add a Group'), controller='group', action='new')}
+      </li>
+    </ul></py:match><xi:include href="../layout.html" />


--- a/ckan/templates/group/new.html	Mon Oct 17 10:03:54 2011 +0100
+++ b/ckan/templates/group/new.html	Mon Oct 17 11:29:24 2011 +0100
@@ -2,8 +2,8 @@
   xmlns:xi="http://www.w3.org/2001/XInclude"
   py:strip="">
   
-  <py:def function="page_title">New - Groups</py:def>
-  <py:def function="page_heading">New Group</py:def>
+  <py:def function="page_title">Add A Group</py:def>
+  <py:def function="page_heading">Add A Group</py:def><div py:match="content">
     ${Markup(c.form)}


--- a/ckan/templates/group/new_group_form.html	Mon Oct 17 10:03:54 2011 +0100
+++ b/ckan/templates/group/new_group_form.html	Mon Oct 17 11:29:24 2011 +0100
@@ -12,23 +12,34 @@
 </ul></div>
 
-<fieldset>
-  <legend>Details</legend>
+<fieldset id="basic-information"><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>
+    <dt><label class="field_opt" for="name">Title</label></dt>
+    <dd><input class="js-title" id="title" name="title" type="text" value="${data.get('title', '')}"/></dd>
+
+    <dt><label class="field_opt" for="title">Url</label></dt>
+    <dd class="name-field">
+      <span class="url-text">${g.site_url+h.url_for(controller='group',id=None)+'/'}<span class="js-url-viewmode js-url-suffix"> </span><a style="display: none;" href="#" class="url-edit js-url-editlink js-url-viewmode">(edit)</a></span>
+      <input style="display: none;" id="name" maxlength="100" name="name" type="text" class="url-input js-url-editmode js-url-input" value="${data.get('name', '')}" />
+      <p class="js-url-is-valid"> </p>
+    </dd>
+    <dd style="display: none;" class="js-url-editmode instructions basic">2+ chars, lowercase, using only 'a-z0-9' and '-_'</dd><dd class="field_error" py:if="errors.get('name', '')">${errors.get('name', '')}</dd>
 
-    <dt><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 class="description-label"><label class="field_opt" for="title">Description</label></dt>
+    <dd class="description-field"><div class="markdown-editor">
+      <ul class="button-row">
+        <li><button class="pretty-button js-markdown-edit depressed">Edit</button></li>
+        <li><button class="pretty-button js-markdown-preview">Preview</button></li>
+      </ul>
+      <textarea class="markdown-input" name="description" id="notes" placeholder="${_('Start with a summary sentence ...')}">${data.get('description','')}</textarea>
+      <div class="markdown-preview" style="display: none;"></div>
+      <span class="hints">You can use <a href="http://daringfireball.net/projects/markdown/syntax" target="_blank">Markdown formatting</a> here.</span>
+    </div></dd>
 
-    <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">
+    <dt class="state-label" py:if="c.is_sysadmin or c.auth_for_change_state"><label class="field_opt" for="state">State</label></dt>
+    <dd class="state-field" py:if="c.is_sysadmin or c.auth_for_change_state"><select id="state" name="state" ><option py:attrs="{'selected': 'selected' if data.get('state') == 'active' else None}" value="active">active</option><option py:attrs="{'selected': 'selected' if data.get('state') == 'deleted' else None}" value="deleted">deleted</option>
@@ -37,8 +48,8 @@
   </dl></fieldset>
   
-<fieldset>
-  <legend>Extras</legend>
+<fieldset id="extras">
+  <h3>Extras</h3><dl><py:with vars="extras = data.get('extras', [])"><py:for each="num, extra in enumerate(data.get('extras', []))">
@@ -62,8 +73,8 @@
   </dl></fieldset>
 
-<fieldset>
-  <legend>Datasets</legend>
+<fieldset id="datasets">
+  <h3>Datasets</h3><dl py:if="data.get('packages')"><py:for each="num, package in enumerate(data.get('packages'))"><dt><input checked="checked" id="datasets__${num}__name" name="packages__${num}__name" type="checkbox" value="${package['name']}"/></dt>
@@ -73,18 +84,18 @@
     </py:for></dl><p py:if="not data.get('packages')">There are no datasets currently in this group.</p>
-</fieldset>
 
-<fieldset>
-  <legend>
-    Add datasets
-  </legend>
+  <h3>Add datasets</h3><dl><dt><label class="field_opt" for="packages__${len(data.get('packages', []))}__name">Dataset</label></dt><dd><input class="autocomplete-dataset" id="datasets__${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" />
+<div class="form-submit">
+  <input id="save" class="pretty-button primary" name="save" type="submit" value="${_('Save Changes')}" />
+  <py:if test="c.group">
+    <input id="cancel" class="pretty-button href-action" name="cancel" type="reset" value="${_('Cancel')}" action="${h.url_for(controller='group', action='read', id=c.group.name)}" />
+  </py:if>
+</div></form>


--- a/ckan/templates/group/read.html	Mon Oct 17 10:03:54 2011 +0100
+++ b/ckan/templates/group/read.html	Mon Oct 17 11:29:24 2011 +0100
@@ -3,21 +3,12 @@
   xmlns:xi="http://www.w3.org/2001/XInclude"
   py:strip="">
   
-  <py:def function="page_title">${c.group.display_name} - Groups</py:def>
+  <py:def function="page_title">${c.group.display_name}</py:def>
+  <py:def function="page_heading">${c.group.display_name}</py:def>
 
-  <py:def function="page_heading">
-      <py:if test="not c.group.title">
-        <em>No Title</em>
-      </py:if>
-      ${c.group.title}
-  </py:def>
-  
   <py:match path="primarysidebar"><li class="widget-container widget_text">
-      <div class="description">
-        ${c.group_description_formatted}
-      </div><ul class="property-list"><py:if test="c.group_admins"><li>
@@ -32,6 +23,9 @@
   </py:match><py:match path="content">
+    <div class="notes" py:if="str(c.group_description_formatted).strip()">
+      ${c.group_description_formatted}
+    </div><h3>Datasets:</h3><p i18n:msg="item_count">There are ${c.page.item_count} datasets in this group.</p>
     ${c.page.pager()}


--- a/ckan/templates/js_strings.html	Mon Oct 17 10:03:54 2011 +0100
+++ b/ckan/templates/js_strings.html	Mon Oct 17 11:29:24 2011 +0100
@@ -14,13 +14,17 @@
    * Used in application.js.
    */
   CKAN.Strings.helloWorld = "${_('Hello there, world!')}";
-  CKAN.Strings.datasetNameAvailable = "${_('This dataset name is available!')}";
-  CKAN.Strings.datasetNameNotAvailable = "${_('This dataset name is already used, please use a different name')}";
+  CKAN.Strings.checking = "${_('Checking...')}";
+  CKAN.Strings.urlIsAvailable = "${_('This URL is available!')}";
+  CKAN.Strings.urlIsNotAvailable = "${_('This URL is already used, please use a different one.')}";
   CKAN.Strings.bracketsNone = "${_('(none)')}";
   CKAN.Strings.failedToSave = "${_('Failed to save, possibly due to invalid data ')}";
   CKAN.Strings.addDataset = "${_('Add Dataset')}";
+  CKAN.Strings.addGroup = "${_('Add Group')}";
   CKAN.Strings.youHaveUnsavedChanges = "${_('You have unsaved changes. Hit Save Changes at the bottom of the page to submit them.')}";
   CKAN.Strings.loading = "${_('Loading...')}";
+  CKAN.Strings.noNameBrackets = "${_('(no name)')}";
+  CKAN.Strings.deleteThisResourceQuestion = "${_('Delete the resource \'%name%\'?')}"
 
   /*
    * Used in templates.js.
@@ -41,7 +45,7 @@
   CKAN.Strings.mimetypeInner = "${_('Mimetype (Inner)')}";
   CKAN.Strings.hash = "${_('Hash')}";
   CKAN.Strings.id = "${_('ID')}";
-  CKAN.Strings.deleteResource = "${_('Delete Resource')}";
+  CKAN.Strings.doneEditing = "${_('Done')}";
   CKAN.Strings.resourceHasUnsavedChanges = "${_('This resource has unsaved changes.')}";
   
 


--- a/ckan/templates/layout_base.html	Mon Oct 17 10:03:54 2011 +0100
+++ b/ckan/templates/layout_base.html	Mon Oct 17 11:29:24 2011 +0100
@@ -81,7 +81,7 @@
       </header></div><py:with vars="messages = list(h._flash.pop_messages())">
-    <div class="class container" py:if="len(messages)">
+    <div class="flash-messages container"><div class="${m.category}" py:for="m in messages">
         ${h.literal(m)}
       </div>
@@ -215,7 +215,6 @@
   <!-- TODO should not be necessary; we use AJAX to produce consistent previews --><script type="text/javascript" src="${g.site_url}/scripts/vendor/jquery.fileupload/20110801/jquery.iframe-transport.js"></script><script type="text/javascript" src="${g.site_url}/scripts/vendor/jquery.fileupload/20110801/jquery.fileupload.js"></script>
-  <script src="https://raw.github.com/okfn/ckanjs/master/pkg/ckanjs.js"></script><script type="text/javascript" src="${g.site_url}/scripts/ckanjs.js"></script><!-- Translated js strings live inside an html template. --><xi:include href="js_strings.html" />


--- a/ckan/templates/package/authz.html	Mon Oct 17 10:03:54 2011 +0100
+++ b/ckan/templates/package/authz.html	Mon Oct 17 11:29:24 2011 +0100
@@ -2,7 +2,7 @@
   xmlns:xi="http://www.w3.org/2001/XInclude"
   py:strip="">
   
-  <py:def function="page_title">${c.pkgtitle or c.pkgname} - Authorization - Datasets</py:def>
+  <py:def function="page_title">Authorization: ${c.pkgtitle or c.pkgname}</py:def><py:def function="page_heading">Authorization: ${c.pkgtitle or c.pkgname}</py:def><div py:match="content">
@@ -10,16 +10,17 @@
 
     <form id="theform" method="POST">
       ${authz_form_table('theform', c.roles, c.users, c.user_role_dict)}
-      <button type="submit" name="save">
-        Save
+      <button type="submit" name="save" class="pretty-button primary">
+        Save Changes
       </button>
+      <div class="clear"></div></form><h3>Add Roles for Any User</h3>
-
     <form id="addform" method="POST">
       ${authz_add_table(c.roles)}
-      <button type="submit" name="add"> Add </button>
+      <button type="submit" name="add" class="pretty-button primary">Add Role</button>
+      <div class="clear"></div></form><hr/>
@@ -28,16 +29,16 @@
 
     <form id="authzgroup_form" method="POST">
       ${authz_form_group_table('authzgroup_form', c.roles, c.authz_groups, c.authz_groups_role_dict)}
-      <button type="submit" name="authz_save">
-        Save
-      </button>
+      <button type="submit" name="authz_save" class="pretty-button primary">Save Changes</button>
+      <div class="clear"></div></form><h3>Add Roles for Any Authorization Group</h3><form id="authzgroup_addform" method="POST">
       ${authz_add_group_table(c.roles)}
-      <button type="submit" name="authz_add"> Add </button>
+      <button type="submit" name="authz_add" class="pretty-button primary">Add Role</button>
+      <div class="clear"></div></form></div>


--- a/ckan/templates/package/edit.html	Mon Oct 17 10:03:54 2011 +0100
+++ b/ckan/templates/package/edit.html	Mon Oct 17 11:29:24 2011 +0100
@@ -19,9 +19,9 @@
       <ul class="edit-form-navigation"><!-- One button for each fieldset --><li><a href="#section-basic-information">Basic Information</a></li>
+        <li><a href="#section-further-information">Futher Information</a></li><li><a href="#section-resources">Resources</a></li>
-        <li><a href="#section-groups">Groups</a></li>
-        <li><a href="#section-detail">Details</a></li>
+        <li><a href="#section-groups">Groups & Tags</a></li><li><a href="#section-extras">Extras</a></li></ul></li>


--- a/ckan/templates/package/history.html	Mon Oct 17 10:03:54 2011 +0100
+++ b/ckan/templates/package/history.html	Mon Oct 17 11:29:24 2011 +0100
@@ -7,7 +7,7 @@
   <py:def function="page_heading">History: ${c.pkg.title or c.pkg.name}</py:def><!-- Sidebar -->
-  <py:match path="primarysidebar">
+  <!--py:match path="primarysidebar"><li class="widget-container widget_text"><h4>Updates</h4><p class="atom-feed-link dataset-history-link">
@@ -18,6 +18,7 @@
         </p></li></py:match>
+  --><div py:match="content" class="dataset"><h3>Revisions</h3>
@@ -52,7 +53,7 @@
           </tr></py:for></table>
-      ${h.submit('diff', _('Compare »'))}
+      <input type="submit" name="diff" value="${_('Compare »')}" class="pretty-button primary" /></form></div><!-- content -->
 


--- a/ckan/templates/package/layout.html	Mon Oct 17 10:03:54 2011 +0100
+++ b/ckan/templates/package/layout.html	Mon Oct 17 11:29:24 2011 +0100
@@ -16,10 +16,10 @@
       <li py:attrs="{'class':'current-tab'} if c.action=='authz' else {}" py:if="h.check_access('package_edit_permissions',{'id':c.pkg.id})">
         ${h.subnav_link(c, h.icon('lock') + _('Authorization'), controller='package', action='authz', id=c.pkg.name)}
       </li>
-      <li class="action">
+      <!--li class="action">
         ${h.subnav_link(c, h.icon('atom_feed') + _('Subscribe'),
         controller='package', action='history', id=c.pkg.name, format='atom', days=7)}
-      </li>
+      </li--></ul></py:if></py:match>


--- a/ckan/templates/package/new.html	Mon Oct 17 10:03:54 2011 +0100
+++ b/ckan/templates/package/new.html	Mon Oct 17 11:29:24 2011 +0100
@@ -8,16 +8,6 @@
 
   <py:def function="body_class">hide-sidebar</py:def>
 
-  <py:def function="optional_footer">
-    <!-- Auto-generate 'name' field -->
-    <script type="text/javascript">
-      jQuery(document).ready(function($) {
-        CKAN.Utils.PackageSlugCreator.create($('#title'), $('#name'));
-        $("#title").focus();
-      });
-    </script>
-  </py:def>
-
   <div py:match="content"><h3 py:if="c.error" class="form-errors">
       Error: ${c.error}


--- a/ckan/templates/package/new_package_form.html	Mon Oct 17 10:03:54 2011 +0100
+++ b/ckan/templates/package/new_package_form.html	Mon Oct 17 11:29:24 2011 +0100
@@ -31,32 +31,46 @@
     <dt class="title-label"><label class="field_opt" for="title">Title</label></dt><dd class="title-field"><input id="title"
-        tabindex="1" name="title" type="text"
+        class="js-title"
+        name="title" type="text"
         value="${data.get('title', '')}"
         placeholder="${_('A short descriptive title for the dataset')}"
       /></dd><dd class="title-instructions field_error" py:if="errors.get('title', '')">${errors.get('title', '')}</dd>
 
-    <dt class="name-label"><label class="field_req" for="name">Slug *</label></dt>
-    <dd class="name-field"><input id="name" tabindex="999" maxlength="100" name="name" type="text" value="${data.get('name', '')}" /></dd>
-    <dd class="name-instructions instructions basic">A unique identifier used in urls. Renaming is possible but discouraged.</dd>
-    <dd class="name-instructions hints">2+ characters, lowercase, using only 'a-z0-9' and '-_'</dd>
-    <dd class="name-instructions field_error" py:if="errors.get('name', '')">${errors.get('name', '')}</dd>
+    <dt class="name-label"><label class="field_req" for="name">Url</label></dt>
+    <dd class="name-field">
+      <span class="url-text">${g.site_url+h.url_for(controller='package', id=None)+'/'}<span class="js-url-viewmode js-url-suffix"> </span><a href="#" style="display: none;" class="url-edit js-url-editlink js-url-viewmode">(edit)</a></span>
+      <input style="display: none;" id="name" maxlength="100" name="name" type="text" class="url-input js-url-editmode js-url-input" value="${data.get('name', '')}" />
+      <p class="js-url-is-valid"> </p>
+    </dd>
+    <dd style="display: none;" class="js-url-editmode name-instructions basic">2+ characters, lowercase, using only 'a-z0-9' and '-_'</dd>
+    <dd class="js-url-editmode name-instructions field_error" py:if="errors.get('name', '')">${errors.get('name', '')}</dd><dt class="homepage-label"><label class="field_opt" for="url">Home Page</label></dt>
-    <dd class="homepage-field"><input id="url" tabindex="2" name="url" type="text" value="${data.get('url', '')}"/></dd>
+    <dd class="homepage-field"><input id="url" name="url" type="text" value="${data.get('url', '')}"/></dd><dd class="homepage-instructions instructions basic">The URL for the web page describing the data (not the data itself).</dd><dd class="homepage-instructions hints">e.g. http://www.example.com/growth-figures.html</dd><dd class="homepage-instructions field_error" py:if="errors.get('url', '')">${errors.get('url', '')}</dd>
 
+    <dt class="license-label"><label class="field_opt" for="license_id">License</label></dt>
+    <dd class="license-field">
+      <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="license-instructions instructions basic">The licence under which the dataset is released.</dd>
+
     <dt class="description-label"><label class="field_opt" for="notes">Description</label></dt><dd class="description-field"><div class="markdown-editor">
-      <ul class="tabs">
-        <li><a href="#" action="write" class="selected">Write</a></li>
-        <li><a href="#" action="preview">Preview</a></li>
+      <ul class="button-row">
+        <li><button class="pretty-button js-markdown-edit depressed">Edit</button></li>
+        <li><button class="pretty-button js-markdown-preview">Preview</button></li></ul>
-      <textarea class="markdown-input" tabindex="3" name="notes" id="notes" placeholder="${_('Start with a summary sentence ...')}">${data.get('notes','')}</textarea>
+      <textarea class="markdown-input" name="notes" id="notes" placeholder="${_('Start with a summary sentence ...')}">${data.get('notes','')}</textarea><div class="markdown-preview" style="display: none;"></div><span class="hints">You can use <a href="http://daringfireball.net/projects/markdown/syntax" target="_blank">Markdown formatting</a> here.</span><!--
@@ -64,86 +78,35 @@
       <dd class="instructions further">It is often displayed with the dataset title. In particular, it should start with a short sentence that describes the dataset succinctly, because the first few words alone may be used in some views of the datasets.</dd>
       --></div></dd>
-
-    <dt class="license-label"><label class="field_opt" for="license_id">License</label></dt>
-    <dd class="license-field">
-      <select id="license_id" tabindex="4" 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="license-instructions instructions basic">The licence under which the dataset is released.</dd>
-
-    <dt class="tags-label"><label class="field_opt" for="tags">Tags</label></dt>
-    <dd class="tags-field">
-      <input class="long autocomplete-tag" tabindex="5" 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="tags-instructions instructions basic" i18n:msg="">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="tags-instructions hints">e.g. pollution rivers water-quality</dd>
-    <dd class="tags-instructions field_error" py:if="errors.get('tag_string', '')">${errors.get('tag_string', '')}</dd></dl></fieldset><fieldset id="resources"><div class="instructions basic"><h3>Resources: the files and APIs associated with this dataset</h3></div>
-  <table class="resource-table edit">
+  <table class="resource-table-edit"><thead><tr>
-        <th class="resource-expand-link"></th>
-        <th class="field_req resource-url">URL</th>
-        <th class="field_opt resource-description">Name</th>
-        <th class="field_opt resource-format">Format</th>
-        <th class="field_opt resource-is-changed"></th>
+        <th class="field_req resource-url">Resource</th>
+        <th class="resource-delete-link"></th></tr></thead>
-    <tbody class="resource-table">
-      <py:for each="num, res in enumerate(data.get('resources', []))">
-      <tr class="resource-summary">
-        <td class="resource-expand-link">
-          <a class="resource-expand-link" href="#"><img src="/images/icons/edit-expand.png" /></a>
-          <a class="resource-expand-link" href="#" style="display: none;"><img src="/images/icons/edit-collapse.png" /></a>
-        </td>
-        <td class="resource-summary resource-url">
-          ${res.get('url', '')}
-        </td>
-        <td class="resource-summary resource-name">
-          ${res.get('name', '')}
-        </td>
-        <td class="resource-summary resource-format">
-          ${res.get('format', '')}
-        </td>
-        <td class="resource-expanded" colspan="3" style="display: none;">
-          <!-- Replaced with a javascript template -->
-          <input name="resources__${num}__url" type="text" value="${res.get('url', '')}" />
-          <input name="resources__${num}__format" type="text" value="${res.get('format', '')}" />
-          <input name="resources__${num}__description" type="text" value="${res.get('description', '')}" />
-          <input name="resources__${num}__hash" type="text" value="${res.get('hash', '')}" />
-          <input name="resources__${num}__id" type="hidden" value="${res.get('id', '')}" />
-        </td>
-        <td class="resource-is-changed">
-        </td>
-      </tr>
-      </py:for>
-      <py:if test="not len(data.get('resources', []))">
-        <tr class="table-empty"><td colspan="5">(none)</td></tr>
-      </py:if>
+    <tbody class="js-resource-editor"></tbody></table><div class="resource-add">
-    <ul class="tabs">
+    <ul class="button-row"><li><h4>Add a resource:</h4></li>
-      <li><a href="#" action="link-file" class="action-resource-tab">Link to a file</a></li>
-      <li><a href="#" action="link-api" class="action-resource-tab">Link to an API</a></li>
-      <li class="upload-file" style="display:none;"><a href="#" action="upload-file" class="action-resource-tab">Upload a file</a></li>
+      <li><button class="pretty-button js-link-file">Link to a file</button></li>
+      <li><button class="pretty-button js-link-api">Link to an API</button></li>
+      <li class="js-upload-file" style="display:none;"><button class="pretty-button js-upload-file">Upload a file</button></li></ul></div></fieldset><fieldset id="groups">
+  <h3>Groups</h3><dl><py:for each="num, group in enumerate(data.get('groups', []))"><?python
@@ -167,10 +130,21 @@
         </py:for></select></dd> 
-    <dd py:if="not c.groups_available">Cannot add any groups.</dd>
+    <dd py:if="not c.groups_available"><em>Cannot add any groups.</em></dd>
+  </dl>
+  <h3>Tags</h3>
+  <dl>
+    <dt class="tags-label"><label class="field_opt" for="tags">Tags</label></dt>
+    <dd class="tags-field">
+      <input class="long autocomplete-tag" 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="tags-instructions instructions basic" i18n:msg="">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="tags-instructions hints">e.g. pollution rivers water-quality</dd>
+    <dd class="tags-instructions field_error" py:if="errors.get('tag_string', '')">${errors.get('tag_string', '')}</dd></dl></fieldset>
-<fieldset id='detail'>
+<fieldset id='further-information'><dl><dt><label class="field_opt" for="author">Author</label></dt><dd><input id="author" name="author" type="text" value="${data.get('author', '')}" /></dd>
@@ -241,9 +215,9 @@
 </div><div class="form-submit">
-  <input id="save" tabindex="99" class="pretty-button primary" name="save" type="submit" value="${_('Save Changes')}" />
+  <input id="save" class="pretty-button primary" name="save" type="submit" value="${_('Save Changes')}" /><py:if test="c.pkg">
-    <input id="cancel" tabindex="100" class="pretty-button href-action" name="cancel" type="reset" value="${_('Cancel')}" action="${h.url_for(controller='package', action='read', id=c.pkg.name)}" />
+    <input id="cancel" class="pretty-button href-action" name="cancel" type="reset" value="${_('Cancel')}" action="${h.url_for(controller='package', action='read', id=c.pkg.name)}" /></py:if><p i18n:msg="" class="hints"><strong>Important:</strong> By submitting content, you agree to release your contributions under the <a href="http://opendatacommons.org/licenses/odbl/1.0/">Open Database License</a>. Please <strong>refrain</strong> from editing this page if you are <strong>not</strong> happy to do this.


--- a/ckan/templates/package/read.html	Mon Oct 17 10:03:54 2011 +0100
+++ b/ckan/templates/package/read.html	Mon Oct 17 11:29:24 2011 +0100
@@ -49,14 +49,11 @@
         <li><h3>Tags</h3>
           ${tag_list(c.pkg_dict.get('tags', ''))}
-          <span class="widget_action" py:if="h.check_access('package_update',{'id':c.pkg.id})">
-            ${h.subnav_link(c, 'add tags »', controller='package', action='edit', id=c.pkg.name)}
-          </span></li><li><h3>Groups</h3><py:if test="c.pkg.groups">        
-              <ul>
+              <ul class="groups"><li py:for="group in sorted(c.pkg.groups, key=lambda g: g.display_name)"><a href="${h.url_for(controller='group', action='read', id=group.name)}">${group.display_name}</a></li>
@@ -66,11 +63,6 @@
             <py:if test="not c.pkg.groups">
                Groups are collections of dataset maintained by users of ${g.site_title}. This dataset has not been added to any groups yet.
             </py:if>
-
-
-            <p class="widget_action" py:if="h.check_access('package_update',{'id':c.pkg.id})">
-              ${h.subnav_link(c, 'add to a group »', controller='package', action='edit', id=c.pkg.name)}        
-            </p></p></li><li py:if="h.check_access('package_update',{'id':c.pkg.id})">


--- a/ckan/templates/package/read_core.html	Mon Oct 17 10:03:54 2011 +0100
+++ b/ckan/templates/package/read_core.html	Mon Oct 17 11:29:24 2011 +0100
@@ -13,7 +13,7 @@
     <!-- Resources --><div class="resources subsection"><h3>Resources</h3>
-      <table>
+      <table class="resource-table-view"><thead><th>Resource</th><th>Format</th>


--- a/ckan/templates/revision/list.html	Mon Oct 17 10:03:54 2011 +0100
+++ b/ckan/templates/revision/list.html	Mon Oct 17 11:29:24 2011 +0100
@@ -9,10 +9,10 @@
     <ul class="tabbed"><li class="current-tab">
         ${h.subnav_link(c,_('Home'), controller='revision', action='index')}</li>
-      <li class="action">
+      <!--li class="action">
       ${h.subnav_link(c, h.icon('atom_feed') + _('Subscribe'),
       controller='revision', action='index', format='atom', days=1)}
-      </li>
+      </li--></ul></py:match>
 


--- a/ckan/templates/user/edit_user_form.html	Mon Oct 17 10:03:54 2011 +0100
+++ b/ckan/templates/user/edit_user_form.html	Mon Oct 17 11:29:24 2011 +0100
@@ -25,9 +25,9 @@
     <dt><label for="about">About:</label></dt><dd class="description-field"><div class="markdown-editor">
-        <ul class="tabs">
-          <li><a href="#" action="write" class="selected">Write</a></li>
-          <li><a href="#" action="preview">Preview</a></li>
+        <ul class="button-row">
+          <li><button class="pretty-button js-markdown-edit depressed">Edit</button></li>
+          <li><button class="pretty-button js-markdown-preview">Preview</button></li></ul><textarea class="markdown-input" tabindex="3" name="about" id="about" placeholder="${_('A little about you...')}">${data.get('about','')}</textarea><div class="markdown-preview" style="display: none;"></div>


--- a/ckan/tests/functional/api/test_ajax.py	Mon Oct 17 10:03:54 2011 +0100
+++ b/ckan/tests/functional/api/test_ajax.py	Mon Oct 17 11:29:24 2011 +0100
@@ -14,15 +14,28 @@
     def teardown(cls):
         model.repo.rebuild_db()
         
-    def test_package_create_slug(self):
+    def test_package_slug_valid(self):
+        CreateTestData.create()
         response = self.app.get(
-            url=url_for(controller='api', action='create_slug'),
+            url=url_for(controller='api', action='is_slug_valid'),
             params={
-               'title': u'A New Title * With & Funny CHARacters',
+               'type': u'package',
+               'slug': u'A New Title * With & Funny CHARacters',
             },
             status=200,
         )
-        assert_equal(response.body, '{"valid": true, "name": "a-new-title-with-funny-characters"}')
+        assert_equal(response.body, '{"valid": true}')
+        assert_equal(response.header('Content-Type'), 'application/json;charset=utf-8')
+
+        response = self.app.get(
+            url=url_for(controller='api', action='is_slug_valid'),
+            params={
+               'type': u'package',
+               'slug': u'warandpeace',
+            },
+            status=200,
+        )
+        assert_equal(response.body, '{"valid": false}')
         assert_equal(response.header('Content-Type'), 'application/json;charset=utf-8')
 
     def test_tag_autocomplete(self):


--- a/ckan/tests/functional/test_group.py	Mon Oct 17 10:03:54 2011 +0100
+++ b/ckan/tests/functional/test_group.py	Mon Oct 17 11:29:24 2011 +0100
@@ -115,7 +115,7 @@
     def test_new_page(self):
         offset = url_for(controller='group', action='new')
         res = self.app.get(offset, extra_environ={'REMOTE_USER': 'russianfan'})
-        assert 'Create a new group' in res, res
+        assert 'Add A Group' in res, res
 
 
 class TestEdit(FunctionalTestCase):
@@ -247,10 +247,10 @@
         group_title = u'Test Title'
         group_description = u'A Description'
 
-        # Open 'new group' page
+        # Open 'Add A Group' page
         offset = url_for(controller='group', action='new')
         res = self.app.get(offset, status=200, extra_environ={'REMOTE_USER': 'russianfan'})
-        assert 'New Group' in res, res
+        assert 'Add A Group' in res, res
         fv = res.forms['group-edit']
         assert fv[prefix+'name'].value == '', fv.fields
         assert fv[prefix+'title'].value == ''
@@ -282,21 +282,20 @@
         group_name = u'testgrp1'
         offset = url_for(controller='group', action='new')
         res = self.app.get(offset, status=200, extra_environ={'REMOTE_USER': 'russianfan'})
-        assert 'New Group' in res, res
+        assert 'Add A Group' in res, res
         fv = res.forms['group-edit']
         assert fv[prefix+'name'].value == '', fv.fields
         fv[prefix+'name'] = group_name
         res = fv.submit('save', status=302, extra_environ={'REMOTE_USER': 'russianfan'})
         res = res.follow()
         assert group_name in res, res
-        assert 'No Title' in res, res
         model.Session.remove()
 
         # Create duplicate group
         group_name = u'testgrp1'
         offset = url_for(controller='group', action='new')
         res = self.app.get(offset, status=200, extra_environ={'REMOTE_USER': 'russianfan'})
-        assert 'New Group' in res, res
+        assert 'Add A Group' in res, res
         fv = res.forms['group-edit']
         assert fv[prefix+'name'].value == '', fv.fields
         fv[prefix+'name'] = group_name


--- a/ckan/tests/functional/test_package.py	Mon Oct 17 10:03:54 2011 +0100
+++ b/ckan/tests/functional/test_package.py	Mon Oct 17 11:29:24 2011 +0100
@@ -148,11 +148,11 @@
         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']):
-            ### only check fields that are on the form 
-            if res_field not in ['url', 'id', 'description', 'hash']:
-                continue
-            self.check_tag(main_res, '%sresources__%i__%s' % (prefix, res_index, res_field), expected_value)
+        #for res_index, res_field, expected_value in self._get_resource_values(params['resources']):
+        #    ## only check fields that are on the form 
+        #    if res_field not in ['url', 'id', 'description', 'hash']:
+        #        continue
+        #    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)):
@@ -644,7 +644,7 @@
             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')
@@ -657,7 +657,7 @@
             pkg = model.Package.by_name(new_name)
             assert pkg.title == new_title 
             assert pkg.url == newurl
-            assert pkg.resources[0].url == new_download_url
+            #assert pkg.resources[0].url == new_download_url
             assert pkg.version == newversion
             assert newlicense_id == pkg.license.id
         finally:
@@ -1187,7 +1187,7 @@
         fv[prefix+'title'] = pkgtitle
         res = fv.submit('save')
         assert 'Error' in res, res
-        assert 'Dataset name already exists in database' in res, res
+        assert 'That URL is already in use.' in res, res
         self._assert_form_errors(res)
         
     def test_missing_fields(self):


--- a/ckan/tests/functional/test_user.py	Mon Oct 17 10:03:54 2011 +0100
+++ b/ckan/tests/functional/test_user.py	Mon Oct 17 11:29:24 2011 +0100
@@ -53,7 +53,7 @@
                                  'http://anna.com',
                                  'target="_blank"',
                                  'rel="nofollow"')
-        assert 'Edit' not in main_res, main_res
+        assert 'Edit Profile' not in main_res, main_res
         assert 'Number of edits:</strong> 3' in res, res
         assert 'Number of datasets administered:</strong> 1' in res, res
         assert 'Revision History' in res, res
@@ -81,7 +81,7 @@
         main_res = self.main_div(res)
         assert 'annafan' in res, res
         assert 'My Account' in main_res, main_res
-        assert 'Edit' in main_res, main_res
+        assert 'Edit Profile' in main_res, main_res
 
     def test_user_read_about_unfinished(self):
         user = model.User.by_name(u'unfinisher')


--- a/ckan/tests/lib/test_dictization_schema.py	Mon Oct 17 10:03:54 2011 +0100
+++ b/ckan/tests/lib/test_dictization_schema.py	Mon Oct 17 11:29:24 2011 +0100
@@ -102,9 +102,9 @@
         converted_data, errors = validate(data, default_package_schema(), context)
 
         assert errors == {
-            'name': [u'Dataset name already exists in database'],
-            'resources': [{},
-                          {'url': [u'Missing value']}]
+            'name': [u'That URL is already in use.'],
+            #'resources': [{}
+            #              {'name': [u'That URL is already in use.']}]
         }, pformat(errors)
 
         data["id"] = package_id
@@ -112,14 +112,14 @@
         converted_data, errors = validate(data, default_package_schema(), context)
 
         assert errors == {
-            'resources': [{}, {'url': [u'Missing value']}]
+            #'resources': [{}, {'url': [u'Missing value']}]
         }, pformat(errors)
 
         data['name'] = '????jfaiofjioafjij'
         converted_data, errors = validate(data, default_package_schema(), context)
         assert errors == {
-            'name': [u'Name must be purely lowercase alphanumeric (ascii) characters and these symbols: -_'],
-            'resources': [{}, {'url': [u'Missing value']}]
+            'name': [u'Url must be purely lowercase alphanumeric (ascii) characters and these symbols: -_'],
+            #'resources': [{}, {'url': [u'Missing value']}]
         },pformat(errors)
 
     def test_2_group_schema(self):


--- a/ckan/tests/lib/test_munge.py	Mon Oct 17 10:03:54 2011 +0100
+++ b/ckan/tests/lib/test_munge.py	Mon Oct 17 11:29:24 2011 +0100
@@ -1,18 +1,8 @@
 from nose.tools import assert_equal
 
-from ckan.lib.munge import munge_title_to_name, munge_name
+from ckan.lib.munge import munge_name
 
 class TestMunge:
-    def test_munge_title_to_name(self):
-        def test_munge(title, expected_munge):
-            munge = munge_title_to_name(title)
-            assert_equal(munge, expected_munge)
-
-        test_munge('Adult participation in learning', 'adult-participation-in-learning')
-        test_munge('Alcohol Profile: Alcohol-specific hospital admission, males', 'alcohol-profile-alcohol-specific-hospital-admission-males')
-        test_munge('Age and limiting long-term illness by NS-SeC', 'age-and-limiting-long-term-illness-by-ns-sec')
-        test_munge('Higher Education Statistics: HE qualifications obtained in the UK by level, mode of study, domicile, gender, class of first degree and subject area 2001/02', 'higher-education-statistics-he-qualifications-obtained-in-the-uk-by-level-mode-of-study-2001-02')        
-
     def test_munge_name(self):
         def test_munge(title, expected_munge):
             munge = munge_name(title)

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