/ckan/public/scripts/vendor/ckanjs/1.0.0/ckanjs.js
https://bitbucket.org/okfn/ckan/ · JavaScript · 1754 lines · 1369 code · 173 blank · 212 comment · 76 complexity · df3688ae905b70055f396ef0491ff010 MD5 · raw file
- 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);