1 HTMLView = Marionette.ItemView.extend({
3 this.$el.append(this.options.html);
7 XOSRouter = Marionette.AppRouter.extend({
8 initialize: function() {
\r
12 onRoute: function(x,y,z) {
\r
13 this.routeStack.push(Backbone.history.fragment);
\r
16 prevPage: function() {
\r
17 return this.routeStack.slice(-2)[0];
20 showPreviousURL: function() {
21 prevPage = this.prevPage();
23 this.navigate("#"+prevPage, {trigger: false, replace: true} );
30 XOSApplication = Marionette.Application.extend({
31 detailBoxId: "#detailBox",
32 errorBoxId: "#errorBox",
33 errorCloseButtonId: "#close-error-box",
34 successBoxId: "#successBox",
35 successCloseButtonId: "#close-success-box",
36 errorTemplate: "#xos-error-template",
37 successTemplate: "#xos-success-template",
40 confirmDialog: function(view, event, callback) {
41 $("#xos-confirm-dialog").dialog({
45 "Confirm" : function() {
46 $(this).dialog("close");
54 "Cancel" : function() {
55 $(this).dialog("close");
59 $("#xos-confirm-dialog").dialog("open");
62 popupErrorDialog: function(responseText) {
64 parsed_error=$.parseJSON(responseText);
68 parsed_error=undefined;
69 width=640; // django stacktraces like wide width
72 $("#xos-error-dialog").html(templateFromId("#xos-error-response")(parsed_error));
74 $("#xos-error-dialog").html(templateFromId("#xos-error-rawresponse")({responseText: responseText}))
77 $("#xos-error-dialog").dialog({
81 Ok: function() { $(this).dialog("close"); }
86 hideLinkedItems: function(result) {
89 this["linkedObjs" + (index+1)].empty();
94 createListHandler: function(listViewName, collection_name, regionName, title) {
97 app[regionName].show(new app[listViewName]);
98 app.hideLinkedItems();
99 $("#contentTitle").html(templateFromId("#xos-title-list")({"title": title}));
101 $("#xos-listview-button-box").show();
103 $("#xos-detail-button-box").hide();
107 createAddHandler: function(detailName, collection_name, regionName, title) {
110 model = new xos[collection_name].model();
111 detailViewClass = app[detailName];
112 detailView = new detailViewClass({model: model, collection:xos[collection_name]});
113 app[regionName].show(detailView);
114 $("#xos-detail-button-box").show();
115 $("#xos-listview-button-box").hide();
119 createAddChildHandler: function(addChildName, collection_name) {
121 return function(parent_modelName, parent_fieldName, parent_id) {
122 app.Router.showPreviousURL();
124 console.log(parent_modelName);
125 console.log(parent_fieldName);
126 console.log(parent_id);
127 model = new xos[collection_name].model();
128 model.attributes[parent_fieldName] = parent_id;
130 detailViewClass = app[addChildName];
131 var detailView = new detailViewClass({model: model, collection:xos[collection_name]});
132 detailView.dialog = $("xos-addchild-dialog");
133 app["addChildDetail"].show(detailView);
134 $("#xos-addchild-dialog").dialog({
139 "Save" : function() {
140 var addDialog = this;
141 detailView.synchronous = true;
142 detailView.afterSave = function() { $(addDialog).dialog("close"); }
145 //$(this).dialog("close");
147 "Cancel" : function() {
148 $(this).dialog("close");
152 $("#xos-addchild-dialog").dialog("open");
156 createDeleteHandler: function(collection_name) {
158 return function(model_id) {
159 console.log("deleteCalled");
160 collection = xos[collection_name];
161 model = collection.get(model_id);
162 assert(model!=undefined, "failed to get model " + model_id + " from collection " + collection_name);
163 app.deleteDialog(model,"back");
167 createDetailHandler: function(detailName, collection_name, regionName, title) {
169 showModelId = function(model_id) {
170 $("#contentTitle").html(templateFromId("#xos-title-detail")({"title": title}));
172 collection = xos[collection_name];
173 model = collection.get(model_id);
174 if (model == undefined) {
175 app[regionName].show(new HTMLView({html: "failed to load object " + model_id + " from collection " + collection_name}));
177 detailViewClass = app[detailName];
178 detailView = new detailViewClass({model: model});
179 app[regionName].show(detailView);
180 detailView.showLinkedItems();
181 $("#xos-detail-button-box").show();
182 $("#xos-listview-button-box").hide();
188 /* error handling callbacks */
190 hideError: function() {
191 if (this.logWindowId) {
193 $(this.errorBoxId).hide();
194 $(this.successBoxId).hide();
198 showSuccess: function(result) {
199 result["statusclass"] = "success";
200 if (this.logTableId) {
201 this.appendLogWindow(result);
203 $(this.successBoxId).show();
204 $(this.successBoxId).html(_.template($(this.successTemplate).html())(result));
206 $(this.successCloseButtonId).unbind().bind('click', function() {
207 $(that.successBoxId).hide();
212 showError: function(result) {
213 result["statusclass"] = "failure";
214 if (this.logTableId) {
215 this.appendLogWindow(result);
216 this.popupErrorDialog(result.responseText);
218 // this is really old stuff
219 $(this.errorBoxId).show();
220 $(this.errorBoxId).html(_.template($(this.errorTemplate).html())(result));
222 $(this.errorCloseButtonId).unbind().bind('click', function() {
223 $(that.errorBoxId).hide();
228 showInformational: function(result) {
229 result["statusclass"] = "inprog";
230 if (this.logTableId) {
231 return this.appendLogWindow(result);
237 appendLogWindow: function(result) {
238 // compute a new logMessageId for this log message
239 logMessageId = "logMessage" + this.logMessageCount;
240 this.logMessageCount = this.logMessageCount + 1;
241 result["logMessageId"] = logMessageId;
243 logMessageTemplate=$("#xos-log-template").html();
244 assert(logMessageTemplate != undefined, "logMessageTemplate is undefined");
245 newRow = _.template(logMessageTemplate, result);
246 assert(newRow != undefined, "newRow is undefined");
248 if (result["infoMsgId"] != undefined) {
249 // We were passed the logMessageId of an informational message,
250 // and the caller wants us to replace that message with our own.
251 // i.e. replace an informational message with a success or an error.
252 $("#"+result["infoMsgId"]).replaceWith(newRow);
254 // Create a brand new log message rather than replacing one.
255 logTableBody = $(this.logTableId + " tbody");
256 logTableBody.prepend(newRow);
259 if (this.statusMsgId) {
260 $(this.statusMsgId).html( templateFromId("#xos-status-template")(result) );
263 limitTableRows(this.logTableId, 5);
268 saveError: function(model, result, xhr, infoMsgId) {
269 console.log("saveError");
270 result["what"] = "save " + model.modelName + " " + model.attributes.humanReadableName;
271 result["infoMsgId"] = infoMsgId;
272 this.showError(result);
275 saveSuccess: function(model, result, xhr, infoMsgId, addToCollection) {
276 console.log("saveSuccess");
277 if (model.addToCollection) {
278 console.log("addToCollection");
279 model.addToCollection.add(model);
280 model.addToCollection.sort();
281 model.addToCollection = undefined;
283 result = {status: xhr.xhr.status, statusText: xhr.xhr.statusText};
284 result["what"] = "save " + model.modelName + " " + model.attributes.humanReadableName;
285 result["infoMsgId"] = infoMsgId;
286 this.showSuccess(result);
289 destroyError: function(model, result, xhr, infoMsgId) {
290 result["what"] = "destroy " + model.modelName + " " + model.attributes.humanReadableName;
291 result["infoMsgId"] = infoMsgId;
292 this.showError(result);
295 destroySuccess: function(model, result, xhr, infoMsgId) {
296 result = {status: xhr.xhr.status, statusText: xhr.xhr.statusText};
297 result["what"] = "destroy " + model.modelName + " " + model.attributes.humanReadableName;
298 result["infoMsgId"] = infoMsgId;
299 this.showSuccess(result);
302 /* end error handling callbacks */
304 destroyModel: function(model) {
305 //console.log("destroyModel"); console.log(model);
307 var infoMsgId = this.showInformational( {what: "destroy " + model.modelName + " " + model.attributes.humanReadableName, status: "", statusText: "in progress..."} );
309 model.destroy({error: function(model, result, xhr) { that.destroyError(model,result,xhr,infoMsgId);},
310 success: function(model, result, xhr) { that.destroySuccess(model,result,xhr,infoMsgId);}});
313 deleteDialog: function(model, afterDelete) {
316 console.log(Backbone.history.fragment);
317 assert(model!=undefined, "deleteDialog's model is undefined");
318 //console.log("deleteDialog"); console.log(model);
319 this.confirmDialog(null, null, function() {
320 //console.log("deleteConfirm"); console.log(model);
321 modelName = model.modelName;
322 that.destroyModel(model);
323 if (afterDelete=="list") {
324 that.navigate("list", modelName);
325 } else if (afterDelete=="back") {
326 that.Router.showPreviousURL();
335 app - MarionetteApplication
336 template - template (See XOSHelper.html)
339 XOSDetailView = Marionette.ItemView.extend({
342 events: {"click button.btn-xos-save-continue": "submitContinueClicked",
343 "click button.btn-xos-save-leave": "submitLeaveClicked",
344 "click button.btn-xos-save-another": "submitAddAnotherClicked",
345 "click button.btn-xos-delete": "deleteClicked",
346 "change input": "inputChanged"},
348 /* inputChanged is watching the onChange events of the input controls. We
349 do this to track when this view is 'dirty', so we can throw up a warning
350 if the user tries to change his slices without saving first.
353 initialize: function() {
354 this.on("saveSuccess", this.afterSave);
355 this.synchronous = false;
358 afterSave: function(e) {
361 inputChanged: function(e) {
365 submitContinueClicked: function(e) {
366 console.log("saveContinue");
368 this.afterSave = function() {};
372 submitLeaveClicked: function(e) {
373 console.log("saveLeave");
376 this.afterSave = function() {
377 that.app.navigate("list", that.model.modelName);
382 submitAddAnotherClicked: function(e) {
383 console.log("saveAnother");
386 this.afterSave = function() {
387 that.app.navigate("add", that.model.modelName);
393 this.app.hideError();
394 var data = Backbone.Syphon.serialize(this);
396 var isNew = !this.model.id;
398 this.$el.find(".help-inline").remove();
400 /* although model.validate() is called automatically by
401 model.save, we call it ourselves, so we can throw up our
402 validation error before creating the infoMsg in the log
404 errors = this.model.xosValidate(data);
406 this.onFormDataInvalid(errors);
411 this.model.attributes.humanReadableName = "new " + model.modelName;
412 this.model.addToCollection = this.collection;
414 this.model.addToCollection = undefined;
417 var infoMsgId = this.app.showInformational( {what: "save " + model.modelName + " " + model.attributes.humanReadableName, status: "", statusText: "in progress..."} );
419 this.model.save(data, {error: function(model, result, xhr) { that.app.saveError(model,result,xhr,infoMsgId);},
420 success: function(model, result, xhr) { that.app.saveSuccess(model,result,xhr,infoMsgId);
421 if (that.synchronous) {
422 that.trigger("saveSuccess");
427 if (!this.synchronous) {
432 deleteClicked: function(e) {
434 this.app.deleteDialog(this.model, "list");
437 tabClick: function(tabId, regionName) {
438 region = this.app[regionName];
439 if (this.currentTabRegion != undefined) {
440 this.currentTabRegion.$el.hide();
442 if (this.currentTabId != undefined) {
443 $(this.currentTabId).removeClass('active');
445 this.currentTabRegion = region;
446 this.currentTabRegion.$el.show();
448 this.currentTabId = tabId;
449 $(tabId).addClass('active');
452 showTabs: function(tabs) {
453 template = templateFromId("#xos-tabs-template", {tabs: tabs});
454 $("#tabs").html(template(tabs));
457 _.each(tabs, function(tab) {
458 var regionName = tab["region"];
459 var tabId = '#xos-nav-'+regionName;
460 $(tabId).bind('click', function() { that.tabClick(tabId, regionName); });
466 showLinkedItems: function() {
469 tabs.push({name: "details", region: "detail"});
472 for (relatedName in this.model.collection.relatedCollections) {
473 relatedField = this.model.collection.relatedCollections[relatedName];
474 regionName = "linkedObjs" + (index+1);
476 relatedListViewClassName = relatedName + "ListView";
477 assert(this.app[relatedListViewClassName] != undefined, relatedListViewClassName + " not found");
478 relatedListViewClass = this.app[relatedListViewClassName].extend({collection: xos[relatedName].filterBy(relatedField,this.model.id),
479 parentModel: this.model});
480 this.app[regionName].show(new relatedListViewClass());
481 if (this.app.hideTabsByDefault) {
482 this.app[regionName].$el.hide();
484 tabs.push({name: relatedName, region: regionName});
489 this.app["linkedObjs" + (index+1)].empty();
494 this.tabClick('#xos-nav-detail', 'detail');
497 onFormDataInvalid: function(errors) {
499 var markErrors = function(value, key) {
500 console.log("name='" + key + "'");
501 var $inputElement = self.$el.find("[name='" + key + "']");
502 var $inputContainer = $inputElement.parent();
503 //$inputContainer.find(".help-inline").remove();
504 var $errorEl = $("<span>", {class: "help-inline error", text: value});
505 $inputContainer.append($errorEl).addClass("error");
507 _.each(errors, markErrors);
510 templateHelpers: function() { return { modelName: this.model.modelName,
511 collectionName: this.model.collectionName,
512 addFields: this.model.addFields,
513 detailFields: this.model.detailFields,
514 foreignFields: this.model.foreignFields,
515 inputType: this.model.inputType,
522 This is for items that will be displayed as table rows.
524 app - MarionetteApplication
525 template - template (See XOSHelper.html)
526 detailClass - class of detail view, probably an XOSDetailView
529 XOSItemView = Marionette.ItemView.extend({
531 className: 'test-tablerow',
533 templateHelpers: function() { return { modelName: this.model.modelName,
534 collectionName: this.model.collectionName,
535 addFields: this.model.addFields,
536 detailFields: this.model.detailFields,
537 foreignFields: this.model.foreignFields,
538 inputType: this.model.inputType,
545 app - MarionetteApplication
546 childView - class of ItemView, probably an XOSItemView
547 template - template (see xosHelper.html)
548 collection - collection that holds these objects
549 title - title to display in template
552 XOSListView = Marionette.CompositeView.extend({
553 childViewContainer: 'tbody',
556 events: {"click button.btn-xos-add": "addClicked",
557 "click button.btn-xos-refresh": "refreshClicked",
560 _fetchStateChange: function() {
561 if (this.collection.fetching) {
562 $("#xos-list-title-spinner").show();
564 $("#xos-list-title-spinner").hide();
568 addClicked: function(e) {
570 this.app.Router.navigate("add" + firstCharUpper(this.collection.modelName), {trigger: true});
573 refreshClicked: function(e) {
575 this.collection.refresh(refreshRelated=true);
578 initialize: function() {
579 this.listenTo(this.collection, 'change', this._renderChildren)
580 this.listenTo(this.collection, 'fetchStateChange', this._fetchStateChange);
582 // Because many of the templates use idToName(), we need to
583 // listen to the collections that hold the names for the ids
584 // that we want to display.
585 for (i in this.collection.foreignCollections) {
586 foreignName = this.collection.foreignCollections[i];
587 if (xos[foreignName] == undefined) {
588 console.log("Failed to find xos class " + foreignName);
590 this.listenTo(xos[foreignName], 'change', this._renderChildren);
591 this.listenTo(xos[foreignName], 'sort', this._renderChildren);
595 getAddChildHash: function() {
596 if (this.parentModel) {
597 // Find the field name in the model that should point to
598 // the parent object. For example, when adding a sliver, the
599 // fieldName that should point to 'users' is 'creator'.
600 parentFieldName = "unknown";
601 for (fieldName in this.collection.foreignFields) {
602 cname = this.collection.foreignFields[fieldName];
603 if (cname = this.collection.collectionName) {
604 parentFieldName = fieldName;
607 return "#addChild" + firstCharUpper(this.collection.modelName) + "/" + this.parentModel.modelName + "/" + parentFieldName + "/" + this.parentModel.id; // modelName, fieldName, id
613 templateHelpers: function() {
614 return { title: this.title,
615 addChildHash: this.getAddChildHash() };
619 /* Give an id, the name of a collection, and the name of a field for models
620 within that collection, lookup the id and return the value of the field.
623 idToName = function(id, collectionName, fieldName) {
624 linkedObject = xos[collectionName].get(id);
625 if (linkedObject == undefined) {
628 return linkedObject.attributes[fieldName];
632 /* Constructs lists of <option> html blocks for items in a collection.
634 selectedId = the id of an object that should be selected, if any
635 collectionName = name of collection
636 fieldName = name of field within models of collection that will be displayed
639 idToOptions = function(selectedId, collectionName, fieldName) {
641 for (index in xos[collectionName].models) {
642 linkedObject = xos[collectionName].models[index];
643 linkedId = linkedObject["id"];
644 linkedName = linkedObject.attributes[fieldName];
645 if (linkedId == selectedId) {
646 selected = " selected";
650 result = result + '<option value="' + linkedId + '"' + selected + '>' + linkedName + '</option>';
655 /* Constructs an html <select> and the <option>s to go with it.
657 variable = variable name to return to form
658 selectedId = the id of an object that should be selected, if any
659 collectionName = name of collection
660 fieldName = name of field within models of collection that will be displayed
663 idToSelect = function(variable, selectedId, collectionName, fieldName) {
664 result = '<select name="' + variable + '">' +
665 idToOptions(selectedId, collectionName, fieldName) +