diff --git a/ckan/controllers/package.py b/ckan/controllers/package.py index 90b9a3bff01..cb2e2cf9d64 100644 --- a/ckan/controllers/package.py +++ b/ckan/controllers/package.py @@ -261,7 +261,7 @@ def read(self, id, format='html'): try: c.pkg_dict = get_action('package_show')(context, data_dict) c.pkg = context['package'] - c.pkg_json = json.dumps(c.pkg_dict) + c.resources_json = json.dumps(c.pkg_dict.get('resources',[])) except NotFound: abort(404, _('Dataset not found')) except NotAuthorized: @@ -400,7 +400,7 @@ def new(self, data=None, errors=None, error_summary=None): data = data or clean_dict(unflatten(tuplize_dict(parse_params( request.params, ignore_keys=[CACHE_PARAMETER])))) - c.pkg_json = json.dumps(data) + c.resources_json = json.dumps(data.get('resources',[])) errors = errors or {} error_summary = error_summary or {} @@ -439,7 +439,7 @@ def edit(self, id, data=None, errors=None, error_summary=None): abort(404, _('Dataset not found')) c.pkg = context.get("package") - c.pkg_json = json.dumps(data) + c.resources_json = json.dumps(data.get('resources',[])) try: check_access('package_update',context) diff --git a/ckan/public/css/style.css b/ckan/public/css/style.css index 8e7e7e77c3d..354734e3795 100644 --- a/ckan/public/css/style.css +++ b/ckan/public/css/style.css @@ -415,8 +415,7 @@ ul.no-break li { /* ======================= */ div.form-actions p.hints { width: 50%; - float: right; - margin: 0; + margin: 10px 0 0 0; } @@ -900,13 +899,6 @@ span.extras-label { display: block; } -.dataset-editresources-form .resource-add li h4 { - display: inline; - padding-right: 20px; -} -.dataset-editresources-form .resource-add .subpane { - margin-top: 10px; -} .dataset-editresources-form .resource-add .fileinfo { margin: 7px 0; } @@ -920,7 +912,18 @@ span.extras-label { width: 400px; height: 70px; } +.resource-add > .nav { + margin-bottom: 0; + width: 522px; +} +.resource-add .tab-pane { + padding: 20px; + background: #fff; + border: 1px solid #DDD; + border-top: none; + width: 480px; +} /* ==================== */ /* = Add Dataset Page = */ @@ -933,8 +936,6 @@ span.extras-label { .dataset-create-form fieldset#resources, .dataset-create-form fieldset#summary { display: block; - padding-bottom: 0; - margin-bottom: 0; } .dataset-create-form .homepage-field, .dataset-create-form .tags-field { @@ -1224,52 +1225,33 @@ body.authz form button { /* ==================== */ /* = Activity Streams = */ /* ==================== */ - .activity-stream .activity { padding-bottom:1em; - } .activity-stream .activity a { font-weight:bold; - -} -.activity-stream .activity .actor { - } .activity-stream .activity .verb { background-color:PapayaWhip; padding:.25em; margin:.25em; - -} -.activity-stream .activity .object { - -} -.activity-stream .activity .target { - } .activity-stream .activity .date { color:#999; - } - - - - - -/* Dev */ +/* ===================== */ +/* == Resource Editor == */ +/* ===================== */ fieldset#resources { - min-height: 230px; margin-bottom: 40px; - position: relative; padding: 0; } +body.editresources fieldset#resources > legend, body.editresources fieldset#resources > .instructions { display: none; } fieldset#resources > .instructions { - width: 300px; padding: 12px 12px 2px 12px; } .resource-list { @@ -1278,114 +1260,67 @@ fieldset#resources > .instructions { margin: 0; } .resource-list li { + white-space: nowrap; + overflow: hidden; background: #fff; - border: 0; - border: 1px solid #eee; - border-top-color: transparent; - border-left-color: transparent; - border-right: 0; + border-right: 1px solid transparent; + border-left: 1px solid transparent; + border-top: 1px solid transparent; + border-bottom: 1px solid #eee; + margin-bottom: 0; position: relative; - margin: 0 20px 0 0; z-index: 1; - white-space: nowrap; - overflow: hidden; - font-weight: bold; +} +.resource-list li:hover { + background-color: #f7f7f7; } .resource-list li:last-child { border-bottom-color: transparent; } +.resource-list li.active { + border-color: #888; + border-right-color: #f9f9f9; + background-color: #f9f9f9; + margin-right: -21px; +} +/**/ .resource-list li a { display: block; padding: 5px 10px; color: #333; + border-right: 0; + font-weight: bold; } -.resource-list li:hover a{ +.resource-list li a:hover { color: #B22; -} -.resource-list li:hover { - background-color: #f7f7f7; -} -.resource-list a:hover { text-decoration: none; } -.resource-list li.active { - border-color: #888; - background-color: #f9f9f9; - margin-right: 0; -} - -/* HasErrors */ -.resource-list li.hasErrors { - border-color: #c00; - border-right: 1px solid #c00; -} -.resource-list li.active.hasErrors { - border-right: 0; -} -.resource-list li.hasErrors a { - color: #c00; -} -.resource-errors { - display: none; -} -.resource-errors dl { - margin-bottom: 0; -} -body.editresources .error-explanation { - /* Let JS render the resource errors inline */ - display: none; +/* Resource-list-edit */ +.resource-list-edit { + padding-top: 10px; } - - /* While dragging.... */ .resource-list-edit li.ui-sortable-helper { + border-color: #888; box-shadow: 2px 2px 8px rgba(0,0,0,0.1); } -.resource-list-edit li.ui-sortable-helper.active { - margin-right: 20px; - border-right: 1px solid #888; -} -.resource-list-add, -.resource-list-edit { - width: 350px; -} + +/* Resource-list-add */ .resource-list-add { margin-top: 20px; } .resource-list-add li { border-top: 1px solid #eee; -} -.resource-list-add li.active { - border-bottom-color: #888; -} -.resource-list-add li a { padding-left: 43px; } -.resource-list-edit li a { - padding-left: 0; -} - -button.resource-edit-delete { - margin-top: 10px; - float: right; -} - - /* Right-hand-side edit resource panel */ -/* ----------------------------------- */ .resource-panel { background: #f9f9f9; border: 1px solid #888; - box-shadow: 2px 2px 4px #888; - margin-bottom: 20px; - position: absolute; - left: 349px; - top: 0px; padding: 10px 20px; - min-height: 100px; - min-width: 530px; + position: relative; } .resource-panel .resource-panel-close { position: absolute; @@ -1395,28 +1330,28 @@ button.resource-edit-delete { padding: 0; text-align: center; } -.resource-panel .hint { - font-size: 11px; -} -.resource-details input[type="text"] { +.resource-panel input[type="text"] { width: 397px; } -.resource-add input[type="text"] { - width: 280px; +.resource-panel .markdown-editor { + width: 390px; } -.resource-details .markdown-editor { - width: 30em; +.resource-panel textarea { + height: 90px; } -.resource-details .control-group { +.resource-panel .control-group { margin-bottom: 3px; } -.resource-details textarea { - width: 340px; - height: 90px; +.resource-panel .hint { + font-size: 11px; } -.resource-details.resource-add { +.resource-panel .resource-add { + min-height: 140px; margin-bottom: 30px; } +.resource-panel .resource-add input[type="text"] { + width: 280px; +} /* Resource extra fields */ /* --------------------- */ @@ -1434,3 +1369,32 @@ button.resource-edit-delete { width: 164px; } + +button.resource-edit-delete { + margin-top: 10px; + float: right; +} + + + +/* HasErrors */ +.resource-list li.hasErrors { + border-color: #c00; + border-right: 1px solid #c00; +} +.resource-list li.active.hasErrors { + border-right: 0; +} +.resource-list li.hasErrors a { + color: #c00; +} +.resource-errors { + display: none; +} +.resource-errors dl { + margin-bottom: 0; +} +body.editresources .error-explanation { + /* Let JS render the resource errors inline */ + display: none; +} diff --git a/ckan/public/scripts/application.js b/ckan/public/scripts/application.js index 160b59e4ad8..1906cd52668 100644 --- a/ckan/public/scripts/application.js +++ b/ckan/public/scripts/application.js @@ -1,5 +1,9 @@ var CKAN = CKAN || {}; +CKAN.View = CKAN.View || {}; +CKAN.Model = CKAN.Model || {}; +CKAN.Utils = CKAN.Utils || {}; + /* ================================= */ /* == Initialise CKAN Application == */ /* ================================= */ @@ -17,15 +21,6 @@ var CKAN = CKAN || {}; CKAN.Utils.setupMarkdownEditor($('.markdown-editor')); // bootstrap collapse $('.collapse').collapse({toggle: false}); - // set up ckan js - var config = { - endpoint: CKAN.SITE_URL + '/' - }; - var client = new CKAN.Client(config); - // serious hack to deal with hacky code in ckanjs - CKAN.UI.workspace = { - client: client - }; // Buttons with href-action should navigate when clicked $('input.href-action').click(function(e) { @@ -85,12 +80,12 @@ var CKAN = CKAN || {}; if (storageEnabled) { $('li.js-upload-file').show(); } - // Backbone model/view - var _dataset = new CKAN.Model.Dataset(preload_dataset); - var $el=$('form#dataset-edit'); - var view=new CKAN.View.ResourceEditor({ - collection: _dataset.get('resources'), - el: $el + // Backbone collection class + var CollectionOfResources = Backbone.Collection.extend({model: CKAN.Model.Resource}); + // 'resources_json' was embedded into the page + var view = new CKAN.View.ResourceEditor({ + collection: new CollectionOfResources(resources_json), + el: $('form#dataset-edit') }); view.render(); @@ -125,6 +120,21 @@ var CKAN = CKAN || {}; }(jQuery)); +/* =============================== */ +/* Backbone Model: Resource object */ +/* =============================== */ +CKAN.Model.Resource = Backbone.Model.extend({ + constructor: function Resource() { + Backbone.Model.prototype.constructor.apply(this, arguments); + }, + toTemplateJSON: function() { + var obj = Backbone.Model.prototype.toJSON.apply(this, arguments); + obj.displaytitle = obj.description ? obj.description : 'No description ...'; + return obj; + } +}); + + /* ============================== */ /* == Backbone View: UrlEditor == */ @@ -269,13 +279,23 @@ CKAN.View.ResourceEditor = Backbone.View.extend({ // Trigger the Add Resource pane this.el.find('.js-resource-add').click(this.openAddPanel); - // Tabbed view for adding resources - var $resourceAdd = this.el.find('.resource-add'); - this.addView=new CKAN.View.ResourceAddTabs({ + // Tabs for adding resources + new CKAN.View.ResourceAddUrl({ collection: this.collection, - el: $resourceAdd + el: this.el.find('.js-add-url-form'), + mode: 'file' + }); + new CKAN.View.ResourceAddUrl({ + collection: this.collection, + el: this.el.find('.js-add-api-form'), + mode: 'api' + }); + new CKAN.View.ResourceAddUpload({ + collection: this.collection, + el: this.el.find('.js-add-upload-form') }); + // Close details button this.el.find('.resource-panel-close').click(this.closePanel); @@ -319,20 +339,19 @@ CKAN.View.ResourceEditor = Backbone.View.extend({ openAddPanel: function(e) { if (e) { e.preventDefault(); } var panel = this.el.find('.resource-panel'); - var addLi = this.el.find('.resource-list-add li'); - this.el.find('.resource-list li').removeClass('active'); - this.el.find('.resource-details').hide(); + var addLi = this.el.find('.resource-list-add > li'); + this.el.find('.resource-list > li').removeClass('active'); + $('.resource-details').hide(); this.el.find('.resource-details.resource-add').show(); addLi.addClass('active'); panel.show(); - panel.css('top', Math.max(0, addLi.position().top + addLi.height() - panel.height())); }, /* * Close the panel on the right. */ closePanel: function(e) { if (e) { e.preventDefault(); } - this.el.find('.resource-list li').removeClass('active'); + this.el.find('.resource-list > li').removeClass('active'); this.el.find('.resource-panel').hide(); }, /* @@ -536,12 +555,11 @@ CKAN.View.Resource = Backbone.View.extend({ // Close all tables var panel = this.table.parents('.resource-panel'); panel.find('.resource-details').hide(); - this.li.parents('fieldset#resources').find('li').removeClass('active'); + this.li.parents('fieldset#resources').find('.resource-list > li').removeClass('active'); panel.show(); this.table.show(); this.table.find('.js-resource-edit-name').focus(); this.li.addClass('active'); - panel.css('top', Math.max(0, this.li.position().top+this.li.height() - panel.height())); }, /* * Called when my delete button is clicked. Calls back to the parent @@ -652,120 +670,195 @@ CKAN.View.Resource = Backbone.View.extend({ } }); -/* ===================================== */ -/* == Backbone View: ResourceAdd Tabs == */ -/* ===================================== */ -CKAN.View.ResourceAddTabs = Backbone.View.extend({ - initialize: function() { - _.bindAll(this, 'render', 'addNewResource', 'reset'); - }, - render: function() { +/* ============================================= */ +/* Backbone View: Add Resource by uploading file */ +/* ============================================= */ +CKAN.View.ResourceAddUpload = Backbone.View.extend({ + tagName: 'div', + + initialize: function(options) { + this.el = $(this.el); + _.bindAll(this, 'render', 'updateFormData', 'setMessage', 'uploadFile'); + $(CKAN.Templates.resourceUpload).appendTo(this.el); + this.$messages = this.el.find('.alert'); + this.setupFileUpload(); }, events: { - 'click button': 'clickButton', - 'click input[name=reset]': 'reset' + 'click input[type="submit"]': 'uploadFile' }, - reset: function() { - this.el.find('button').removeClass('depressed'); - if (this.subView) { - this.subView.remove(); - this.subView = null; - } - return false; + setupFileUpload: function() { + var self = this; + this.el.find('.fileupload').fileupload({ + // needed because we are posting to remote url + forceIframeTransport: true, + replaceFileInput: false, + autoUpload: false, + fail: function(e, data) { + alert('Upload failed'); + }, + add: function(e,data) { + self.fileData = data; + self.fileUploadData = data; + self.key = self.makeUploadKey(data.files[0].name); + self.updateFormData(self.key); + }, + send: function(e, data) { + self.setMessage('Uploading file ... '); + }, + done: function(e, data) { + self.onUploadComplete(self.key); + } + }) }, - clickButton: function(e) { - e.preventDefault(); - var $target = $(e.target); + ISODateString: function(d) { + function pad(n) {return n<10 ? '0'+n : n}; + return d.getUTCFullYear()+'-' + + pad(d.getUTCMonth()+1)+'-' + + pad(d.getUTCDate())+'T' + + pad(d.getUTCHours())+':' + + pad(d.getUTCMinutes())+':' + + pad(d.getUTCSeconds()); + }, - if ($target.is('.depressed')) { - this.reset(); - } - else { - this.reset(); - $target.addClass('depressed'); - - var $subPane = $('
').addClass('subpane'); - this.el.append($subPane); - - var tempResource = new CKAN.Model.Resource({}); - - 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 + // Create an upload key/label for this file. + // + // Form: {current-date}/file-name. Do not just use the file name as this + // would lead to collisions. + // (Could add userid/username and/or a small random string to reduce + // collisions but chances seem very low already) + makeUploadKey: function(fileName) { + // google storage replaces ' ' with '+' which breaks things + // See http://trac.ckan.org/ticket/1518 for more. + var corrected = fileName.replace(/ /g, '-'); + // note that we put hh mm ss as hhmmss rather hh:mm:ss (former is 'basic + // format') + var now = new Date(); + // replace ':' with nothing + var str = this.ISODateString(now).replace(':', '').replace(':', ''); + return str + '/' + corrected; + }, + + updateFormData: function(key) { + var self = this; + self.setMessage('Checking upload permissions ... '); + self.el.find('.fileinfo').text(key); + $.ajax({ + url: CKAN.SITE_URL + '/api/storage/auth/form/' + key, + async: false, + success: function(data) { + self.el.find('form').attr('action', data.action); + _tmpl = ''; + var $hidden = $(self.el.find('form div.hidden-inputs')[0]); + $.each(data.fields, function(idx, item) { + $hidden.append($.tmpl(_tmpl, item)); }); + self.hideMessage(); + }, + error: function(jqXHR, textStatus, errorThrown) { + // TODO: more graceful error handling (e.g. of 409) + self.setMessage('Failed to get credentials for storage upload. Upload cannot proceed', 'error'); + self.el.find('input[name="add-resource-upload"]').hide(); } - this.subView.render(); + }); + }, + + uploadFile: function(e) { + e.preventDefault(); + if (!this.fileData) { + alert('No file selected'); + return; } + var jqXHR = this.fileData.submit(); + }, + + onUploadComplete: function(key) { + var self = this; + $.ajax({ + url: CKAN.SITE_URL + '/api/storage/metadata/' + self.key, + success: function(data) { + var name = data._label; + if (name && name.length > 0 && name[0] === '/') { + name = name.slice(1); + } + var d = new Date(data._last_modified); + var lastmod = self.ISODateString(d); + var newResource = new CKAN.Model.Resource({}); + newResource.set({ + url: data._location + , name: name + , size: data._content_length + , last_modified: lastmod + , format: data._format + , mimetype: data._format + , resource_type: 'file.upload' + , owner: data['uploaded-by'] + , hash: data._checksum + , cache_url: data._location + , cache_url_updated: lastmod + } + , { + error: function(model, error) { + var msg = 'Filed uploaded OK but error adding resource: ' + error + '.'; + msg += 'You may need to create a resource directly. Uploaded file at: ' + data._location; + self.setMessage(msg, 'error'); + } + } + ); + self.collection.add(newResource); + self.setMessage('File uploaded OK and resource added', 'success'); + } + }); }, - addNewResource: function(tempResource) { - // Deep-copy the tempResource we had bound to - var resource=new CKAN.Model.Resource(tempResource.toJSON()); + setMessage: function(msg, category) { + var category = category || 'alert-info'; + this.$messages.removeClass('alert-info alert-success alert-error'); + this.$messages.addClass(category); + this.$messages.show(); + this.$messages.html(msg); + }, - this.collection.add(resource); - this.reset(); + hideMessage: function() { + this.$messages.hide('slow'); } }); -/* ================================================= */ -/* == Backbone View: ResourceAdd Link-To-Resource == */ -/* ================================================= */ -CKAN.View.ResourceAddLink = Backbone.View.extend({ +/* ======================================== */ +/* == Backbone View: Add resource by URL == */ +/* ======================================== */ +CKAN.View.ResourceAddUrl = Backbone.View.extend({ initialize: function(options) { - _.bindAll(this, 'render'); - this.mode = options.mode; + _.bindAll(this, 'clickSubmit'); }, - render: function() { - var tmpl = null; - if (this.mode=='file') { - tmpl = $.tmpl(CKAN.Templates.resourceAddLinkFile); - } - else if (this.mode=='api') { - tmpl = $.tmpl(CKAN.Templates.resourceAddLinkApi); - } - $(this.el).html(tmpl); - return this; - }, + clickSubmit: function(e) { + e.preventDefault(); - setResourceInfo: function(e) { - e.preventDefault(); + var self = this; + var newResource = new CKAN.Model.Resource({}); - this.el.find('input[name=save]').addClass("disabled"); - this.el.find('input[name=reset]').addClass("disabled"); - var urlVal=this.el.find('input[name=url]').val(); + this.el.find('input[name="add-resource-save"]').addClass("disabled"); + var urlVal = this.el.find('input[name="add-resource-url"]').val(); var qaEnabled = $.inArray('qa',CKAN.plugins)>=0; - if(qaEnabled && this.mode=='file') { + if(qaEnabled && this.options.mode=='file') { $.ajax({ url: CKAN.SITE_URL + '/qa/link_checker', - context: this.model, + context: newResource, data: {url: urlVal}, dataType: 'json', error: function(){ - this.set({url: urlVal, resource_type: 'file'}); + newResource.set({url: urlVal, resource_type: 'file'}); + self.collection.add(newResource); + self.resetForm(); }, success: function(data){ data = data[0]; - this.set({ + newResource.set({ url: urlVal, resource_type: 'file', format: data.format, @@ -774,15 +867,25 @@ CKAN.View.ResourceAddLink = Backbone.View.extend({ last_modified: data.last_modified, url_error: (data.url_errors || [""])[0] }); + self.collection.add(newResource); + self.resetForm(); } }); - } else { - this.model.set({url: urlVal, resource_type: this.mode}); + } + else { + newResource.set({url: urlVal, resource_type: this.options.mode}); + this.collection.add(newResource); + this.resetForm(); } }, + resetForm: function() { + this.el.find('input[name="add-resource-save"]').removeClass("disabled"); + this.el.find('input[name="add-resource-url"]').val(''); + }, + events: { - 'submit form': 'setResourceInfo' + 'click .btn': 'clickSubmit' } }); diff --git a/ckan/public/scripts/templates.js b/ckan/public/scripts/templates.js index b1c376f12a3..b27cabc3387 100644 --- a/ckan/public/scripts/templates.js +++ b/ckan/public/scripts/templates.js @@ -1,25 +1,5 @@ - -CKAN.Templates.resourceAddLinkFile = ' \ - \ -'; - -CKAN.Templates.resourceAddLinkApi = ' \ - \ -'; +var CKAN = CKAN || {}; +CKAN.Templates = CKAN.Templates || {}; CKAN.Templates.resourceUpload = ' \