-- Indentation fixes
[civicrm-core.git] / js / crm.backbone.js
index 9d5e2cba7e322cfeac00ce0ef585086058c1215e..746ddaf7be3559ee8aa0d9529885de9a8b85d71a 100644 (file)
@@ -8,7 +8,7 @@
    * To load collections using API queries, set the "crmCriteria" property or override the
    * method "toCrmCriteria".
    *
-   * @param method
+   * @param method Accepts normal Backbone.sync methods; also accepts "crm-replace"
    * @param model
    * @param options
    * @see tests/qunit/crm-backbone
         case 'read':
           CRM.api(model.crmEntityName, 'get', model.toCrmCriteria(), apiOptions);
           break;
+        // replace all entities matching "x.crmCriteria" with new entities in "x.models"
+        case 'crm-replace':
+          var params = this.toCrmCriteria();
+          params.version = 3;
+          params.values = this.toJSON();
+          CRM.api(model.crmEntityName, 'replace', params, apiOptions);
+          break;
         default:
           apiOptions.error({is_error: 1, error_message: "CRM.Backbone.sync(" + method + ") not implemented for collections"});
           break;
       switch (method) {
         case 'create': // pass-through
         case 'update':
-          CRM.api(model.crmEntityName, 'create', model.toJSON(), apiOptions);
+          var params = model.toJSON();
+          params.options || (params.options = {});
+          params.options.reload = 1;
+          if (!model._isDuplicate) {
+            CRM.api(model.crmEntityName, 'create', params, apiOptions);
+          } else {
+            CRM.api(model.crmEntityName, 'duplicate', params, apiOptions);
+          }
           break;
         case 'read':
         case 'delete':
       crmEntityName: crmEntityName,
       toCrmCriteria: function() {
         return (this.get('id')) ? {id: this.get('id')} : {};
+      },
+      duplicate: function() {
+        var newModel = new ModelClass(this.toJSON());
+        newModel._isDuplicate = true;
+        if (newModel.setModified) newModel.setModified();
+        newModel.listenTo(newModel, 'sync', function(){
+          // may get called on subsequent resaves -- don't care!
+          delete newModel._isDuplicate;
+        });
+        return newModel;
       }
     });
     // Overrides - if specified in ModelClass, replace
   /**
    * Configure a model class to track whether a model has unsaved changes.
    *
-   * The ModelClass will be extended with:
-   *  - Method: isSaved() - true if there have been no changes to the data since the last fetch or save
-   *  - Event: saved(object model, bool is_saved) - triggered whenever isSaved() value would change
+   * Methods:
+   *  - setModified() - flag the model as modified/dirty
+   *  - isSaved() - return true if there have been no changes to the data since the last fetch or save
+   * Events:
+   *  - saved(object model, bool is_saved) - triggered whenever isSaved() value would change
    *
    *  Note: You should not directly call isSaved() within the context of the success/error/sync callback;
    *  I haven't found a way to make isSaved() behave correctly within these callbacks without patching
     // Defaults - if specified in ModelClass, preserve
     _.defaults(ModelClass.prototype, {
       isSaved: function() {
-        var result = !this.isNew() && !this._modified;
+        var result = !this.isNew() && !this.isModified();
         return result;
       },
+      isModified: function() {
+        return this._modified;
+      },
       _saved_onchange: function(model, options) {
         if (options.parse) return;
+        // console.log('change', model.changedAttributes(), model.previousAttributes());
         this.setModified();
       },
       setModified: function() {
    * deletion (or not) -- however, deletion will be deferred until save()
    * is called.
    *
+   * Methods:
+   *   setSoftDeleted(boolean) - flag the model as deleted (or not-deleted)
+   *   isSoftDeleted() - determine whether model has been soft-deleted
    * Events:
-   *   softDelete: function(model, is_deleted) -- change value of is_deleted
+   *   softDelete(model, is_deleted) -- change value of is_deleted
    *
    * @param ModelClass
    */
    * Connect a "collection" class to CiviCRM's APIv3
    *
    * Note: the collection supports a special property, crmCriteria, which is an array of
-   * query options to send to the API
+   * query options to send to the API.
    *
    * @code
    * // Setup class
    *   crmCriteria: {contact_type: 'Organization'}
    * });
    * c.fetch();
+   * c.get(123).set('property', 'value');
+   * c.get(456).setDeleted(true);
+   * c.save();
    * @endcode
    *
    * @param Class CollectionClass
     _.defaults(CollectionClass.prototype, {
       crmEntityName: CollectionClass.prototype.model.prototype.crmEntityName,
       toCrmCriteria: function() {
-        return this.crmCriteria || {};
+        return (this.crmCriteria) ? _.extend({}, this.crmCriteria) : {};
+      },
+
+      /**
+       * Reconcile the server's collection with the client's collection.
+       * New/modified items from the client will be saved/updated on the
+       * server. Deleted items from the client will be deleted on the
+       * server.
+       *
+       * @param Object options - accepts "success" and "error" callbacks
+       */
+      save: function(options) {
+        options || (options = {});
+        var collection = this;
+        var success = options.success;
+        options.success = function(resp) {
+          // Ensure attributes are restored during synchronous saves.
+          collection.reset(resp, options);
+          if (success) success(collection, resp, options);
+          // collection.trigger('sync', collection, resp, options);
+        };
+        wrapError(collection, options);
+
+        return this.sync('crm-replace', this, options)
       }
     });
     // Overrides - if specified in CollectionClass, replace
         if (origInit) {
           return origInit.apply(this, arguments);
         }
+      },
+      toJSON: function() {
+        var result = [];
+        // filter models list, excluding any soft-deleted items
+        this.each(function(model) {
+          // if model doesn't track soft-deletes
+          // or if model tracks soft-deletes and wasn't soft-deleted
+          if (!model.isSoftDeleted || !model.isSoftDeleted()) {
+            result.push(model.toJSON());
+          }
+        });
+        return result;
       }
     });
   };
     }
   });
   */
+
+  // Wrap an optional error callback with a fallback error event.
+  var wrapError = function (model, options) {
+    var error = options.error;
+    options.error = function(resp) {
+      if (error) error(model, resp, options);
+      model.trigger('error', model, resp, options);
+    };
+  };
 })(cj);