Skip to content
GitLab
Explore
Sign in
Primary navigation
Search or go to…
Project
C
Core
Manage
Activity
Members
Labels
Plan
Issues
Issue boards
Milestones
Wiki
Code
Merge requests
Repository
Branches
Commits
Tags
Repository graph
Compare revisions
Snippets
Deploy
Releases
Container registry
Model registry
Monitor
Incidents
Service Desk
Analyze
Value stream analytics
Contributor analytics
Repository analytics
Model experiments
Help
Help
Support
GitLab documentation
Compare GitLab plans
Community forum
Contribute to GitLab
Provide feedback
Keyboard shortcuts
?
Snippets
Groups
Projects
Show more breadcrumbs
justinfreeman (Agileware)
Core
Commits
870a9447
Commit
870a9447
authored
11 years ago
by
Tim Otten
Browse files
Options
Downloads
Plain Diff
Merge pull request #1251 from totten/master-bb-issaved
CRM-12934 - Multiple Backbone updates
parents
0824d2bb
4630e5b5
Branches
Branches containing commit
Tags
Tags containing commit
No related merge requests found
Changes
2
Hide whitespace changes
Inline
Side-by-side
Showing
2 changed files
js/crm.backbone.js
+179
-3
179 additions, 3 deletions
js/crm.backbone.js
tests/qunit/crm-backbone/test.js
+33
-13
33 additions, 13 deletions
tests/qunit/crm-backbone/test.js
with
212 additions
and
16 deletions
js/crm.backbone.js
+
179
−
3
View file @
870a9447
...
...
@@ -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
...
...
@@ -34,6 +34,13 @@
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
;
...
...
@@ -113,10 +120,132 @@
};
/**
* Configure a model class to track whether a model has unsaved changes.
*
* 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
* Backbone. Instead, attach an event listener to the 'saved' event.
*
* @param ModelClass
*/
CRM
.
Backbone
.
trackSaved
=
function
(
ModelClass
)
{
// Retain references to some of the original class's functions
var
Parent
=
_
.
pick
(
ModelClass
.
prototype
,
'
initialize
'
,
'
save
'
,
'
fetch
'
);
// Private callback
var
onSyncSuccess
=
function
()
{
this
.
_modified
=
false
;
if
(
this
.
_oldModified
.
length
>
0
)
{
this
.
_oldModified
.
pop
();
}
this
.
trigger
(
'
saved
'
,
this
,
this
.
isSaved
());
};
var
onSaveError
=
function
()
{
if
(
this
.
_oldModified
.
length
>
0
)
{
this
.
_modified
=
this
.
_oldModified
.
pop
();
this
.
trigger
(
'
saved
'
,
this
,
this
.
isSaved
());
}
};
// Defaults - if specified in ModelClass, preserve
_
.
defaults
(
ModelClass
.
prototype
,
{
isSaved
:
function
()
{
var
result
=
!
this
.
isNew
()
&&
!
this
.
_modified
;
return
result
;
},
_saved_onchange
:
function
(
model
,
options
)
{
if
(
options
.
parse
)
return
;
this
.
setModified
();
},
setModified
:
function
()
{
var
oldModified
=
this
.
_modified
;
this
.
_modified
=
true
;
if
(
!
oldModified
)
{
this
.
trigger
(
'
saved
'
,
this
,
this
.
isSaved
());
}
}
});
// Overrides - if specified in ModelClass, replace
_
.
extend
(
ModelClass
.
prototype
,
{
initialize
:
function
(
options
)
{
this
.
_modified
=
false
;
this
.
_oldModified
=
[];
this
.
listenTo
(
this
,
'
change
'
,
this
.
_saved_onchange
);
this
.
listenTo
(
this
,
'
error
'
,
onSaveError
);
this
.
listenTo
(
this
,
'
sync
'
,
onSyncSuccess
);
if
(
Parent
.
initialize
)
{
return
Parent
.
initialize
.
apply
(
this
,
arguments
);
}
},
save
:
function
()
{
// we'll assume success
this
.
_oldModified
.
push
(
this
.
_modified
);
return
Parent
.
save
.
apply
(
this
,
arguments
);
},
fetch
:
function
()
{
this
.
_oldModified
.
push
(
this
.
_modified
);
return
Parent
.
fetch
.
apply
(
this
,
arguments
);
}
});
};
/**
* Configure a model class to support client-side soft deletion.
* One can call "model.setDeleted(BOOLEAN)" to flag an entity for
* 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(model, is_deleted) -- change value of is_deleted
*
* @param ModelClass
*/
CRM
.
Backbone
.
trackSoftDelete
=
function
(
ModelClass
)
{
// Retain references to some of the original class's functions
var
Parent
=
_
.
pick
(
ModelClass
.
prototype
,
'
save
'
);
// Defaults - if specified in ModelClass, preserve
_
.
defaults
(
ModelClass
.
prototype
,
{
is_soft_deleted
:
false
,
setSoftDeleted
:
function
(
is_deleted
)
{
if
(
this
.
is_soft_deleted
!=
is_deleted
)
{
this
.
is_soft_deleted
=
is_deleted
;
this
.
trigger
(
'
softDelete
'
,
this
,
is_deleted
);
if
(
this
.
setModified
)
this
.
setModified
();
// FIXME: ugly interaction, trackSoftDelete-trackSaved
}
},
isSoftDeleted
:
function
()
{
return
this
.
is_soft_deleted
;
}
});
// Overrides - if specified in ModelClass, replace
_
.
extend
(
ModelClass
.
prototype
,
{
save
:
function
(
attributes
,
options
)
{
if
(
this
.
isSoftDeleted
())
{
return
this
.
destroy
(
options
);
}
else
{
return
Parent
.
save
.
apply
(
this
,
arguments
);
}
}
});
};
/**
* 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
...
...
@@ -132,6 +261,9 @@
* crmCriteria: {contact_type: 'Organization'}
* });
* c.fetch();
* c.get(123).set('property', 'value');
* c.get(456).setDeleted(true);
* c.save();
* @endcode
*
* @param Class CollectionClass
...
...
@@ -143,7 +275,30 @@
_
.
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
...
...
@@ -157,6 +312,18 @@
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
;
}
});
};
...
...
@@ -315,4 +482,13 @@
}
});
*/
// 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
);
This diff is collapsed.
Click to expand it.
tests/qunit/crm-backbone/test.js
+
33
−
13
View file @
870a9447
...
...
@@ -5,6 +5,7 @@ var MALFORMED_CONTACT_ID = 'z';
var
ContactModel
=
Backbone
.
Model
.
extend
({});
CRM
.
Backbone
.
extendModel
(
ContactModel
,
'
Contact
'
);
CRM
.
Backbone
.
trackSaved
(
ContactModel
);
var
ContactCollection
=
Backbone
.
Collection
.
extend
({
model
:
ContactModel
...
...
@@ -80,27 +81,33 @@ asyncTest("create/read/delete/read (ok)", function() {
first_name
:
"
George
"
+
TOKEN
,
last_name
:
"
Anon
"
+
TOKEN
});
equal
(
c1
.
isSaved
(),
false
,
""
);
// Create the new contact
c1
.
save
({},
{
error
:
onUnexpectedError
,
success
:
function
()
{
equal
(
c1
.
get
(
"
first_name
"
),
"
George
"
+
TOKEN
,
"
save() should return new first name
"
);
equal
(
c1
.
isSaved
(),
true
,
""
);
// Fetch the newly created contact
var
c2
=
new
ContactModel
({
id
:
c1
.
get
(
'
id
'
)});
equal
(
c2
.
isSaved
(),
true
,
""
);
c2
.
fetch
({
error
:
onUnexpectedError
,
success
:
function
()
{
equal
(
c2
.
get
(
"
first_name
"
),
c1
.
get
(
"
first_name
"
),
"
fetch() should return first name
"
);
equal
(
c2
.
isSaved
(),
true
,
""
);
// Destroy the newly created contact
c2
.
destroy
({
error
:
onUnexpectedError
,
success
:
function
()
{
equal
(
c2
.
isSaved
(),
true
,
""
);
// Attempt (but fail) to fetch the deleted contact
var
c3
=
new
ContactModel
({
id
:
c1
.
get
(
'
id
'
)});
equal
(
c3
.
isSaved
(),
true
,
""
);
c3
.
fetch
({
success
:
onUnexpectedSuccess
,
error
:
function
(
model
,
error
)
{
...
...
@@ -139,20 +146,27 @@ module('model - update');
asyncTest
(
"
update (ok)
"
,
function
()
{
var
NICKNAME
=
"
George
"
+
new
Date
().
getTime
();
var
c
=
new
ContactModel
({
id
:
VALID_CONTACT_ID
});
c
.
save
({
equal
(
c
.
isSaved
(),
true
,
""
);
c
.
set
({
nick_name
:
NICKNAME
},
{
});
equal
(
c
.
isSaved
(),
false
,
""
);
c
.
save
({},
{
error
:
onUnexpectedError
,
success
:
function
()
{
equal
(
c
.
get
(
"
nick_name
"
),
NICKNAME
,
"
save() should return new nickname
"
);
var
c2
=
new
ContactModel
({
id
:
VALID_CONTACT_ID
});
c2
.
fetch
({
error
:
onUnexpectedError
,
success
:
function
()
{
equal
(
c2
.
get
(
"
nick_name
"
),
NICKNAME
,
"
fetch() should return new nickname
"
);
start
();
}
_
.
defer
(
function
(){
equal
(
c
.
isSaved
(),
true
,
""
);
// read back - make sure the save worked
var
c2
=
new
ContactModel
({
id
:
VALID_CONTACT_ID
});
c2
.
fetch
({
error
:
onUnexpectedError
,
success
:
function
()
{
equal
(
c2
.
get
(
"
nick_name
"
),
NICKNAME
,
"
fetch() should return new nickname
"
);
start
();
}
});
});
}
});
...
...
@@ -161,13 +175,19 @@ asyncTest("update (ok)", function() {
asyncTest
(
"
update (error)
"
,
function
()
{
var
NICKNAME
=
"
George
"
+
new
Date
().
getTime
();
var
c
=
new
ContactModel
({
id
:
VALID_CONTACT_ID
});
c
.
save
({
equal
(
c
.
isSaved
(),
true
,
""
);
c
.
set
({
contact_type
:
'
Not-a.va+lidConta(ype
'
},
{
});
equal
(
c
.
isSaved
(),
false
,
""
);
c
.
save
({},
{
success
:
onUnexpectedSuccess
,
error
:
function
(
model
,
error
)
{
assertApiError
(
error
);
start
();
_
.
defer
(
function
(){
equal
(
c
.
isSaved
(),
false
,
""
);
start
();
});
}
});
});
...
...
This diff is collapsed.
Click to expand it.
Preview
0%
Loading
Try again
or
attach a new file
.
Cancel
You are about to add
0
people
to the discussion. Proceed with caution.
Finish editing this message first!
Save comment
Cancel
Please
register
or
sign in
to comment