[ckan-changes] commit/ckan: zephod: [refactor][s]: Tidied ckanjs.js integration.
Bitbucket
commits-noreply at bitbucket.org
Mon Oct 17 14:09:12 UTC 2011
1 new changeset in ckan:
http://bitbucket.org/okfn/ckan/changeset/ba2951a3a03d/
changeset: ba2951a3a03d
user: zephod
date: 2011-10-17 16:07:58
summary: [refactor][s]: Tidied ckanjs.js integration.
affected #: 2 files (-1 bytes)
--- /dev/null Thu Jan 01 00:00:00 1970 +0000
+++ b/ckan/public/scripts/vendor/ckanjs/1.0.0/ckanjs.js Mon Oct 17 15:07:58 2011 +0100
@@ -0,0 +1,1754 @@
+this.CKAN = this.CKAN || {};
+
+this.CKAN.Client = (function (CKAN, $, _, Backbone, undefined) {
+
+ // Client constructor. Creates a new client for communicating with
+ // the CKAN API.
+ function Client(config) {
+ this._environment = {};
+ this.configure(config || Client.defaults);
+
+ _.bindAll(this, 'syncDataset', '_datasetConverter');
+ }
+
+ // Default config parameters for the Client.
+ Client.defaults = {
+ apiKey: '',
+ endpoint: 'http://ckan.net'
+ };
+
+ // Extend the Client prototype with Backbone.Events to provide .bind(),
+ // .unbind() and .trigger() methods.
+ _.extend(Client.prototype, Backbone.Events, {
+
+ cache: {
+ dataset: new Backbone.Collection()
+ },
+
+ // Allows the implementor to specify an object literal of settings to
+ // configure the current client. Options include:
+ //
+ // - apiKey: The API key for the current user to create/edit datasets.
+ // - endpoint: The API endpoint to connect to.
+ configure: function (config) {
+ config = config || {};
+ if (config.endpoint) {
+ config.endpoint = config.endpoint.replace(/\/$/, '');
+ config.restEndpoint = config.endpoint + '/api/2/rest';
+ config.searchEndpoint = config.endpoint + '/api/2/search';
+ }
+ return this.environment(config);
+ },
+
+ // Client environment getter/setter. Environment variables can be retrieved
+ // by providing a key string, if the key does not exist the method will
+ // return `undefined`. To set keys either a key value pair can be provided
+ // or an object literal containing multiple key/value pairs.
+ environment: function (key, value) {
+ if (typeof key === "string") {
+ if (arguments.length === 1) {
+ return this._environment[key];
+ }
+ this._environment[key] = value;
+ } else {
+ _.extend(this._environment, key);
+ }
+ return this;
+ },
+
+ // Helper method to fetch datasets from the server. Using this method to
+ // fetch datasets will ensure that only one instance of a model per server
+ // resource exists on the page at one time.
+ //
+ // The method accepts the dataset `"id"` and an object of `"options"`, these
+ // can be any options accepted by the `.fetch()` method on `Backbone.Model`.
+ // If the model already exists it will simply be returned otherwise an empty
+ // model will be returned and the data requested from the server.
+ //
+ // var dataset = client.getDatasetById('my-data-id', {
+ // success: function () {
+ // // The model is now populated.
+ // },
+ // error: function (xhr) {
+ // // Something went wrong check response status etc.
+ // }
+ // });
+ //
+ getDatasetById: function (id, options) {
+ var cache = this.cache.dataset,
+ dataset = cache.get(id);
+ var ourOptions = options || {};
+
+ if (!dataset) {
+ dataset = this.createDataset({id: id});
+
+ // Add the stub dataset to the global cache to ensure that only one
+ // is ever created.
+ cache.add(dataset);
+
+ // Fetch the dataset from the server passing in any options provided.
+ // Also set up a callback to remove the model from the cache in
+ // case of error.
+ ourOptions.error = function () {
+ cache.remove(dataset);
+ };
+ // TODO: RP not sure i understand what this does and why it is needed
+ dataset.fetch(ourOptions);
+ }
+ return dataset;
+ },
+
+ // Helper method to create a new instance of CKAN.Model.Dataset and
+ // register a sync listener to update the representation on the server when
+ // the model is created/updated/deleted.
+ //
+ // var myDataset = client.createDataset({
+ // title: "My new data set"
+ // });
+ //
+ // This ensures that the models are always saved with the latest environment
+ // data.
+ createDataset: function (attributes) {
+ return (new CKAN.Model.Dataset(attributes)).bind('sync', this.syncDataset);
+ },
+
+ // A wrapper around Backbone.sync() that adds additional ajax options to
+ // each request. These include the API key and the request url rather than
+ // using the model to generate it.
+ syncDataset: function (method, model, options) {
+ // Get the package url.
+ var url = this.environment('restEndpoint') + '/package';
+
+ // Add additional request options.
+ options = _.extend({}, {
+ url: model.isNew() ? url : url + '/' + model.id,
+ headers: {
+ 'X-CKAN-API-KEY': this.environment('apiKey')
+ }
+ }, options);
+
+ Backbone.sync(method, model, options);
+ return this;
+ },
+
+ // Performs a search for datasets against the CKAN API. The `options`
+ // argument can contain any keys supported by jQuery.ajax(). The query
+ // parameters should be provided in the `options.query` property.
+ //
+ // var query = client.searchDatasets({
+ // success: function (datasets) {
+ // console.log(datasets); // Logs a Backbone.Collection
+ // }
+ // });
+ //
+ // The `options.success` method (and any other success callbacks) will
+ // recieve a `SearchCollection` containing `Dataset` models. The method
+ // returns a jqXHR object so that additional callbacks can be registered
+ // with .success() and .error().
+ searchDatasets: function (options) {
+ options = options || {};
+ options.data = _.defaults(options.query, {'limit': 10, 'all_fields': 1});
+ delete options.query;
+
+ return $.ajax(_.extend({
+ url: this.environment('searchEndpoint') + '/package',
+ converters: {
+ 'text json': this._datasetConverter
+ }
+ }, options));
+ },
+
+ // A "converter" method for jQuery.ajax() this is used to convert the
+ // results from a search API request into models which in turn will be
+ // passed into any registered success callbacks. We do this here so that
+ // _all_ registered success callbacks recieve the same data rather than
+ // just the callback registered when the search was made.
+ _datasetConverter: function (raw) {
+ var json = $.parseJSON(raw),
+ models = _.map(json.results, function (attributes) {
+ return this.createDataset(attributes);
+ }, this);
+
+ return new CKAN.Model.SearchCollection(models, {total: json.count});
+ },
+
+ // Performs a query on CKAN API.
+ // The `options` argument can contain any keys supported by jQuery.ajax().
+ // In addition it should contain either a url or offset variable. If
+ // offset provided it will be used to construct the full api url by
+ // prepending the endpoint plus 'api' (i.e. offset of '/2/rest/package'
+ // will become '{endpoint}/api/2/rest'.
+ //
+ // The `options.success` method (and any other success callbacks) will
+ // recieve a `SearchCollection` containing `Dataset` models. The method
+ // returns a jqXHR object so that additional callbacks can be registered
+ // with .success() and .error().
+ apiCall: function (options) {
+ options = options || {};
+ // Add additional request options.
+ options = _.extend({}, {
+ url: this.environment('endpoint') + '/api' + options.offset,
+ headers: {
+ 'X-CKAN-API-KEY': this.environment('apiKey')
+ }
+ }, options);
+
+ return $.ajax(options);
+ },
+
+ // wrap CKAN /api/storage/auth/form - see http://packages.python.org/ckanext-storage
+ // params and returns value are as for that API
+ // key is file label/path
+ getStorageAuthForm: function(key, options) {
+ options.offset = '/storage/auth/form/' + key;
+ this.apiCall(options);
+ }
+ });
+
+ return Client;
+
+})(this.CKAN, this.$, this._, this.Backbone);
+this.CKAN = this.CKAN || {};
+
+// Global object that stores all CKAN models.
+CKAN.Model = function ($, _, Backbone, undefined) {
+
+ var Model = {};
+
+ // Simple validator helper returns a `validate()` function that checks
+ // the provided model keys and returns an error object if these do not
+ // exist on the model or the attributes object provided.\
+ //
+ // validate: validator('title', 'description', url)
+ //
+ function validator() {
+ var required = arguments;
+ return function (attrs) {
+ var errors;
+ if (attrs) {
+ _.each(required, function (key) {
+ if (!attrs[key] && !this.get(key)) {
+ if (!errors) {
+ errors = {};
+ }
+ errors[key] = 'The "' + key + '" is required';
+ }
+ }, this);
+ }
+ return errors;
+ };
+ }
+
+ // A Base model that all CKAN models inherit from. Methods that should be
+ // shared across all models should be defined here.
+ Model.Base = Backbone.Model.extend({
+
+ // Extend the default Backbone.Model constructor simply to provide a named
+ // function. This improves debugging in consoles such as the Webkit inspector.
+ constructor: function Base(attributes, options) {
+ Backbone.Model.prototype.constructor.apply(this, arguments);
+ },
+
+ // Rather than letting the models connect to the server themselves we
+ // leave this to the implementor to decide how models are saved. This allows
+ // the API details such as API key and enpoints to change without having
+ // to update the models. When `.save()` or `.destroy()` is called the
+ // `"sync"` event will be published with the arguments provided to `.sync()`.
+ //
+ // var package = new Package({name: 'My Package Name'});
+ // package.bind('sync', Backbone.sync);
+ //
+ // This method returns itself for chaining.
+ sync: function () {
+ return this.trigger.apply(this, ['sync'].concat(_.toArray(arguments)));
+ },
+
+ // Overrides the standard `toJSON()` method to serialise any nested
+ // Backbone models and collections (or any other object that has a `toJSON()`
+ // method).
+ toJSON: function () {
+ var obj = Backbone.Model.prototype.toJSON.apply(this, arguments);
+ _.each(obj, function (value, key) {
+ if (value && typeof value === 'object' && value.toJSON) {
+ obj[key] = value.toJSON();
+ }
+ });
+ return obj;
+ }
+ });
+
+ // Model objects
+ Model.Dataset = Model.Base.extend({
+ constructor: function Dataset() {
+ // Define an key/model mapping for child relationships. These will be
+ // managed as a Backbone collection when setting/getting the key.
+ this.children = {
+ resources: Model.Resource,
+ relationships: Model.Relationship
+ };
+ Model.Base.prototype.constructor.apply(this, arguments);
+ },
+
+ defaults: {
+ title: '',
+ name: '',
+ notes: '',
+ resources: [],
+ tags: []
+ },
+
+ // Override the `set()` method on `Backbone.Model` to handle resources as
+ // relationships. This will now manually update the `"resouces"` collection
+ // (using `_updateResources()`) with any `Resource` models provided rather
+ // than replacing the key.
+ set: function (attributes, options) {
+ var children, validated;
+
+ // If not yet defined set the child collections. This will be done when
+ // set is called for the first time in the constructor.
+ this._createChildren();
+
+ // Check to see if any child keys are present in the attributes and
+ // remove them from the object. Then update them seperately after the
+ // parent `set()` method has been called.
+ _.each(this.children, function (Model, key) {
+ if (attributes && attributes[key]) {
+ if (!(attributes[key] instanceof Backbone.Collection)) {
+ if (!children) {
+ children = {};
+ }
+ children[key] = attributes[key];
+ delete attributes[key];
+ }
+ }
+ }, this);
+
+ validated = Model.Base.prototype.set.call(this, attributes, options);
+
+ // Ensure validation passed before updating child models.
+ if (validated && children) {
+ this._updateChildren(children);
+ }
+
+ return validated;
+ },
+
+ // Checks to see if our model instance has Backbone collections defined for
+ // child keys. If they do not exist it creates them.
+ _createChildren: function () {
+ _.each(this.children, function (Model, key) {
+ if (!this.get(key)) {
+ var newColl = new Backbone.Collection();
+ this.attributes[key] = newColl;
+ newColl.model = Model;
+ // bind change events so updating the children trigger change on Dataset
+ var self = this;
+ // TODO: do we want to do all or be more selective
+ newColl.bind('all', function() {
+ self.trigger('change');
+ });
+ }
+ }, this);
+ return this;
+ },
+
+ // Manages the one to many relationship between resources and the dataset.
+ // Accepts an array of Resources (ideally model instances but will convert
+ // object literals into resources for you). New models will be added to the
+ // collection and existing ones updated. Any pre-existing models not found
+ // in the new array will be removed.
+ _updateChildren: function (children) {
+ _.each(children, function (models, key) {
+ var collection = this.get(key),
+ ids = {};
+
+ // Add/Update models.
+ _.each(models, function (model) {
+ var existing = collection.get(model.id),
+ isLiteral = !(model instanceof this.children[key]);
+
+ // Provide the dataset key if not already there and current model is
+ // not a relationship.
+ if (isLiteral && key !== 'relationships') {
+ model.dataset = this;
+ delete model.package_id;
+ }
+
+ if (!existing) {
+ collection.add(model);
+ }
+ else if (existing && isLiteral) {
+ existing.set(model);
+ }
+
+ ids[model.id] = 1;
+ }, this);
+
+ // Remove missing models.
+ collection.remove(collection.select(function (model) {
+ return !ids[model.id];
+ }));
+ }, this);
+ return this;
+ },
+
+ // NOTE: Returns localised URL.
+ toTemplateJSON: function () {
+ var out = this.toJSON();
+ var title = this.get('title');
+ out.displaytitle = title ? title : 'No title ...';
+ var notes = this.get('notes');
+ // Don't use a global Showdown; CKAN doesn't need that library
+ var showdown = new Showdown.converter();
+ out.notesHtml = showdown.makeHtml(notes ? notes : '');
+ out.snippet = this.makeSnippet(out.notesHtml);
+ return out;
+ },
+
+ makeSnippet: function (notesHtml) {
+ var out = $(notesHtml).text();
+ if (out.length > 190) {
+ out = out.slice(0, 190) + ' ...';
+ }
+ return out;
+ }
+ });
+
+ // A model for working with resources. Each resource is _required_ to have a
+ // parent `Dataset`. This must be provided under the `"dataset"` key when the
+ // `Resource` is created. This is handled for you when creating resources
+ // via the `Dataset` `set()` method.
+ //
+ // The `save()`, `fetch()` and `delete()` methods are mapped to the parent
+ // dataset and can be used to update a Resource's metadata.
+ //
+ // var resource = new Model.Resource({
+ // name: 'myresource.csv',
+ // url: 'http://www.example.com/myresource.csv',
+ // dataset: dataset
+ // });
+ //
+ // // Updates the resource name on the server by saving the parent dataset
+ // resource.set({name: 'Some new name'});
+ //
+ Model.Resource = Model.Base.extend({
+ constructor: function Resource() {
+ Model.Base.prototype.constructor.apply(this, arguments);
+ },
+
+ // Override the `save()` method to update the Resource with attributes then
+ // call the parent dataset and save that. Any `options` provided will be
+ // passed on to the dataset `save()` method.
+ save: function (attrs, options) {
+ var validated = this.set(attrs);
+ if (validated) {
+ return this.get('dataset').save({}, options);
+ }
+ return validated;
+ },
+
+ // Override the `fetch()` method to call `fetch()` on the parent dataset.
+ fetch: function (options) {
+ return this.get('dataset').fetch(options);
+ },
+
+ // Override the `fetch()` method to trigger the `"destroy"` event which
+ // will remove it from any collections then save the parent dataset.
+ destroy: function (options) {
+ return this.trigger('destroy', this).get('dataset').save({}, options);
+ },
+
+ // Override the `toJSON()` method to set the `"package_id"` key required
+ // by the server.
+ toJSON: function () {
+ // Call Backbone.Model rather than Base to break the circular reference.
+ var obj = Backbone.Model.prototype.toJSON.apply(this, arguments);
+ if (obj.dataset) {
+ obj.package_id = obj.dataset.id;
+ delete obj.dataset;
+ } else {
+ obj.package_id = null;
+ }
+ return obj;
+ },
+
+ toTemplateJSON: function() {
+ var obj = Backbone.Model.prototype.toJSON.apply(this, arguments);
+ obj.displaytitle = obj.description ? obj.description : 'No description ...';
+ return obj;
+ },
+
+ // Validates the provided attributes. Returns an object literal of
+ // attribute/error pairs if invalid, `undefined` otherwise.
+ validate: validator('url')
+ });
+
+ // Helper function that returns a stub method that warns the devloper that
+ // this method has not yet been implemented.
+ function apiPlaceholder(method) {
+ var console = window.console;
+ return function () {
+ if (console && console.warn) {
+ console.warn('The method "' + method + '" has not yet been implemented');
+ }
+ return this;
+ };
+ }
+
+ // A model for working with relationship objects. These are currently just the
+ // realtionship objects returned by the server wrapped in a `Base` model
+ // instance. Currently there is no save or delete functionality.
+ Model.Relationship = Model.Base.extend({
+ constructor: function Relationship() {
+ Model.Base.prototype.constructor.apply(this, arguments);
+ },
+
+ // Add placeholder method that simply returns itself to all methods that
+ // interact with the server. This will also log a warning message to the
+ // developer into the console.
+ save: apiPlaceholder('save'),
+ fetch: apiPlaceholder('fetch'),
+ destroy: apiPlaceholder('destroy'),
+
+ // Validates the provided attributes. Returns an object literal of
+ // attribute/error pairs if invalid, `undefined` otherwise.
+ validate: validator('object', 'subject', 'type')
+ });
+
+ // Collection for managing results from the CKAN search API. An additional
+ // `options.total` parameter can be provided on initialisation to
+ // indicate how many models there are on the server in total. This can
+ // then be accessed via the `total` property.
+ Model.SearchCollection = Backbone.Collection.extend({
+ constructor: function SearchCollection(models, options) {
+ if (options) {
+ this.total = options.total;
+ }
+ Backbone.Collection.prototype.constructor.apply(this, arguments);
+ }
+ });
+
+ return Model;
+
+}(this.jQuery, this._, this.Backbone);
+var CKAN = CKAN || {};
+
+CKAN.Templates = {
+ minorNavigationDataset: ' \
+ <ul class="tabbed"> \
+ <li><a href="#dataset/${dataset.id}/view">View</a></li> \
+ <li><a href="#dataset/${dataset.id}/edit">Edit</a></li> \
+ </ul> \
+ '
+};
+var CKAN = CKAN || {};
+
+CKAN.View = function($) {
+ var my = {};
+
+ // Flash a notification message
+ //
+ // Parameters: msg, type. type is set as class on notification and should be one of success, error.
+ // If type not defined defaults to success
+ my.flash = function(msg, type) {
+ if (type === undefined) {
+ var type = 'success'
+ }
+ $.event.trigger('notification', [msg, type]);
+ };
+
+ my.NotificationView = Backbone.View.extend({
+ initialize: function() {
+ $.template('notificationTemplate',
+ '<div class="flash-banner ${type}">${message} <button>X</button></div>');
+
+ var self = this;
+ $(document).bind('notification', function(e, msg, type) {
+ self.render(msg, type)
+ });
+ },
+
+ events: {
+ 'click .flash-banner button': 'hide'
+ },
+
+ render: function(msg, type) {
+ var _out = $.tmpl('notificationTemplate', {'message': msg, 'type': type})
+ this.el.html(_out);
+ this.el.slideDown(400);
+ },
+
+ hide: function() {
+ this.el.slideUp(200);
+ }
+ });
+
+ my.ConfigView = Backbone.View.extend({
+ initialize: function() {
+ this.cfg = {};
+ this.$ckanUrl = this.el.find('input[name=ckan-url]');
+ this.$apikey = this.el.find('input[name=ckan-api-key]');
+
+ var cfg = this.options.config;
+ this.$ckanUrl.val(cfg.endpoint);
+ this.$apikey.val(cfg.apiKey);
+ },
+
+ events: {
+ 'submit #config-form': 'updateConfig'
+ },
+
+ updateConfig: function(e) {
+ e.preventDefault();
+ this.saveConfig();
+ CKAN.View.flash('Saved configuration');
+ },
+
+ saveConfig: function() {
+ this.cfg = {
+ 'endpoint': this.$ckanUrl.val(),
+ 'apiKey': this.$apikey.val()
+ };
+ $.event.trigger('config:update', this.cfg);
+ }
+ });
+
+ my.DatasetEditView = Backbone.View.extend({
+ initialize: function() {
+ _.bindAll(this, 'saveData', 'render');
+ this.model.bind('change', this.render);
+ },
+
+ render: function() {
+ tmplData = {
+ dataset: this.model.toTemplateJSON()
+ }
+ var tmpl = $.tmpl(CKAN.Templates.datasetForm, tmplData);
+ $(this.el).html(tmpl);
+ if (tmplData.dataset.id) { // edit not add
+ $('#minornavigation').html($.tmpl(CKAN.Templates.minorNavigationDataset, tmplData));
+ }
+ return this;
+ },
+
+ events: {
+ 'submit form.dataset': 'saveData',
+ 'click .previewable-textarea a': 'togglePreview',
+ 'click .dataset-form-navigation a': 'showFormPart'
+ },
+
+ showFormPart: function(e) {
+ e.preventDefault();
+ var action = $(e.target)[0].href.split('#')[1];
+ $('.dataset-form-navigation a').removeClass('selected');
+ $('.dataset-form-navigation a[href=#' + action + ']').addClass('selected');
+ },
+
+ saveData: function(e) {
+ e.preventDefault();
+ this.model.set(this.getData());
+ this.model.save({}, {
+ success: function(model) {
+ CKAN.View.flash('Saved dataset');
+ window.location.hash = '#dataset/' + model.id + '/view';
+ },
+ error: function(model, error) {
+ CKAN.View.flash('Error saving dataset ' + error.responseText, 'error');
+ }
+ });
+ },
+
+ getData: function() {
+ var _data = $(this.el).find('form.dataset').serializeArray();
+ modelData = {};
+ $.each(_data, function(idx, value) {
+ modelData[value.name.split('--')[1]] = value.value
+ });
+ return modelData;
+ },
+
+ togglePreview: function(e) {
+ // set model data as we use it below for notesHtml
+ this.model.set(this.getData());
+ e.preventDefault();
+ var el = $(e.target);
+ var action = el.attr('action');
+ var div = el.closest('.previewable-textarea');
+ div.find('.tabs a').removeClass('selected');
+ div.find('.tabs a[action='+action+']').addClass('selected');
+ var textarea = div.find('textarea');
+ var preview = div.find('.preview');
+ if (action=='preview') {
+ preview.html(this.model.toTemplateJSON().notesHtml);
+ textarea.hide();
+ preview.show();
+ } else {
+ textarea.show();
+ preview.hide();
+ }
+ return false;
+ }
+
+ });
+
+ my.DatasetFullView = Backbone.View.extend({
+ initialize: function() {
+ _.bindAll(this, 'render');
+ this.model.bind('change', this.render);
+
+ // slightly painful but we have to set this up here so
+ // it has access to self because when called this will
+ // be overridden and refer to the element in dom that
+ // was being saved
+ var self = this;
+ this.saveFromEditable = function(value, settings) {
+ var _attribute = $(this).attr('backbone-attribute');
+ var _data = {};
+ _data[_attribute] = value;
+ self.model.set(_data);
+ self.model.save({}, {
+ success: function(model) {
+ CKAN.View.flash('Saved updated notes');
+ },
+ error: function(model, error) {
+ CKAN.View.flash('Error saving notes ' + error.responseText, 'error');
+ }
+ });
+ // do not worry too much about what we return here
+ // because update of model will automatically lead to
+ // re-render
+ return value;
+ };
+ },
+
+ events: {
+ 'click .action-add-resource': 'showResourceAdd'
+ },
+
+ render: function() {
+ var tmplData = {
+ domain: this.options.domain,
+ dataset: this.model.toTemplateJSON(),
+ };
+ $('.page-heading').html(tmplData.dataset.displaytitle);
+ $('#minornavigation').html($.tmpl(CKAN.Templates.minorNavigationDataset, tmplData));
+ $('#sidebar .widget-list').html($.tmpl(CKAN.Templates.sidebarDatasetView, tmplData));
+ this.el.html($.tmpl(CKAN.Templates.datasetView, tmplData));
+ this.setupEditable();
+ return this;
+ },
+
+ setupEditable: function() {
+ var self = this;
+ this.el.find('.editable-area').editable(
+ self.saveFromEditable, {
+ type : 'textarea',
+ cancel : 'Cancel',
+ submit : 'OK',
+ tooltip : 'Click to edit...',
+ onblur : 'ignore',
+ data : function(value, settings) {
+ var _attribute = $(this).attr('backbone-attribute');
+ return self.model.get(_attribute);
+ }
+ }
+ );
+ },
+
+ showResourceAdd: function(e) {
+ var self = this;
+ e.preventDefault();
+ var $el = $('<div />').addClass('resource-add-dialog');
+ $('body').append($el);
+ var resource = new CKAN.Model.Resource({
+ 'dataset': self.model
+ });
+ function handleNewResourceSave(model) {
+ var res = self.model.get('resources');
+ res.add(model);
+ $el.dialog('close');
+ self.model.save({}, {
+ success: function(model) {
+ CKAN.View.flash('Saved dataset');
+ // TODO: no need to re-render (should happen automatically)
+ self.render();
+ }
+ , error: function(model, error) {
+ CKAN.View.flash('Failed to save: ' + error, 'error');
+ }
+ });
+ }
+ resource.bind('change', handleNewResourceSave);
+ var resourceView = new CKAN.View.ResourceCreate({
+ el: $el,
+ model: resource
+ });
+ resourceView.render();
+ dialogOptions = {
+ autoOpen: false,
+ // does not seem to work for width ...
+ position: ['center', 'center'],
+ buttons: [],
+ width: 660,
+ resize: 'auto',
+ modal: false,
+ draggable: true,
+ resizable: true
+ };
+ dialogOptions.title = 'Add Data (File, API, ...)';
+ $el.dialog(dialogOptions);
+ $el.dialog('open');
+ $el.bind("dialogbeforeclose", function () {
+ self.el.find('.resource-add-dialog').remove();
+ });
+ }
+ });
+
+ my.DatasetSearchView = Backbone.View.extend({
+ events: {
+ 'submit #search-form': 'onSearch'
+ },
+
+ initialize: function(options) {
+ var view = this;
+
+ // Temporarily provide the view with access to the client for searching.
+ this.client = options.client;
+ this.$results = this.el.find('.results');
+ this.$datasetList = this.$results.find('.datasets');
+ this.$dialog = this.el.find('.dialog');
+
+ this.resultView = new CKAN.View.DatasetListing({
+ collection: new Backbone.Collection(),
+ el: this.$datasetList
+ });
+
+ _.bindAll(this, "render");
+ },
+
+ render: function() {
+ this.$('.count').html(this.totalResults);
+ this.hideSpinner();
+ this.$results.show();
+ return this;
+ },
+
+ onSearch: function (event) {
+ event.preventDefault();
+ var q = $(this.el).find('input.search').val();
+ this.doSearch(q);
+ },
+
+ doSearch: function (q) {
+ $(this.el).find('input.search').val(q),
+ self = this;
+
+ this.showSpinner();
+ this.$results.hide();
+ this.$results.find('.datasets').empty();
+ this.client.searchDatasets({
+ query: {q:q},
+ success: function (collection) {
+ self.totalResults = collection.total;
+ self.resultView.setCollection(collection);
+ self.render();
+ }
+ });
+ },
+
+ showSpinner: function() {
+ this.$dialog.empty();
+ this.$dialog.html('<h2>Loading results...</h2><img src="http://assets.okfn.org/images/icons/ajaxload-circle.gif" />');
+ this.$dialog.show();
+ },
+
+ hideSpinner: function() {
+ this.$dialog.empty().hide();
+ }
+ });
+
+ my.ResourceView = Backbone.View.extend({
+ render: function() {
+ var resourceData = this.model.toTemplateJSON();
+ var resourceDetails = {};
+ var exclude = [ 'resource_group_id',
+ 'description',
+ 'url',
+ 'position',
+ 'id',
+ 'webstore',
+ 'qa',
+ 'dataset',
+ 'displaytitle'
+ ];
+ $.each(resourceData, function(key, value) {
+ if (! _.contains(exclude, key)) {
+ resourceDetails[key] = value;
+ }
+ });
+ tmplData = {
+ dataset: this.model.get('dataset').toTemplateJSON(),
+ resource: resourceData,
+ resourceDetails: resourceDetails
+ };
+ $('.page-heading').html(tmplData.dataset.name + ' / ' + tmplData.resource.displaytitle);
+ var tmpl = $.tmpl(CKAN.Templates.resourceView, tmplData);
+ $(this.el).html(tmpl);
+ return this;
+ },
+
+ events: {
+ }
+ });
+
+ my.ResourceEditView = Backbone.View.extend({
+ render: function() {
+ var tmpl = $.tmpl(CKAN.Templates.resourceForm, this.model.toJSON());
+ $(this.el).html(tmpl);
+ return this;
+ },
+
+ events: {
+ 'submit form': 'saveData'
+ },
+
+ saveData: function() {
+ // only set rather than save as can only save resources as part of a dataset atm
+ this.model.set(this.getData(), {
+ error: function(model, error) {
+ var msg = 'Failed to save, possibly due to invalid data ';
+ msg += JSON.stringify(error);
+ alert(msg);
+ }
+ });
+ return false;
+ },
+
+ getData: function() {
+ var _data = $(this.el).find('form.resource').serializeArray();
+ modelData = {};
+ $.each(_data, function(idx, value) {
+ modelData[value.name.split('--')[1]] = value.value
+ });
+ return modelData;
+ }
+
+ });
+
+ return my;
+}(jQuery);
+var CKAN = CKAN || {};
+
+CKAN.UI = function($) {
+ var my = {};
+
+ my.Workspace = Backbone.Router.extend({
+ routes: {
+ "": "index",
+ "search": "search",
+ "search/:query": "search",
+ "search/:query/p:page": "search",
+ "dataset/:id/view": "datasetView",
+ "dataset/:id/edit": "datasetEdit",
+ "dataset/:datasetId/resource/:resourceId": "resourceView",
+ "add-dataset": "datasetAdd",
+ "add-resource": "resourceAdd",
+ "config": "config"
+ },
+
+ initialize: function(options) {
+ var self = this;
+ var defaultConfig = {
+ endpoint: 'http://ckan.net',
+ apiKey: ''
+ };
+
+ var config = options.config || defaultConfig;
+ this.client = new CKAN.Client(config);
+ if (options.fixtures && options.fixtures.datasets) {
+ $.each(options.fixtures.datasets, function(idx, obj) {
+ var collection = self.client.cache.dataset;
+ collection.add(new CKAN.Model.Dataset(obj));
+ });
+ }
+
+ var newPkg = this.client.createDataset();
+ var newCreateView = new CKAN.View.DatasetEditView({model: newPkg, el: $('#dataset-add-page')});
+ newCreateView.render();
+
+ var newResource = new CKAN.Model.Resource({
+ dataset: newPkg
+ });
+ var newResourceEditView = new CKAN.View.ResourceEditView({model: newResource, el: $('#add-resource-page')});
+ newResourceEditView.render();
+
+ var searchView = this.searchView = new CKAN.View.DatasetSearchView({
+ client: this.client,
+ el: $('#search-page')
+ });
+
+ // set up top bar search
+ $('#menusearch').find('form').submit(function(e) {
+ e.preventDefault();
+ var _el = $(e.target);
+ var _q = _el.find('input[name="q"]').val();
+ searchView.doSearch(_q);
+ self.search(_q);
+ });
+
+
+ var configView = new CKAN.View.ConfigView({
+ el: $('#config-page'),
+ config: config
+ });
+ $(document).bind('config:update', function(e, cfg) {
+ self.client.configure(cfg);
+ });
+
+ this.notificationView = new CKAN.View.NotificationView({
+ el: $('.flash-banner-box')
+ });
+ },
+
+ switchView: function(view) {
+ $('.page-view').hide();
+ $('#sidebar .widget-list').empty();
+ $('#minornavigation').empty();
+ $('#' + view + '-page').show();
+ },
+
+ index: function(query, page) {
+ this.search();
+ },
+
+ search: function(query, page) {
+ this.switchView('search');
+ $('.page-heading').html('Search');
+ },
+
+ _findDataset: function(id, callback) {
+ var pkg = this.client.getDatasetById(id);
+
+ if (pkg===undefined) {
+ pkg = this.client.createDataset({id: id});
+ pkg.fetch({
+ success: callback,
+ error: function() {
+ alert('There was an error');
+ }
+ });
+ } else {
+ callback(pkg);
+ }
+ },
+
+ datasetView: function(id) {
+ var self = this;
+ self.switchView('view');
+ var $viewpage = $('#view-page');
+ this._findDataset(id, function (model) {
+ var newView = new CKAN.View.DatasetFullView({
+ model: model,
+ el: $viewpage
+ });
+ newView.render();
+ });
+ },
+
+ datasetEdit: function(id) {
+ this.switchView('dataset-edit');
+ $('.page-heading').html('Edit Dataset');
+ function _show(model) {
+ var newView = new CKAN.View.DatasetEditView({model: model});
+ $('#dataset-edit-page').html(newView.render().el);
+ }
+ this._findDataset(id, _show)
+ },
+
+ datasetAdd: function() {
+ this.switchView('dataset-add');
+ $('.page-heading').html('Add Dataset');
+ $('#sidebar .widget-list').empty();
+ },
+
+ resourceView: function(datasetId, resourceId) {
+ this.switchView('resource-view');
+ var $viewpage = $('#resource-view-page');
+ this._findDataset(datasetId, function (model) {
+ var resource = model.get('resources').get(resourceId);
+ var newView = new CKAN.View.ResourceView({
+ model: resource,
+ el: $viewpage
+ });
+ newView.render();
+ });
+ },
+
+ resourceAdd: function() {
+ this.switchView('add-resource');
+ },
+
+ config: function() {
+ this.switchView('config');
+ },
+
+ url: function(controller, action, id) {
+ if (id) {
+ return '#' + controller + '/' + id + '/' + action;
+ } else {
+ return '#' + controller + '/' + action;
+ }
+ }
+ });
+
+ my.initialize = function(options) {
+ my.workspace = new my.Workspace(options);
+ Backbone.history.start()
+ };
+
+ return my;
+}(jQuery);
+
+CKAN.Templates.datasetForm = ' \
+ <form class="dataset" action="" method="POST"> \
+ <dl> \
+ <dt> \
+ <label class="field_opt" for="dataset--title"> \
+ Title * \
+ </label> \
+ </dt> \
+ <dd> \
+ <input id="Dataset--title" name="Dataset--title" type="text" value="${dataset.title}" placeholder="A title (not a description) ..."/> \
+ </dd> \
+ \
+ <dt> \
+ <label class="field_req" for="Dataset--name"> \
+ Name * \
+ <span class="hints"> \
+ A short unique name for the dataset - used in urls and restricted to [a-z] -_ \
+ </span> \
+ </label> \
+ </dt> \
+ <dd> \
+ <input id="Dataset--name" maxlength="100" name="Dataset--name" type="text" value="${dataset.name}" placeholder="A shortish name usable in urls ..." /> \
+ </dd> \
+ \
+ <dt> \
+ <label class="field_opt" for="Dataset--license_id"> \
+ Licence \
+ </label> \
+ </dt> \
+ <dd> \
+ <select id="Dataset--license_id" name="Dataset--license_id"> \
+ <option selected="selected" value=""></option> \
+ <option value="notspecified">Other::License Not Specified</option> \
+ </select> \
+ </dd> \
+ \
+ <dt> \
+ <label class="field_opt" for="Dataset--notes"> \
+ Description and Notes \
+ <span class="hints"> \
+ (You can use <a href="http://daringfireball.net/projects/markdown/syntax">Markdown formatting</a>) \
+ </span> \
+ </label> \
+ </dt> \
+ <dd> \
+ <div class="previewable-textarea"> \
+ <ul class="tabs"> \
+ <li><a href="#" action="write" class="selected">Write</a></li> \
+ <li><a href="#" action="preview">Preview</a></li> \
+ </ul> \
+ <textarea id="Dataset--notes" name="Dataset--notes" placeholder="Start with a summary sentence ...">${dataset.notes}</textarea> \
+ <div id="Dataset--notes-preview" class="preview" style="display: none;"> \
+ <div> \
+ </div> \
+ </dd> \
+ </dl> \
+ \
+ <div class="submit"> \
+ <input id="save" name="save" type="submit" value="Save" /> \
+ </div> \
+ </form> \
+';
+
+CKAN.Templates.datasetFormSidebar = ' \
+ <div class="dataset-form-navigation"> \
+ <ul> \
+ <li> \
+ <a href="#basics" class="selected">Basics</a> \
+ </li> \
+ <li> \
+ <a href="#data">The Data</a> \
+ </li> \
+ <li> \
+ <a href="#additional"> \
+ Additional Information \
+ </a> \
+ </li> \
+ </ul> \
+ </div> \
+';
+CKAN.Templates.datasetView = ' \
+ <div class="dataset view" dataset-id="${dataset.id}"> \
+ <div class="extract"> \
+ ${dataset.snippet} \
+ {{if dataset.snippet.length > 50}} \
+ <a href="#anchor-notes">Read more</a> \
+ {{/if}} \
+ </div> \
+ <div class="tags"> \
+ {{if dataset.tags.length}} \
+ <ul class="dataset-tags"> \
+ {{each dataset.tags}} \
+ <li>${$value}</li> \
+ {{/each}} \
+ </ul> \
+ {{/if}} \
+ </div> \
+ <div class="resources subsection"> \
+ <h3>Resources</h3> \
+ <table> \
+ <tr> \
+ <th>Description</th> \
+ <th>Format</th> \
+ <th>Actions</th> \
+ </tr> \
+ {{each dataset.resources}} \
+ <tr> \
+ <td> \
+ <a href="#dataset/${dataset.id}/resource/${$value.id}"> \
+ {{if $value.description}} \
+ ${$value.description} \
+ {{else}} \
+ (No description) \
+ {{/if}} \
+ </a> \
+ </td> \
+ <td>${$value.format}</td> \
+ <td><a href="${$value.url}" target="_blank" class="resource-download">Download</a> \
+ </tr> \
+ {{/each}} \
+ {{if !dataset.resources.length }} \
+ <tr><td>No resources.</td><td></td></tr> \
+ {{/if}} \
+ </table> \
+ <div class="add-resource"> \
+ <a href="#" class="action-add-resource">Add a resource</a> \
+ </div> \
+ </div> \
+ <div class="notes subsection"> \
+ <h3 id="anchor-notes">Notes</h3> \
+ <div class="notes-body editable-area" backbone-attribute="notes"> \
+ {{html dataset.notesHtml}} \
+ {{if !dataset.notes || dataset.notes.length === 0}} \
+ <em>No notes yet. Click to add some ...</em> \
+ {{/if}} \
+ </div> \
+ </div> \
+ <div class="details subsection"> \
+ <h3>Additional Information</h3> \
+ <table> \
+ <thead> \
+ <tr> \
+ <th>Field</th> \
+ <th>Value</th> \
+ </tr> \
+ </thead> \
+ <tbody> \
+ <tr> \
+ <td>Creator</td> \
+ <td>${dataset.author}</td> \
+ </tr> \
+ <tr> \
+ <td>Maintainer</td> \
+ <td>${dataset.maintainer}</td> \
+ </tr> \
+ {{each dataset.extras}} \
+ <tr> \
+ <td class="package-label" property="rdfs:label">${$index}</td> \
+ <td class="package-details" property="rdf:value">${$value}</td> \
+ </tr> \
+ {{/each}} \
+ </tbody> \
+ </table> \
+ </div> \
+ </div> \
+';
+
+CKAN.Templates.sidebarDatasetView = ' \
+ <li class="widget-container widget_text"> \
+ <h3>Connections</h3> \
+ <ul> \
+ {{each dataset.relationships}} \
+ <li> \
+ ${$value.type} dataset \
+ <a href="#dataset/${$value.object}/view">${$value.object}</a> \
+ {{if $value.comment}} \
+ <span class="relationship_comment"> \
+ (${$value.comment}) \
+ </span> \
+ {{/if}} \
+ </li> \
+ {{/each}} \
+ </ul> \
+ {{if dataset.relationships.length == 0}} \
+ No connections to other datasets. \
+ {{/if}} \
+ </li> \
+';
+CKAN.Templates.resourceForm = ' \
+ <form class="resource" action="" method="POST"> \
+ <dl> \
+ <dt> \
+ <label class="field_opt" for="Resource--url"> \
+ Link \
+ </label> \
+ </dt> \
+ <dd> \
+ <input id="Resource--url" name="Resource--url" type="text" value="${url}" placeholder="http://mydataset.com/file.csv" /> \
+ </dd> \
+ <dt> \
+ <label class="field_opt" for="Resource--type"> \
+ Kind \
+ </label> \
+ </dt> \
+ <dd> \
+ <select id="Resource--type" name="Resource--type"> \
+ <option selected="selected" value="file">File</option> \
+ <option value="api">API</option> \
+ <option value="listing">Listing</option> \
+ <option value="example">Example</option> \
+ </select> \
+ </dd> \
+ </dl> \
+ \
+ <fieldset> \
+ <legend> \
+ <h3>Optional Info</h3> \
+ </legend> \
+ <dl> \
+ <dt> \
+ <label class="field_opt" for="Resource--description"> \
+ Description \
+ </label> \
+ </dt> \
+ <dd> \
+ <input id="Resource--description" name="Resource--description" type="text" value="${description}" placeholder="A short description ..."/> \
+ </dd> \
+ \
+ \
+ <dt> \
+ <label class="field_opt" for="Resource--format"> \
+ Format \
+ </label> \
+ </dt> \
+ <dd> \
+ <input id="Resource--format" name="Resource--format" type="text" value="${format}" placeholder="e.g. csv, zip:csv (zipped csv), sparql"/> \
+ </dd> \
+ </fieldset> \
+ \
+ <div class="submit"> \
+ <input id="save" name="save" type="submit" value="Save" /> \
+ </div> \
+ </form> \
+';
+
+CKAN.Templates.resourceCreate = ' \
+ <div class="resource-create"> \
+ <table> \
+ <tr class="heading"> \
+ <td> \
+ <h3>Link to data already online</h3> \
+ </td> \
+ <td><h3>or</h3></td> \
+ <td><h3>Upload data</h3></td> \
+ </tr> \
+ <tr> \
+ <td class="edit"></td> \
+ <td class="separator"></td> \
+ <td class="upload"></td> \
+ </tr> \
+ </table> \
+ </div> \
+';
+CKAN.Templates.resourceUpload = ' \
+<div class="fileupload"> \
+ <form action="http://test-ckan-net-storage.commondatastorage.googleapis.com/" class="resource-upload" \
+ enctype="multipart/form-data" \
+ method="POST"> \
+ \
+ <div class="fileupload-buttonbar"> \
+ <div class="hidden-inputs"></div> \
+ <label class="fileinput-button"> \
+ File \
+ </label> \
+ <input type="file" name="file" /> \
+ <span class="fileinfo"></span> \
+ <input type="submit" value="upload" /> \
+ </div> \
+ </form> \
+ <div class="messages" style="display: none;"></div> \
+</div> \
+';
+
+CKAN.Templates.resourceView = ' \
+ <div class="resource view" resource-id="${resource.id}"> \
+ <h3> \
+ <a href="${resource.url}" class="url">${resource.url}</a> [${resource.format}] \
+ </h3> \
+ <div class="description"> \
+ ${resource.description} \
+ </div> \
+ \
+ <div class="details subsection"> \
+ <h3>Additional Information</h3> \
+ <table> \
+ <thead> \
+ <tr> \
+ <th>Field</th> \
+ <th>Value</th> \
+ </tr> \
+ </thead> \
+ <tbody> \
+ {{each resourceDetails}} \
+ <tr> \
+ <td class="label">${$index}</td> \
+ <td class="value">${$value}</td> \
+ </tr> \
+ {{/each}} \
+ </tbody> \
+ </table> \
+ </div> \
+ </div> \
+';
+this.CKAN || (this.CKAN = {});
+this.CKAN.View || (this.CKAN.View = {});
+
+(function (CKAN, $, _, Backbone, undefined) {
+ CKAN.View.DatasetListing = Backbone.View.extend({
+ tagName: 'ul',
+
+ constructor: function DatasetListing() {
+ Backbone.View.prototype.constructor.apply(this, arguments);
+
+ _.bindAll(this, 'addItem', 'removeItem');
+
+ this.el = $(this.el);
+ this.setCollection(this.collection);
+ },
+
+ setCollection: function (collection) {
+ if (this.collection) {
+ this.collection.unbind('add', this.addItem);
+ this.collection.unbind('remove', this.removeItem);
+ }
+
+ this.collection = collection;
+ if (collection) {
+ this.collection.bind('add', this.addItem);
+ this.collection.bind('remove', this.removeItem);
+ }
+ return this.render();
+ },
+
+ addItem: function (model) {
+ var view = new CKAN.View.DatasetListingItem({
+ domian: this.options.domain,
+ model: model
+ });
+ this.el.data(model.cid, view).append(view.render().el);
+ return this;
+ },
+
+ removeItem: function (model) {
+ var view = this.el.data(model.cid);
+ if (view) {
+ view.remove();
+ }
+ return this;
+ },
+
+ render: function () {
+ this.el.empty();
+ if (this.collection) {
+ this.collection.each(this.addItem);
+ }
+ return this;
+ },
+
+ remove: function () {
+ this.setCollection(null);
+ return Backbone.View.prototype.remove.apply(this, arguments);
+ }
+ });
+
+ CKAN.View.DatasetListingItem = Backbone.View.extend({
+ tagName: 'li',
+
+ className: 'dataset summary',
+
+ options: {
+ template: '\
+ <div class="header"> \
+ <span class="title" > \
+ <a href="${urls.datasetView}" ckan-attrname="title" class="editable">${displaytitle}</a> \
+ </span> \
+ <div class="search_meta"> \
+ {{if formats.length > 0}} \
+ <ul class="dataset-formats"> \
+ {{each formats}} \
+ <li>${$value}</li> \
+ {{/each}} \
+ </ul> \
+ {{/if}} \
+ </div> \
+ </div> \
+ <div class="extract"> \
+ {{html snippet}} \
+ </div> \
+ <div class="dataset-tags"> \
+ {{if tags.length}} \
+ <ul class="dataset-tags"> \
+ {{each tags}} \
+ <li>${$value}</li> \
+ {{/each}} \
+ </ul> \
+ {{/if}} \
+ </div> \
+ '
+ },
+
+ constructor: function DatasetListingItem() {
+ Backbone.View.prototype.constructor.apply(this, arguments);
+ this.el = $(this.el);
+ },
+
+ render: function () {
+ var dataset = this.model.toTemplateJSON();
+ // if 'UI' mode ...
+ var urls = {};
+ if (CKAN.UI && CKAN.UI.workspace) {
+ urls.datasetView = CKAN.UI.workspace.url('dataset', 'view', this.model.id);
+ } else {
+ urls.datasetView = dataset.ckan_url;
+ }
+ var data = _.extend(dataset, {
+ dataset: dataset,
+ formats: this._availableFormats(),
+ urls: urls
+ });
+ this.el.html($.tmpl(this.options.template, data));
+ return this;
+ },
+
+ _availableFormats: function () {
+ var formats = this.model.get('resources').map(function (resource) {
+ return resource.get('format');
+ });
+ return _.uniq(_.compact(formats));
+ }
+ });
+})(CKAN, $, _, Backbone, undefined);
+this.CKAN || (this.CKAN = {});
+this.CKAN.View || (this.CKAN.View = {});
+
+(function (CKAN, $, _, Backbone, undefined) {
+ CKAN.View.ResourceCreate = Backbone.View.extend({
+ initialize: function() {
+ this.el = $(this.el);
+ _.bindAll(this, 'renderMain');
+ this.renderMain();
+ this.$edit = $(this.el.find('.edit')[0]);
+ this.$upload = $(this.el.find('.upload')[0]);
+ this.editView = new CKAN.View.ResourceEditView({
+ model: this.model,
+ el: this.$edit
+ });
+ this.uploadView = new CKAN.View.ResourceUpload({
+ el: this.$upload,
+ model: this.model,
+ // TODO: horrible reverse depedency ...
+ client: CKAN.UI.workspace.client
+ });
+ },
+
+ renderMain: function () {
+ this.el.empty();
+ tmplData = {
+ };
+ var tmpl = $.tmpl(CKAN.Templates.resourceCreate, tmplData);
+ this.el.html(tmpl);
+ return this;
+ },
+
+ render: function () {
+ this.editView.render();
+ this.uploadView.render();
+ }
+ });
+
+})(CKAN, $, _, Backbone, undefined);
+
+this.CKAN || (this.CKAN = {});
+this.CKAN.View || (this.CKAN.View = {});
+
+(function (CKAN, $, _, Backbone, undefined) {
+ CKAN.View.ResourceUpload = Backbone.View.extend({
+ tagName: 'div',
+
+ // expects a client arguments in its options
+ initialize: function(options) {
+ this.el = $(this.el);
+ this.client = options.client;
+ _.bindAll(this, 'render', 'updateFormData', 'setMessage', 'uploadFile');
+ },
+
+ events: {
+ 'click input[type="submit"]': 'uploadFile'
+ },
+
+ render: function () {
+ this.el.empty();
+ tmplData = {
+ }
+ var tmpl = $.tmpl(CKAN.Templates.resourceUpload, tmplData);
+ this.el.html(tmpl);
+ this.$messages = this.el.find('.messages');
+ this.setupFileUpload();
+ return this;
+ },
+
+ 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 ... <img src="http://assets.okfn.org/images/icons/ajaxload-circle.gif" class="spinner" />');
+ },
+ done: function(e, data) {
+ self.onUploadComplete(self.key);
+ }
+ })
+ },
+
+ 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());
+ },
+
+ // 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) {
+ // 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 + '/' + fileName;
+ },
+
+ updateFormData: function(key) {
+ var self = this;
+ self.setMessage('Checking upload permissions ... <img src="http://assets.okfn.org/images/icons/ajaxload-circle.gif" class="spinner" />');
+ self.el.find('.fileinfo').text(key);
+ self.client.getStorageAuthForm(key, {
+ async: false,
+ success: function(data) {
+ self.el.find('form').attr('action', data.action);
+ _tmpl = '<input type="hidden" name="${name}" value="${value}" />';
+ 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');
+ }
+ });
+ },
+
+ uploadFile: function(e) {
+ e.preventDefault();
+ if (!this.fileData) {
+ alert('No file selected');
+ return;
+ }
+ var jqXHR = this.fileData.submit();
+ },
+
+ onUploadComplete: function(key) {
+ var self = this;
+ self.client.apiCall({
+ offset: '/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);
+ self.model.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;
+ CKAN.View.flash(msg, 'error');
+ }
+ }
+ );
+ self.setMessage('File uploaded OK and resource added', 'success');
+ CKAN.View.flash('File uploaded OK and resource added');
+ }
+ });
+ },
+
+ setMessage: function(msg, category) {
+ var category = category || 'notice';
+ this.$messages.removeClass('notice success error');
+ this.$messages.addClass(category);
+ this.$messages.show();
+ this.$messages.html(msg);
+ },
+
+ hideMessage: function() {
+ this.$messages.hide('slow');
+ }
+ });
+
+})(CKAN, $, _, Backbone, undefined);
--- a/ckan/templates/layout_base.html Mon Oct 17 13:11:24 2011 +0100
+++ b/ckan/templates/layout_base.html Mon Oct 17 15:07:58 2011 +0100
@@ -212,10 +212,9 @@
<!-- for ckanjs etc --><script type="text/javascript" src="${g.site_url}/scripts/vendor/underscore/1.1.6/underscore.js"></script><script type="text/javascript" src="${g.site_url}/scripts/vendor/backbone/0.5.1/backbone.js"></script>
- <!-- 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 type="text/javascript" src="${g.site_url}/scripts/ckanjs.js"></script>
+ <script type="text/javascript" src="${g.site_url}/scripts/vendor/ckanjs/1.0.0/ckanjs.js"></script><!-- Translated js strings live inside an html template. --><xi:include href="js_strings.html" /><!-- finally our application js that sets everything up-->
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