PageRenderTime 97ms CodeModel.GetById 3ms app.highlight 82ms RepoModel.GetById 1ms app.codeStats 1ms

/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

Large files files are truncated, but you can click here to view the full file

   1this.CKAN = this.CKAN || {};
   2
   3this.CKAN.Client = (function (CKAN, $, _, Backbone, undefined) {
   4
   5  // Client constructor. Creates a new client for communicating with
   6  // the CKAN API.
   7  function Client(config) {
   8    this._environment = {};
   9    this.configure(config || Client.defaults);
  10
  11    _.bindAll(this, 'syncDataset', '_datasetConverter');
  12  }
  13
  14  // Default config parameters for the Client.
  15  Client.defaults = {
  16    apiKey: '',
  17    endpoint: 'http://ckan.net'
  18  };
  19
  20  // Extend the Client prototype with Backbone.Events to provide .bind(),
  21  // .unbind() and .trigger() methods.
  22  _.extend(Client.prototype, Backbone.Events, {
  23
  24    cache: {
  25      dataset: new Backbone.Collection()
  26    },
  27
  28    // Allows the implementor to specify an object literal of settings to
  29    // configure the current client. Options include:
  30    //
  31    // - apiKey: The API key for the current user to create/edit datasets.
  32    // - endpoint: The API endpoint to connect to.
  33    configure: function (config) {
  34      config = config || {};
  35      if (config.endpoint) {
  36        config.endpoint = config.endpoint.replace(/\/$/, '');
  37        config.restEndpoint   = config.endpoint + '/api/2/rest';
  38        config.searchEndpoint = config.endpoint + '/api/2/search';
  39      }
  40      return this.environment(config);
  41    },
  42
  43    // Client environment getter/setter. Environment variables can be retrieved
  44    // by providing a key string, if the key does not exist the method will
  45    // return `undefined`. To set keys either a key value pair can be provided
  46    // or an object literal containing multiple key/value pairs.
  47    environment: function (key, value) {
  48      if (typeof key === "string") {
  49        if (arguments.length === 1) {
  50          return this._environment[key];
  51        }
  52        this._environment[key] = value;
  53      } else {
  54        _.extend(this._environment, key);
  55      }
  56      return this;
  57    },
  58
  59    // Helper method to fetch datasets from the server. Using this method to
  60    // fetch datasets will ensure that only one instance of a model per server
  61    // resource exists on the page at one time.
  62    //
  63    // The method accepts the dataset `"id"` and an object of `"options"`, these
  64    // can be any options accepted by the `.fetch()` method on `Backbone.Model`.
  65    // If the model already exists it will simply be returned otherwise an empty
  66    // model will be returned and the data requested from the server.
  67    //
  68    //     var dataset = client.getDatasetById('my-data-id', {
  69    //       success: function () {
  70    //         // The model is now populated.
  71    //       },
  72    //       error: function (xhr) {
  73    //         // Something went wrong check response status etc.
  74    //       }
  75    //     });
  76    //
  77    getDatasetById: function (id, options) {
  78      var cache   = this.cache.dataset,
  79          dataset = cache.get(id);
  80      var ourOptions = options || {};
  81
  82      if (!dataset) {
  83        dataset = this.createDataset({id: id});
  84
  85        // Add the stub dataset to the global cache to ensure that only one
  86        // is ever created.
  87        cache.add(dataset);
  88        
  89        // Fetch the dataset from the server passing in any options provided.
  90        // Also set up a callback to remove the model from the cache in
  91        // case of error.
  92        ourOptions.error = function () {
  93          cache.remove(dataset);
  94        };
  95        // TODO: RP not sure i understand what this does and why it is needed
  96        dataset.fetch(ourOptions);
  97      }
  98      return dataset;
  99    },
 100
 101    // Helper method to create a new instance of CKAN.Model.Dataset and
 102    // register a sync listener to update the representation on the server when
 103    // the model is created/updated/deleted.
 104    //
 105    //     var myDataset = client.createDataset({
 106    //       title: "My new data set"
 107    //     });
 108    //
 109    // This ensures that the models are always saved with the latest environment
 110    // data.
 111    createDataset: function (attributes) {
 112      return (new CKAN.Model.Dataset(attributes)).bind('sync', this.syncDataset);
 113    },
 114
 115    // A wrapper around Backbone.sync() that adds additional ajax options to
 116    // each request. These include the API key and the request url rather than
 117    // using the model to generate it.
 118    syncDataset: function (method, model, options) {
 119      // Get the package url.
 120      var url = this.environment('restEndpoint') + '/package';
 121
 122      // Add additional request options.
 123      options = _.extend({}, {
 124        url: model.isNew() ? url : url + '/' + model.id,
 125        headers: {
 126          'X-CKAN-API-KEY': this.environment('apiKey')
 127        }
 128      }, options);
 129
 130      Backbone.sync(method, model, options);
 131      return this;
 132    },
 133
 134    // Performs a search for datasets against the CKAN API. The `options`
 135    // argument can contain any keys supported by jQuery.ajax(). The query
 136    // parameters should be provided in the `options.query` property.
 137    //
 138    //     var query = client.searchDatasets({
 139    //       success: function (datasets) {
 140    //         console.log(datasets); // Logs a Backbone.Collection
 141    //       }
 142    //     });
 143    //
 144    // The `options.success` method (and any other success callbacks) will
 145    // recieve a `SearchCollection` containing `Dataset` models. The method
 146    // returns a jqXHR object so that additional callbacks can be registered
 147    // with .success() and .error().
 148    searchDatasets: function (options) {
 149      options = options || {};
 150      options.data = _.defaults(options.query, {'limit': 10, 'all_fields': 1});
 151      delete options.query;
 152
 153      return $.ajax(_.extend({
 154        url: this.environment('searchEndpoint') + '/package',
 155        converters: {
 156          'text json': this._datasetConverter
 157        }
 158      }, options));
 159    },
 160
 161    // A "converter" method for jQuery.ajax() this is used to convert the
 162    // results from a search API request into models which in turn will be
 163    // passed into any registered success callbacks. We do this here so that
 164    // _all_ registered success callbacks recieve the same data rather than
 165    // just the callback registered when the search was made.
 166    _datasetConverter: function (raw) {
 167      var json = $.parseJSON(raw),
 168          models = _.map(json.results, function (attributes) {
 169            return this.createDataset(attributes);
 170          }, this);
 171
 172      return new CKAN.Model.SearchCollection(models, {total: json.count});
 173    },
 174
 175    // Performs a query on CKAN API.
 176    // The `options` argument can contain any keys supported by jQuery.ajax().
 177    // In addition it should contain either a url or offset variable. If
 178    // offset provided it will be used to construct the full api url by
 179    // prepending the endpoint plus 'api' (i.e. offset of '/2/rest/package'
 180    // will become '{endpoint}/api/2/rest'.
 181    //
 182    // The `options.success` method (and any other success callbacks) will
 183    // recieve a `SearchCollection` containing `Dataset` models. The method
 184    // returns a jqXHR object so that additional callbacks can be registered
 185    // with .success() and .error().
 186    apiCall: function (options) {
 187      options = options || {};
 188      // Add additional request options.
 189      options = _.extend({}, {
 190        url: this.environment('endpoint') + '/api' + options.offset,
 191        headers: {
 192          'X-CKAN-API-KEY': this.environment('apiKey')
 193        }
 194      }, options);
 195
 196      return $.ajax(options);
 197    },
 198
 199    // wrap CKAN /api/storage/auth/form - see http://packages.python.org/ckanext-storage
 200    // params and returns value are as for that API
 201    // key is file label/path 
 202    getStorageAuthForm: function(key, options) {
 203      options.offset = '/storage/auth/form/' + key;
 204      this.apiCall(options);
 205    }
 206  });
 207
 208  return Client;
 209
 210})(this.CKAN, this.$, this._, this.Backbone);
 211this.CKAN = this.CKAN || {};
 212
 213// Global object that stores all CKAN models.
 214CKAN.Model = function ($, _, Backbone, undefined) {
 215
 216  var Model = {};
 217
 218  // Simple validator helper returns a `validate()` function that checks
 219  // the provided model keys and returns an error object if these do not
 220  // exist on the model or the attributes object provided.\
 221  //
 222  //     validate: validator('title', 'description', url)
 223  //
 224  function validator() {
 225    var required = arguments;
 226    return function (attrs) {
 227      var errors;
 228      if (attrs) {
 229        _.each(required, function (key) {
 230          if (!attrs[key] && !this.get(key)) {
 231            if (!errors) {
 232              errors = {};
 233            }
 234            errors[key] = 'The "' + key + '" is required';
 235          }
 236        }, this);
 237      }
 238      return errors;
 239    };
 240  }
 241
 242  // A Base model that all CKAN models inherit from. Methods that should be
 243  // shared across all models should be defined here.
 244  Model.Base = Backbone.Model.extend({
 245
 246    // Extend the default Backbone.Model constructor simply to provide a named
 247    // function. This improves debugging in consoles such as the Webkit inspector.
 248    constructor: function Base(attributes, options) {
 249      Backbone.Model.prototype.constructor.apply(this, arguments);
 250    },
 251
 252    // Rather than letting the models connect to the server themselves we
 253    // leave this to the implementor to decide how models are saved. This allows
 254    // the API details such as API key and enpoints to change without having
 255    // to update the models. When `.save()` or `.destroy()` is called the
 256    // `"sync"` event will be published with the arguments provided to `.sync()`.
 257    //
 258    //     var package = new Package({name: 'My Package Name'});
 259    //     package.bind('sync', Backbone.sync);
 260    //
 261    // This method returns itself for chaining.
 262    sync: function () {
 263      return this.trigger.apply(this, ['sync'].concat(_.toArray(arguments)));
 264    },
 265
 266    // Overrides the standard `toJSON()` method to serialise any nested
 267    // Backbone models and collections (or any other object that has a `toJSON()`
 268    // method).
 269    toJSON: function () {
 270      var obj = Backbone.Model.prototype.toJSON.apply(this, arguments);
 271      _.each(obj, function (value, key) {
 272        if (value && typeof value === 'object' && value.toJSON) {
 273          obj[key] = value.toJSON();
 274        }
 275      });
 276      return obj;
 277    }
 278  });
 279
 280  // Model objects
 281  Model.Dataset = Model.Base.extend({
 282    constructor: function Dataset() {
 283      // Define an key/model mapping for child relationships. These will be
 284      // managed as a Backbone collection when setting/getting the key.
 285      this.children = {
 286        resources: Model.Resource,
 287        relationships: Model.Relationship
 288      };
 289      Model.Base.prototype.constructor.apply(this, arguments);
 290    },
 291
 292    defaults: {
 293      title: '',
 294      name: '',
 295      notes: '',
 296      resources: [],
 297      tags: []
 298    },
 299
 300    // Override the `set()` method on `Backbone.Model` to handle resources as
 301    // relationships. This will now manually update the `"resouces"` collection
 302    // (using `_updateResources()`) with any `Resource` models provided rather
 303    // than replacing the key.
 304    set: function (attributes, options) {
 305      var children, validated;
 306
 307      // If not yet defined set the child collections. This will be done when
 308      // set is called for the first time in the constructor.
 309      this._createChildren();
 310
 311      // Check to see if any child keys are present in the attributes and
 312      // remove them from the object. Then update them seperately after the
 313      // parent `set()` method has been called.
 314      _.each(this.children, function (Model, key) {
 315        if (attributes && attributes[key]) {
 316          if (!(attributes[key] instanceof Backbone.Collection)) {
 317            if (!children) {
 318              children = {};
 319            }
 320            children[key] = attributes[key];
 321            delete attributes[key];
 322          }
 323        }
 324      }, this);
 325
 326      validated = Model.Base.prototype.set.call(this, attributes, options);
 327
 328      // Ensure validation passed before updating child models.
 329      if (validated && children) {
 330        this._updateChildren(children);
 331      }
 332
 333      return validated;
 334    },
 335
 336    // Checks to see if our model instance has Backbone collections defined for
 337    // child keys. If they do not exist it creates them.
 338    _createChildren: function () {
 339      _.each(this.children, function (Model, key) {
 340        if (!this.get(key)) {
 341          var newColl = new Backbone.Collection();
 342          this.attributes[key] = newColl;
 343          newColl.model = Model;
 344          // bind change events so updating the children trigger change on Dataset
 345          var self = this;
 346          // TODO: do we want to do all or be more selective
 347          newColl.bind('all', function() {
 348            self.trigger('change');
 349          });
 350        }
 351      }, this);
 352      return this;
 353    },
 354
 355    // Manages the one to many relationship between resources and the dataset.
 356    // Accepts an array of Resources (ideally model instances but will convert
 357    // object literals into resources for you). New models will be added to the
 358    // collection and existing ones updated. Any pre-existing models not found
 359    // in the new array will be removed.
 360    _updateChildren: function (children) {
 361      _.each(children, function (models, key) {
 362        var collection = this.get(key),
 363            ids = {};
 364
 365        // Add/Update models.
 366        _.each(models, function (model) {
 367          var existing = collection.get(model.id),
 368              isLiteral = !(model instanceof this.children[key]);
 369
 370          // Provide the dataset key if not already there and current model is
 371          // not a relationship.
 372          if (isLiteral && key !== 'relationships') {
 373            model.dataset = this;
 374            delete model.package_id;
 375          }
 376
 377          if (!existing) {
 378            collection.add(model);
 379          }
 380          else if (existing && isLiteral) {
 381            existing.set(model);
 382          }
 383
 384          ids[model.id] = 1;
 385        }, this);
 386
 387        // Remove missing models.
 388        collection.remove(collection.select(function (model) {
 389          return !ids[model.id];
 390        }));
 391      }, this);
 392      return this;
 393    },
 394
 395    // NOTE: Returns localised URL.
 396    toTemplateJSON: function () {
 397      var out = this.toJSON();
 398      var title = this.get('title');
 399      out.displaytitle = title ? title : 'No title ...';
 400      var notes = this.get('notes');
 401      // Don't use a global Showdown; CKAN doesn't need that library
 402      var showdown = new Showdown.converter();
 403      out.notesHtml = showdown.makeHtml(notes ? notes : '');
 404      out.snippet = this.makeSnippet(out.notesHtml);
 405      return out;
 406    },
 407
 408    makeSnippet: function (notesHtml) {
 409      var out = $(notesHtml).text();
 410      if (out.length > 190) {
 411        out = out.slice(0, 190) + ' ...';
 412      }
 413      return out;
 414    }
 415  });
 416
 417  // A model for working with resources. Each resource is _required_ to have a
 418  // parent `Dataset`. This must be provided under the `"dataset"` key when the
 419  // `Resource` is created. This is handled for you when creating resources
 420  // via the `Dataset` `set()` method.
 421  //
 422  // The `save()`, `fetch()` and `delete()` methods are mapped to the parent
 423  // dataset and can be used to update a Resource's metadata.
 424  //
 425  //     var resource = new Model.Resource({
 426  //       name: 'myresource.csv',
 427  //       url:  'http://www.example.com/myresource.csv',
 428  //       dataset: dataset
 429  //     });
 430  //
 431  //     // Updates the resource name on the server by saving the parent dataset
 432  //     resource.set({name: 'Some new name'});
 433  //
 434  Model.Resource = Model.Base.extend({
 435    constructor: function Resource() {
 436      Model.Base.prototype.constructor.apply(this, arguments);
 437    },
 438
 439    // Override the `save()` method to update the Resource with attributes then
 440    // call the parent dataset and save that. Any `options` provided will be
 441    // passed on to the dataset `save()` method.
 442    save: function (attrs, options) {
 443      var validated = this.set(attrs);
 444      if (validated) {
 445        return this.get('dataset').save({}, options);
 446      }
 447      return validated;
 448    },
 449
 450    // Override the `fetch()` method to call `fetch()` on the parent dataset.
 451    fetch: function (options) {
 452      return this.get('dataset').fetch(options);
 453    },
 454
 455    // Override the `fetch()` method to trigger the `"destroy"` event which
 456    // will remove it from any collections then save the parent dataset.
 457    destroy: function (options) {
 458      return this.trigger('destroy', this).get('dataset').save({}, options);
 459    },
 460
 461    // Override the `toJSON()` method to set the `"package_id"` key required
 462    // by the server.
 463    toJSON: function () {
 464      // Call Backbone.Model rather than Base to break the circular reference.
 465      var obj = Backbone.Model.prototype.toJSON.apply(this, arguments);
 466      if (obj.dataset) {
 467        obj.package_id = obj.dataset.id;
 468        delete obj.dataset;
 469      } else {
 470        obj.package_id = null;
 471      }
 472      return obj;
 473    },
 474
 475    toTemplateJSON: function() {
 476      var obj = Backbone.Model.prototype.toJSON.apply(this, arguments);
 477      obj.displaytitle = obj.description ? obj.description : 'No description ...';
 478      return obj;
 479    },
 480
 481    // Validates the provided attributes. Returns an object literal of
 482    // attribute/error pairs if invalid, `undefined` otherwise.
 483    validate: validator('url')
 484  });
 485
 486  // Helper function that returns a stub method that warns the devloper that
 487  // this method has not yet been implemented.
 488  function apiPlaceholder(method) {
 489    var console = window.console;
 490    return function () {
 491      if (console && console.warn) {
 492        console.warn('The method "' + method + '" has not yet been implemented');
 493      }
 494      return this;
 495    };
 496  }
 497
 498  // A model for working with relationship objects. These are currently just the
 499  // realtionship objects returned by the server wrapped in a `Base` model
 500  // instance. Currently there is no save or delete functionality.
 501  Model.Relationship = Model.Base.extend({
 502    constructor: function Relationship() {
 503      Model.Base.prototype.constructor.apply(this, arguments);
 504    },
 505
 506    // Add placeholder method that simply returns itself to all methods that
 507    // interact with the server. This will also log a warning message to the
 508    // developer into the console.
 509    save: apiPlaceholder('save'),
 510    fetch: apiPlaceholder('fetch'),
 511    destroy: apiPlaceholder('destroy'),
 512
 513    // Validates the provided attributes. Returns an object literal of
 514    // attribute/error pairs if invalid, `undefined` otherwise.
 515    validate: validator('object', 'subject', 'type')
 516  });
 517
 518  // Collection for managing results from the CKAN search API. An additional
 519  // `options.total` parameter can be provided on initialisation to
 520  // indicate how many models there are on the server in total. This can
 521  // then be accessed via the `total` property.
 522  Model.SearchCollection = Backbone.Collection.extend({
 523    constructor: function SearchCollection(models, options) {
 524      if (options) {
 525        this.total = options.total;
 526      }
 527      Backbone.Collection.prototype.constructor.apply(this, arguments);
 528    }
 529  });
 530
 531  return Model;
 532
 533}(this.jQuery, this._, this.Backbone);
 534var CKAN = CKAN || {};
 535
 536CKAN.Templates = {
 537  minorNavigationDataset: ' \
 538    <ul class="tabbed"> \
 539      <li><a href="#dataset/${dataset.id}/view">View</a></li> \
 540      <li><a href="#dataset/${dataset.id}/edit">Edit</a></li> \
 541    </ul> \
 542    '
 543};
 544var CKAN = CKAN || {};
 545
 546CKAN.View = function($) {
 547  var my = {};
 548
 549  // Flash a notification message
 550  // 
 551  // Parameters: msg, type. type is set as class on notification and should be one of success, error.
 552  // If type not defined defaults to success
 553  my.flash = function(msg, type) {
 554    if (type === undefined) {
 555      var type = 'success'
 556    }
 557    $.event.trigger('notification', [msg, type]);
 558  };
 559
 560  my.NotificationView = Backbone.View.extend({
 561    initialize: function() {
 562      $.template('notificationTemplate',
 563          '<div class="flash-banner ${type}">${message} <button>X</button></div>');
 564
 565      var self = this;
 566      $(document).bind('notification', function(e, msg, type) {
 567        self.render(msg, type)
 568      });
 569    },
 570
 571    events: {
 572      'click .flash-banner button': 'hide'
 573    },
 574
 575    render: function(msg, type) {
 576      var _out = $.tmpl('notificationTemplate', {'message': msg, 'type': type})
 577      this.el.html(_out);
 578      this.el.slideDown(400);
 579    },
 580
 581    hide: function() {
 582      this.el.slideUp(200);
 583    }
 584  });
 585
 586  my.ConfigView = Backbone.View.extend({
 587    initialize: function() {
 588      this.cfg = {};
 589      this.$ckanUrl = this.el.find('input[name=ckan-url]');
 590      this.$apikey = this.el.find('input[name=ckan-api-key]');
 591
 592      var cfg = this.options.config;
 593      this.$ckanUrl.val(cfg.endpoint);
 594      this.$apikey.val(cfg.apiKey);
 595    },
 596
 597    events: {
 598      'submit #config-form': 'updateConfig'
 599    },
 600
 601    updateConfig: function(e) {
 602      e.preventDefault();
 603      this.saveConfig();
 604      CKAN.View.flash('Saved configuration');
 605    },
 606
 607    saveConfig: function() {
 608      this.cfg = {
 609        'endpoint': this.$ckanUrl.val(),
 610        'apiKey': this.$apikey.val()
 611      };
 612      $.event.trigger('config:update', this.cfg);
 613    }
 614  });
 615
 616  my.DatasetEditView = Backbone.View.extend({
 617    initialize: function() {
 618      _.bindAll(this, 'saveData', 'render');
 619      this.model.bind('change', this.render);
 620    },
 621
 622    render: function() {
 623      tmplData = {
 624        dataset: this.model.toTemplateJSON()
 625      }
 626      var tmpl = $.tmpl(CKAN.Templates.datasetForm, tmplData);
 627      $(this.el).html(tmpl);
 628      if (tmplData.dataset.id) { // edit not add
 629        $('#minornavigation').html($.tmpl(CKAN.Templates.minorNavigationDataset, tmplData));
 630      }
 631      return this;
 632    },
 633
 634    events: {
 635      'submit form.dataset': 'saveData',
 636      'click .previewable-textarea a': 'togglePreview',
 637      'click .dataset-form-navigation a': 'showFormPart'
 638    },
 639
 640    showFormPart: function(e) {
 641      e.preventDefault();
 642      var action = $(e.target)[0].href.split('#')[1];
 643      $('.dataset-form-navigation a').removeClass('selected');
 644      $('.dataset-form-navigation a[href=#' + action + ']').addClass('selected');
 645    },
 646
 647    saveData: function(e) {
 648      e.preventDefault();
 649      this.model.set(this.getData());
 650      this.model.save({}, {
 651        success: function(model) {
 652          CKAN.View.flash('Saved dataset');
 653          window.location.hash = '#dataset/' + model.id + '/view';
 654        },
 655        error: function(model, error) {
 656          CKAN.View.flash('Error saving dataset ' + error.responseText, 'error');
 657        }
 658      });
 659    },
 660
 661    getData: function() {
 662      var _data = $(this.el).find('form.dataset').serializeArray();
 663      modelData = {};
 664      $.each(_data, function(idx, value) {
 665        modelData[value.name.split('--')[1]] = value.value
 666      });
 667      return modelData;
 668    },
 669
 670    togglePreview: function(e) {
 671      // set model data as we use it below for notesHtml
 672      this.model.set(this.getData());
 673      e.preventDefault();
 674      var el = $(e.target);
 675      var action = el.attr('action');
 676      var div = el.closest('.previewable-textarea');
 677      div.find('.tabs a').removeClass('selected');
 678      div.find('.tabs a[action='+action+']').addClass('selected');
 679      var textarea = div.find('textarea');
 680      var preview = div.find('.preview');
 681      if (action=='preview') {
 682        preview.html(this.model.toTemplateJSON().notesHtml);
 683        textarea.hide();
 684        preview.show();
 685      } else {
 686        textarea.show();
 687        preview.hide();
 688      }
 689      return false;
 690    }
 691
 692  });
 693
 694  my.DatasetFullView = Backbone.View.extend({
 695    initialize: function() {
 696      _.bindAll(this, 'render');
 697      this.model.bind('change', this.render);
 698
 699      // slightly painful but we have to set this up here so
 700      // it has access to self because when called this will
 701      // be overridden and refer to the element in dom that
 702      // was being saved
 703      var self = this;
 704      this.saveFromEditable = function(value, settings) {
 705        var _attribute = $(this).attr('backbone-attribute');
 706        var _data = {};
 707        _data[_attribute] = value;
 708        self.model.set(_data);
 709        self.model.save({}, {
 710          success: function(model) {
 711            CKAN.View.flash('Saved updated notes');
 712          },
 713          error: function(model, error) {
 714            CKAN.View.flash('Error saving notes ' + error.responseText, 'error');
 715          }
 716        });
 717        // do not worry too much about what we return here
 718        // because update of model will automatically lead to
 719        // re-render
 720        return value;
 721      };
 722    },
 723
 724    events: {
 725      'click .action-add-resource': 'showResourceAdd'
 726    },
 727
 728    render: function() {
 729      var tmplData = {
 730        domain: this.options.domain,
 731        dataset: this.model.toTemplateJSON(),
 732      };
 733      $('.page-heading').html(tmplData.dataset.displaytitle);
 734      $('#minornavigation').html($.tmpl(CKAN.Templates.minorNavigationDataset, tmplData));
 735      $('#sidebar .widget-list').html($.tmpl(CKAN.Templates.sidebarDatasetView, tmplData));
 736      this.el.html($.tmpl(CKAN.Templates.datasetView, tmplData));
 737      this.setupEditable();
 738      return this;
 739    },
 740
 741    setupEditable: function() {
 742      var self = this;
 743      this.el.find('.editable-area').editable(
 744        self.saveFromEditable, {
 745          type      : 'textarea',
 746          cancel    : 'Cancel',
 747          submit    : 'OK',
 748          tooltip   : 'Click to edit...',
 749          onblur    : 'ignore',
 750          data      : function(value, settings) {
 751            var _attribute = $(this).attr('backbone-attribute');
 752            return self.model.get(_attribute);
 753          }
 754        }
 755      );
 756    },
 757
 758    showResourceAdd: function(e) {
 759      var self = this;
 760      e.preventDefault();
 761      var $el = $('<div />').addClass('resource-add-dialog');
 762      $('body').append($el);
 763      var resource = new CKAN.Model.Resource({
 764          'dataset': self.model
 765          });
 766      function handleNewResourceSave(model) {
 767        var res = self.model.get('resources');
 768        res.add(model);
 769        $el.dialog('close');
 770        self.model.save({}, {
 771          success: function(model) {
 772            CKAN.View.flash('Saved dataset');
 773            // TODO: no need to re-render (should happen automatically)
 774            self.render();
 775          }
 776          , error: function(model, error) {
 777            CKAN.View.flash('Failed to save: ' + error, 'error');
 778          }
 779        });
 780      }
 781      resource.bind('change', handleNewResourceSave);
 782      var resourceView = new CKAN.View.ResourceCreate({
 783        el: $el,
 784        model: resource
 785      });
 786      resourceView.render();
 787      dialogOptions = {
 788        autoOpen: false,
 789        // does not seem to work for width ...
 790        position: ['center', 'center'],
 791        buttons: [],
 792        width:  660,
 793        resize: 'auto',
 794        modal: false,
 795        draggable: true,
 796        resizable: true
 797      };
 798      dialogOptions.title = 'Add Data (File, API, ...)';
 799      $el.dialog(dialogOptions);
 800      $el.dialog('open');
 801      $el.bind("dialogbeforeclose", function () {
 802        self.el.find('.resource-add-dialog').remove();
 803      });
 804    }
 805  });
 806
 807  my.DatasetSearchView = Backbone.View.extend({
 808    events: {
 809      'submit #search-form': 'onSearch'
 810    },
 811
 812    initialize: function(options) {
 813      var view = this;
 814
 815      // Temporarily provide the view with access to the client for searching.
 816      this.client = options.client;
 817      this.$results = this.el.find('.results');
 818      this.$datasetList = this.$results.find('.datasets');
 819      this.$dialog = this.el.find('.dialog');
 820
 821      this.resultView = new CKAN.View.DatasetListing({
 822        collection: new Backbone.Collection(),
 823        el: this.$datasetList
 824      });
 825
 826      _.bindAll(this, "render");
 827    },
 828
 829    render: function() {
 830      this.$('.count').html(this.totalResults);
 831      this.hideSpinner();
 832      this.$results.show();
 833      return this;
 834    },
 835
 836    onSearch: function (event) {
 837      event.preventDefault();
 838      var q = $(this.el).find('input.search').val();
 839      this.doSearch(q);
 840    },
 841
 842    doSearch: function (q) {
 843      $(this.el).find('input.search').val(q),
 844          self = this;
 845
 846      this.showSpinner();
 847      this.$results.hide();
 848      this.$results.find('.datasets').empty();
 849      this.client.searchDatasets({
 850        query: {q:q},
 851        success: function (collection) {
 852          self.totalResults = collection.total;
 853          self.resultView.setCollection(collection);
 854          self.render();
 855        }
 856      });
 857    },
 858
 859    showSpinner: function() {
 860      this.$dialog.empty();
 861      this.$dialog.html('<h2>Loading results...</h2><img src="http://assets.okfn.org/images/icons/ajaxload-circle.gif" />');
 862      this.$dialog.show();
 863    },
 864
 865    hideSpinner: function() {
 866      this.$dialog.empty().hide();
 867    }
 868  });
 869
 870  my.ResourceView = Backbone.View.extend({
 871    render: function() {
 872      var resourceData = this.model.toTemplateJSON();
 873      var resourceDetails = {};
 874      var exclude = [ 'resource_group_id',
 875        'description',
 876        'url',
 877        'position',
 878        'id',
 879        'webstore',
 880        'qa',
 881        'dataset',
 882        'displaytitle'
 883        ];
 884      $.each(resourceData, function(key, value) {
 885        if (! _.contains(exclude, key)) {
 886          resourceDetails[key] = value;
 887        }
 888      });
 889      tmplData = {
 890        dataset: this.model.get('dataset').toTemplateJSON(),
 891        resource: resourceData,
 892        resourceDetails: resourceDetails
 893      };
 894      $('.page-heading').html(tmplData.dataset.name + ' / ' + tmplData.resource.displaytitle);
 895      var tmpl = $.tmpl(CKAN.Templates.resourceView, tmplData);
 896      $(this.el).html(tmpl);
 897      return this;
 898    },
 899
 900    events: {
 901    }
 902  });
 903
 904  my.ResourceEditView = Backbone.View.extend({
 905    render: function() {
 906      var tmpl = $.tmpl(CKAN.Templates.resourceForm, this.model.toJSON());
 907      $(this.el).html(tmpl);
 908      return this;
 909    },
 910
 911    events: {
 912      'submit form': 'saveData'
 913    },
 914
 915    saveData: function() {
 916      // only set rather than save as can only save resources as part of a dataset atm
 917      this.model.set(this.getData(), {
 918        error: function(model, error) {
 919          var msg = 'Failed to save, possibly due to invalid data ';
 920          msg += JSON.stringify(error);
 921          alert(msg);
 922        }
 923      });
 924      return false;
 925    },
 926
 927    getData: function() {
 928      var _data = $(this.el).find('form.resource').serializeArray();
 929      modelData = {};
 930      $.each(_data, function(idx, value) {
 931        modelData[value.name.split('--')[1]] = value.value
 932      });
 933      return modelData;
 934    }
 935
 936  });
 937
 938  return my;
 939}(jQuery);
 940var CKAN = CKAN || {};
 941
 942CKAN.UI = function($) {
 943  var my = {};
 944
 945  my.Workspace = Backbone.Router.extend({
 946    routes: {
 947      "": "index",
 948      "search": "search",
 949      "search/:query": "search",
 950      "search/:query/p:page": "search",
 951      "dataset/:id/view": "datasetView",
 952      "dataset/:id/edit": "datasetEdit",
 953      "dataset/:datasetId/resource/:resourceId": "resourceView",
 954      "add-dataset": "datasetAdd",
 955      "add-resource": "resourceAdd",
 956      "config": "config"
 957    },
 958
 959    initialize: function(options) {
 960      var self = this;
 961      var defaultConfig = {
 962        endpoint: 'http://ckan.net',
 963        apiKey: ''
 964      };
 965
 966      var config = options.config || defaultConfig;
 967      this.client = new CKAN.Client(config);
 968      if (options.fixtures && options.fixtures.datasets) {
 969        $.each(options.fixtures.datasets, function(idx, obj) {
 970          var collection = self.client.cache.dataset;
 971          collection.add(new CKAN.Model.Dataset(obj));
 972        });
 973      }
 974
 975      var newPkg = this.client.createDataset();
 976      var newCreateView = new CKAN.View.DatasetEditView({model: newPkg, el: $('#dataset-add-page')});
 977      newCreateView.render();
 978
 979      var newResource = new CKAN.Model.Resource({
 980        dataset: newPkg
 981      });
 982      var newResourceEditView = new CKAN.View.ResourceEditView({model: newResource, el: $('#add-resource-page')});
 983      newResourceEditView.render();
 984
 985      var searchView = this.searchView =  new CKAN.View.DatasetSearchView({
 986        client: this.client,
 987        el: $('#search-page')
 988      });
 989
 990      // set up top bar search
 991      $('#menusearch').find('form').submit(function(e) {
 992        e.preventDefault();
 993        var _el = $(e.target);
 994        var _q = _el.find('input[name="q"]').val();
 995        searchView.doSearch(_q);
 996        self.search(_q);
 997      });
 998
 999
1000      var configView = new CKAN.View.ConfigView({
1001        el: $('#config-page'),
1002        config: config
1003      });
1004      $(document).bind('config:update', function(e, cfg) {
1005        self.client.configure(cfg);
1006      });
1007
1008      this.notificationView = new CKAN.View.NotificationView({
1009        el: $('.flash-banner-box')
1010      });
1011    },
1012
1013    switchView: function(view) {
1014      $('.page-view').hide();
1015      $('#sidebar .widget-list').empty();
1016      $('#minornavigation').empty();
1017      $('#' + view + '-page').show();
1018    },
1019
1020    index: function(query, page) {
1021      this.search();
1022    },
1023
1024    search: function(query, page) {
1025      this.switchView('search');
1026      $('.page-heading').html('Search');
1027    },
1028
1029    _findDataset: function(id, callback) {
1030      var pkg = this.client.getDatasetById(id);
1031
1032      if (pkg===undefined) {
1033        pkg = this.client.createDataset({id: id});
1034        pkg.fetch({
1035          success: callback,
1036          error: function() {
1037            alert('There was an error');
1038          }
1039        });
1040      } else {
1041        callback(pkg);
1042      }
1043    },
1044
1045    datasetView: function(id) {
1046      var self = this;
1047      self.switchView('view');
1048      var $viewpage = $('#view-page');
1049      this._findDataset(id, function (model) {
1050        var newView = new CKAN.View.DatasetFullView({
1051          model: model,
1052          el: $viewpage
1053        });
1054        newView.render();
1055      });
1056    },
1057
1058    datasetEdit: function(id) {
1059      this.switchView('dataset-edit');
1060      $('.page-heading').html('Edit Dataset');
1061      function _show(model) {
1062        var newView = new CKAN.View.DatasetEditView({model: model});
1063        $('#dataset-edit-page').html(newView.render().el);
1064      }
1065      this._findDataset(id, _show)
1066    },
1067
1068    datasetAdd: function() {
1069      this.switchView('dataset-add');
1070      $('.page-heading').html('Add Dataset');
1071      $('#sidebar .widget-list').empty();
1072    },
1073
1074    resourceView: function(datasetId, resourceId) {
1075      this.switchView('resource-view');
1076      var $viewpage = $('#resource-view-page');
1077      this._findDataset(datasetId, function (model) {
1078        var resource = model.get('resources').get(resourceId);
1079        var newView = new CKAN.View.ResourceView({
1080          model: resource,
1081          el: $viewpage
1082        });
1083        newView.render();
1084      });
1085    },
1086
1087    resourceAdd: function() {
1088      this.switchView('add-resource');
1089    },
1090
1091    config: function() {
1092      this.switchView('config');
1093    },
1094
1095    url: function(controller, action, id) {
1096      if (id) {
1097        return '#' + controller + '/' + id + '/' + action;
1098      } else {
1099        return '#' + controller + '/' + action;
1100      }
1101    }
1102  });
1103  
1104  my.initialize = function(options) {
1105    my.workspace = new my.Workspace(options);
1106    Backbone.history.start()
1107  };
1108
1109  return my;
1110}(jQuery);
1111
1112CKAN.Templates.datasetForm = ' \
1113  <form class="dataset" action="" method="POST"> \
1114    <dl> \
1115      <dt> \
1116        <label class="field_opt" for="dataset--title"> \
1117          Title * \
1118        </label> \
1119      </dt> \
1120      <dd> \
1121        <input id="Dataset--title" name="Dataset--title" type="text" value="${dataset.title}" placeholder="A title (not a description) ..."/> \
1122      </dd> \
1123 \
1124      <dt> \
1125        <label class="field_req" for="Dataset--name"> \
1126          Name * \
1127          <span class="hints"> \
1128            A short unique name for the dataset - used in urls and restricted to [a-z] -_ \
1129          </span> \
1130        </label> \
1131      </dt> \
1132      <dd> \
1133        <input id="Dataset--name" maxlength="100" name="Dataset--name" type="text" value="${dataset.name}" placeholder="A shortish name usable in urls ..." /> \
1134      </dd> \
1135 \
1136      <dt> \
1137        <label class="field_opt" for="Dataset--license_id"> \
1138          Licence \
1139        </label> \
1140      </dt> \
1141      <dd> \
1142        <select id="Dataset--license_id" name="Dataset--license_id"> \
1143          <option selected="selected" value=""></option> \
1144          <option value="notspecified">Other::License Not Specified</option> \
1145        </select> \
1146      </dd> \
1147 \
1148      <dt> \
1149        <label class="field_opt" for="Dataset--notes"> \
1150          Description and Notes \
1151          <span class="hints"> \
1152            (You can use <a href="http://daringfireball.net/projects/markdown/syntax">Markdown formatting</a>) \
1153          </span> \
1154        </label> \
1155      </dt> \
1156      <dd> \
1157        <div class="previewable-textarea"> \
1158          <ul class="tabs"> \
1159            <li><a href="#" action="write" class="selected">Write</a></li> \
1160            <li><a href="#" action="preview">Preview</a></li> \
1161          </ul> \
1162          <textarea id="Dataset--notes" name="Dataset--notes" placeholder="Start with a summary sentence ...">${dataset.notes}</textarea> \
1163          <div id="Dataset--notes-preview" class="preview" style="display: none;"> \
1164          <div> \
1165        </div> \
1166      </dd> \
1167    </dl> \
1168 \
1169    <div class="submit"> \
1170      <input id="save" name="save" type="submit" value="Save" /> \
1171    </div> \
1172  </form> \
1173';
1174
1175CKAN.Templates.datasetFormSidebar = ' \
1176  <div class="dataset-form-navigation"> \
1177    <ul> \
1178      <li> \
1179        <a href="#basics" class="selected">Basics</a> \
1180      </li> \
1181      <li> \
1182        <a href="#data">The Data</a> \
1183      </li> \
1184      <li> \
1185        <a href="#additional"> \
1186          Additional Information \
1187        </a> \
1188      </li> \
1189    </ul> \
1190  </div> \
1191';
1192CKAN.Templates.datasetView = ' \
1193  <div class="dataset view" dataset-id="${dataset.id}"> \
1194    <div class="extract"> \
1195      ${dataset.snippet} \
1196      {{if dataset.snippet.length > 50}} \
1197      <a href="#anchor-notes">Read more</a> \
1198      {{/if}} \
1199    </div> \
1200    <div class="tags"> \
1201      {{if dataset.tags.length}} \
1202      <ul class="dataset-tags"> \
1203        {{each dataset.tags}} \
1204          <li>${$value}</li> \
1205        {{/each}} \
1206      </ul> \
1207      {{/if}} \
1208    </div> \
1209    <div class="resources subsection"> \
1210      <h3>Resources</h3> \
1211      <table> \
1212        <tr> \
1213          <th>Description</th> \
1214          <th>Format</th> \
1215          <th>Actions</th> \
1216        </tr> \
1217        {{each dataset.resources}} \
1218        <tr> \
1219          <td> \
1220            <a href="#dataset/${dataset.id}/resource/${$value.id}"> \
1221            {{if $value.description}} \
1222            ${$value.description} \
1223            {{else}} \
1224            (No description) \
1225            {{/if}} \
1226            </a> \
1227          </td> \
1228          <td>${$value.format}</td> \
1229          <td><a href="${$value.url}" target="_blank" class="resource-download">Download</a> \
1230        </tr> \
1231        {{/each}} \
1232        {{if !dataset.resources.length }} \
1233        <tr><td>No resources.</td><td></td></tr> \
1234        {{/if}} \
1235      </table> \
1236      <div class="add-resource"> \
1237        <a href="#" class="action-add-resource">Add a resource</a> \
1238      </div> \
1239    </div> \
1240    <div class="notes subsection"> \
1241      <h3 id="anchor-notes">Notes</h3> \
1242      <div class="notes-body editable-area" backbone-attribute="notes"> \
1243        {{html dataset.notesHtml}} \
1244        {{if !dataset.notes || dataset.notes.length === 0}} \
1245        <em>No notes yet. Click to add some ...</em> \
1246        {{/if}} \
1247      </div> \
1248    </div> \
1249    <div class="details subsection"> \
1250      <h3>Additional Information</h3> \
1251      <table> \
1252        <thead> \
1253          <tr> \
1254            <th>Field</th> \
1255            <th>Value</th> \
1256          </tr> \
1257        </thead> \
1258        <tbody> \
1259          <tr> \
1260            <td>Creator</td> \
1261            <td>${dataset.author}</td> \
1262          </tr> \
1263          <tr> \
1264            <td>Maintainer</td> \
1265            <td>${dataset.maintainer}</td> \
1266          </tr> \
1267          {{each dataset.extras}} \
1268          <tr> \
1269            <td class="package-label" property="rdfs:label">${$index}</td> \
1270            <td class="package-details" property="rdf:value">${$value}</td> \
1271          </tr> \
1272          {{/each}} \
1273        </tbody> \
1274      </table> \
1275    </div> \
1276  </div> \
1277';
1278
1279CKAN.Templates.sidebarDatasetView = ' \
1280    <li class="widget-container widget_text"> \
1281      <h3>Connections</h3> \
1282      <ul> \
1283        {{each dataset.relationships}} \
1284        <li> \
1285          ${$value.type} dataset \
1286          <a href="#dataset/${$value.object}/view">${$value.object}</a> \
1287          {{if $value.comment}} \
1288          <span class="relationship_comment"> \
1289            (${$value.comment}) \
1290          </span> \
1291          {{/if}} \
1292        </li> \
1293        {{/each}} \
1294      </ul> \
1295      {{if dataset.relationships.length == 0}} \
1296      No connections to other datasets. \
1297      {{/if}} \
1298    </li> \
1299';
1300CKAN.Templates.resourceForm = ' \
1301  <form class="resource" action="" method="POST"> \
1302    <dl> \
1303      <dt> \
1304        <label class="field_opt" for="Resource--url"> \
1305          Link \
1306        </label> \
1307      </dt> \
1308      <dd> \
1309        <input id="Resource--url" name="Resource--url" type="text" value="${url}" placeholder="http://mydataset.com/file.csv" /> \
1310      </dd> \
1311      <dt> \
1312        <label class="field_opt" for="Resource--type"> \
1313          Kind \
1314        </label> \
1315      </dt> \
1316      <dd> \
1317        <select id="Resource--type" name="Resource--type"> \
1318          <option selected="selected" value="file">File</option> \
1319          <option value="api">API</option> \
1320          <option value="listing">Listing</option> \
1321          <option value="example">Example</option> \
1322        </select> \
1323      </dd> \
1324    </dl> \
1325 \
1326  <fieldset> \
1327    <legend> \
1328      <h3>Optional Info</h3> \
1329    </legend> \
1330    <dl> \
1331      <dt> \
1332        <label class="field_opt" for="Resource--description"> \
1333          Description \
1334        </label> \
1335      </dt> \
1336      <dd> \
1337        <input id="Resource--description" name="Resource--description" type="text" value="${description}" placeholder="A short description ..."/> \
1338      </dd> \
1339 \
1340 \
1341      <dt> \
1342        <label class="field_opt" for="Resource--format"> \
1343          Format \
1344        </label> \
1345      </dt> \
1346      <dd> \
1347        <input id="Resource--format" name="Resource--format" type="text" value="${format}" placeholder="e.g. csv, zip:csv (zipped csv), sparql"/> \
1348      </dd> \
1349    </fieldset> \
1350 \
1351    <div class="submit"> \
1352      <input id="save" name="save" type="submit" value="Save" /> \
1353    </div> \
1354  </form> \
1355';
1356
1357CKAN.Templates.resourceCreate = ' \
1358  <div class="resource-create"> \
1359    <table> \
1360      <tr class="heading"> \
1361        <td> \
1362          <h3>Link to data already online</h3> \
1363        </td> \
1364        <td><h3>or</h3></td> \
1365        <td><h3>Upload data</h3></td> \
1366      </tr> \
1367      <tr> \
1368        <td class="edit"></td> \
1369        <td class="separator"></td> \
1370        <td class="upload"></td> \
1371      </tr> \
1372    </table> \
1373  </div> \
1374';
1375CKAN.Templates.resourceUpload = ' \
1376<div class="fileupload"> \
1377  <form action="http://test-ckan-net-storage.commondatastorage.googleapis.com/" class="resource-upload" \
1378    enctype="multipart/form-data" \
1379    method="POST"> \
1380 \
1381    <div class="fileupload-buttonbar"> \
1382      <div class="hidden-inputs"></div> \
1383      <label class="fileinput-button"> \
1384        File \
1385      </label> \
1386      <input type="file" name="file" /> \
1387      <span class="fileinfo"></span> \
1388      <input type="submit" value="upload" /> \
1389    </div> \
1390  </form> \
1391  <div class="messages" style="display: none;"></div> \
1392</div> \
1393';
1394
1395CKAN.Templates.resourceView = ' \
1396  <div class="resource view" resource-id="${resource.id}"> \
1397    <h3> \
1398      <a href="${resource.url}" class="url">${resource.url}</a> [${resource.format}] \
1399    </h3> \
1400    <div class="description"> \
1401      ${resource.description} \
1402    </div> \
1403    \
1404    <div class="details subsection"> \
1405      <h3>Additional Information</h3> \
1406      <table> \
1407        <thead> \
1408          <tr> \
1409            <th>Field</th> \
1410            <th>Value</th> \
1411          </tr> \
1412        </thead> \
1413        <tbody> \
1414          {{each resourceDetails}} \
1415          <tr> \
1416            <td class="label">${$index}</td> \
1417            <td class="value">${$value}</td> \
1418          </tr> \
1419          {{/each}} \
1420        </tbody> \
1421      </table> \
1422    </div> \
1423  </div> \
1424';
1425this.CKAN || (this.CKAN = {});
1426this.CKAN.View || (this.CKAN.View = {});
1427
1428(function (CKAN, $, _, Backbone, undefined) {
1429  CKAN.View.DatasetListing = Backbone.View.extend({
1430    tagName: 'ul',
1431
1432    constructor: function DatasetListing() {
1433      Backbone.View.prototype.constructor.apply(this, arguments);
1434
1435      _.bindAll(this, 'addItem', 'removeItem');
1436
1437      this.el = $(this.el);
1438      this.setCollection(this.collection);
1439    },
1440
1441    setCollection: function (collection) {
1442      if (this.collection) {
1443        this.collection.unbind('add', this.addItem);
1444        this.collection.unbind('remove', this.removeItem);
1445      }
1446
1447      this.collection = collection;
1448      if (collection) {
1449        this.collection.bind('add', this.addItem);
1450        this.collection.bind('remove', this.removeItem);
1451      }
1452      return this.render();
1453    },
1454
1455    addItem: function (model) {
1456      var view = new CKAN.View.DatasetListingItem({
1457        domian: this.options.domain,
1458        model: model
1459      });
1460      this.el.data(model.cid, view).append(view.render().el);
1461      return this;
1462    },
1463
1464    removeItem: function (model) {
1465      var view = this.el.data(model.cid);
1466      if (view) {
1467        view.remove();
1468      }
1469      return this;
1470    },
1471
1472    render: function () {
1473      this.el.empty();
1474      if (this.collection) {
1475        this.collection.each(this.addItem);
1476      }
1477      return this;
1478    },
1479
1480    remove: function () {
1481      this.setCollection(null);
1482      return Backbone.View.prototype.remove.apply(this, arguments);
1483    }
1484  });
1485  
1486  CKAN.View.DatasetListingItem = Backbone.View.extend({
1487    tagName: 'li',
1488
1489    className: 'dataset summary',
1490
1491    options: {
1492      template: '\
1493        <div class="header"> \
1494          <span class="title" > \
1495            <a href="${urls.datasetView}" ckan-attrname="title" class="editable">${displaytitle}</a> \
1496          </span> \
1497          <div class="search_meta"> \
1498            {{if formats.length > 0}} \
1499            <ul class="dataset-formats"> \
1500              {{each formats}} \
1501                <li>${$value}</li> \
1502              {{/each}} \
1503            </ul> \
1504            {{/if}} \
1505          </div> \
1506        </div> \
1507        <div class="extract"> \
1508          {{html snippet}} \
1509        </div> \
1510        <div class="dataset-tags"> \
1511          {{if tags.length}} \
1512          <ul class="dataset-tags"> \
1513            {{each tags}} \
1514              <li>${$value}</li> \
1515            {{/each}} \
1516          </ul> \
1517          {{/if}} \
1518        </div> \
1519      '
1520    },
1521
1522    constructor: function DatasetListingItem() {
1523      Backbone.View.prototype.constructor.apply(this, arguments);
1524      this.el = $(this.el);
1525    },
1526
1527    render: function () {
1528      var dataset = this.model.toTemplateJSON();
1529      // if 'UI' mode ...
1530      var urls = {};
1531      if (CKAN.UI && CKAN.UI.workspace) {
1532        urls.datasetView = CKAN.UI.workspace.url('dataset', 'view', this.model.id);
1533      } else {
1534        urls.datasetView = dataset.ckan_url;
1535      }
1536      var data = _.extend(dataset, {
1537        dataset: dataset,
1538        formats: this._availableFormats(),
1539        urls: urls
1540      });
1541      this.el.html($.tmpl(this.options.template, data));
1542      return this;
1543    },
1544
1545    _availableFormats: function () {
1546      var formats = this.model.get('resources').map(function (resource) {
1547        return resource.get('format');
1548      });
1549      return _.uniq(_.compact(formats));
1550    }
1551  });
1552})(CKAN, $, _, Backbone, undefined);
1553this.CKAN || (this.CKAN = {});
1554this.CKAN.View || (this.CKAN.View = {});
1555
1556(function (CKAN, $, _, Backbone, undefined) {
1557  CKAN.View.ResourceCreate = Backbone.View.extend({
1558    initialize: function() {
1559      this.el = $(this.el);
1560      _.bindAll(this, 'renderMain');
1561      this.renderMain();
1562      this.$edit = $(this.el.find('.edit')[0]);
1563      this.$upload = $(this.el.find('.upload')[0]);
1564      this.editView = new CKAN.View.ResourceEditView({
1565        model: this.model,
1566        el: this.$edit
1567      });
1568      this.uploadView = new CKAN.View.ResourceUpload({
1569        el: this.$upload,
1570        model: this.model,
1571        // TODO: horrible reverse depedency ...
1572        client: CKAN.UI.workspace.client
1573      });
1574    },
1575
1576    renderMain: function () {
1577      this.el.empty();
1578      tmplData = {
1579      };
1580      var tmpl = $.tmpl(CKAN.Templates.resourceCreate, tmplData);
1581      this.el.html(tmpl);
1582      return this;
1583    },
1584
1585    render: function () {
1586      this.editView.render();
1587      this.uploadView.render();
1588    }
1589  });
1590
1591})(CKAN, $, _, Backbone, undefined);
1592
1593this.CKAN || (this.CKAN = {});
1594this.CKAN.View || (this.CKAN.View = {});
1595
1596(function (CKAN, $, _, Backbone, undefined) {
1597  CKAN.View.ResourceUpload = Backbone.View.extend({
1598    tagName: 'div',
1599
1600    // expects a client arguments in its options
1601    initialize: function(options) {
1602      this.el = $(this.el);
1603      this.client = options.client;
1604      _.bindAll(this, 'render', 'updateFormData', 'setMessage', 'uploadFile');
1605    },
1606
1607    events: {
1608      'click input[type="submit"]': 'uploadFile'
1609    },
1610
1611    render: function () {
1612      this.el.empty();
1613      tmplData = {
1614      }
1615      var tmpl = $.tmpl(CKAN.Templates.resourceUpload, tmplData);
1616      this.el.html(tmpl);
1617      this.$messages = this.el.find('.messages');
1618      this.setupFileUpload();
1619      return this;
1620    },
1621
1622    setupFileUpload: function() 

Large files files are truncated, but you can click here to view the full file