1 HTMLView = Marionette.ItemView.extend({
3 this.$el.append(this.options.html);
7 FilteredCompositeView = Marionette.CompositeView.extend( {
8 showCollection: function() {
10 this.collection.each(function(child, index) {
11 filterFunc = this.options.filter || this.filter;
12 if (filterFunc && !filterFunc(child)) {
15 ChildView = this.getChildView(child);
16 this.addChild(child, ChildView, index);
22 SliceSelectorOption = Marionette.ItemView.extend({
23 template: "#xos-sliceselector-option",
25 attributes: function() {
26 if (this.options.selectedID == this.model.get("id")) {
27 return { value: this.model.get("id"), selected: 1 };
29 return { value: this.model.get("id") };
34 SliceSelectorView = FilteredCompositeView.extend({
35 template: "#xos-sliceselector-select",
36 childViewContainer: "select",
37 childView: SliceSelectorOption,
40 events: {"change select": "onSliceChanged"},
42 childViewOptions: function() {
43 return { selectedID: this.options.selectedID || this.selectedID || null };
46 onSliceChanged: function() {
47 this.sliceChanged(this.$el.find("select").val());
50 sliceChanged: function(id) {
51 console.log("sliceChanged " + id);
54 templateHelpers: function() { return {caption: this.options.caption || this.caption }; },
57 XOSRouter = Marionette.AppRouter.extend({
58 initialize: function() {
\r
62 onRoute: function(x,y,z) {
\r
63 this.routeStack.push(Backbone.history.fragment);
\r
64 this.routeStack = this.routeStack.slice(-32); // limit the size of routeStack to something reasonable
\r
67 prevPage: function() {
\r
68 return this.routeStack.slice(-1)[0];
71 showPreviousURL: function() {
72 prevPage = this.prevPage();
73 //console.log("showPreviousURL");
74 //console.log(this.routeStack);
76 this.navigate("#"+prevPage, {trigger: false, replace: true} );
80 navigate: function(href, options) {
82 Marionette.AppRouter.prototype.navigate.call(this, "nowhere", {trigger: false, replace: true});
84 Marionette.AppRouter.prototype.navigate.call(this, href, options);
88 // XXX - We import backbone multiple times (BAD!) since the import happens
\r
89 // inside of the view's html. The second time it's imported (developer
\r
90 // view), it wipes out Backbone.Syphon. So, save it as Backbone_Syphon for
\r
92 Backbone_Syphon = Backbone.Syphon
\r
93 Backbone_Syphon.InputReaders.register('select', function(el) {
\r
94 // Modify syphon so that if a select has "syphonall" in the class, then
95 // the value of every option will be returned, regardless of whether of
96 // not it is selected.
97 if (el.hasClass("syphonall")) {
99 _.each(el.find("option"), function(option) {
100 result.push($(option).val());
107 XOSApplication = Marionette.Application.extend({
108 detailBoxId: "#detailBox",
109 errorBoxId: "#errorBox",
110 errorCloseButtonId: "#close-error-box",
111 successBoxId: "#successBox",
112 successCloseButtonId: "#close-success-box",
113 errorTemplate: "#xos-error-template",
114 successTemplate: "#xos-success-template",
117 confirmDialog: function(view, event, callback) {
118 $("#xos-confirm-dialog").dialog({
122 "Confirm" : function() {
123 $(this).dialog("close");
131 "Cancel" : function() {
132 $(this).dialog("close");
136 $("#xos-confirm-dialog").dialog("open");
139 popupErrorDialog: function(responseText) {
141 parsed_error=$.parseJSON(responseText);
145 parsed_error=undefined;
146 width=640; // django stacktraces like wide width
148 console.log(responseText);
149 console.log(parsed_error);
151 if (parsed_error && ("error" in parsed_error)) {
152 // this error comes from genapi views
153 $("#xos-error-dialog").html(templateFromId("#xos-error-response")(parsed_error));
154 } else if (parsed_error && ("detail" in parsed_error)) {
155 // this error response comes from rest_framework APIException
156 parsed_error["error"] = "API Error";
157 parsed_error["specific_error"] = parsed_error["detail"];
158 parsed_error["reasons"] = [];
159 $("#xos-error-dialog").html(templateFromId("#xos-error-response")(parsed_error));
161 $("#xos-error-dialog").html(templateFromId("#xos-error-rawresponse")({responseText: strip_scripts(responseText)}))
164 $("#xos-error-dialog").dialog({
168 Ok: function() { $(this).dialog("close"); }
173 hideLinkedItems: function(result) {
176 this["linkedObjs" + (index+1)].empty();
181 hideTabs: function() { $("#tabs").hide(); },
182 showTabs: function() { $("#tabs").show(); },
184 createListHandler: function(listViewName, collection_name, regionName, title) {
187 listView = new app[listViewName];
188 app[regionName].show(listView);
189 app.hideLinkedItems();
190 $("#contentTitle").html(templateFromId("#xos-title-list")({"title": title}));
194 listButtons = new XOSListButtonView({linkedView: listView});
195 app["rightButtonPanel"].show(listButtons);
199 createAddHandler: function(detailName, collection_name, regionName, title) {
202 console.log("addHandler");
204 app.hideLinkedItems();
207 model = new xos[collection_name].model();
208 detailViewClass = app[detailName];
209 detailView = new detailViewClass({model: model, collection:xos[collection_name]});
210 app[regionName].show(detailView);
212 detailButtons = new XOSDetailButtonView({linkedView: detailView});
213 app["rightButtonPanel"].show(detailButtons);
217 createAddChildHandler: function(addChildName, collection_name) {
219 return function(parent_modelName, parent_fieldName, parent_id) {
220 app.Router.showPreviousURL();
221 model = new xos[collection_name].model();
222 model.attributes[parent_fieldName] = parent_id;
223 model.readOnlyFields.push(parent_fieldName);
224 detailViewClass = app[addChildName];
225 var detailView = new detailViewClass({model: model, collection:xos[collection_name]});
226 detailView.dialog = $("xos-addchild-dialog");
227 app["addChildDetail"].show(detailView);
228 $("#xos-addchild-dialog").dialog({
233 "Save" : function() {
234 var addDialog = this;
235 detailView.synchronous = true;
236 detailView.afterSave = function() { console.log("addChild afterSave"); $(addDialog).dialog("close"); }
239 //$(this).dialog("close");
241 "Cancel" : function() {
242 $(this).dialog("close");
246 $("#xos-addchild-dialog").dialog("open");
250 createDeleteHandler: function(collection_name) {
252 return function(model_id) {
253 console.log("deleteCalled");
254 collection = xos[collection_name];
255 model = collection.get(model_id);
256 assert(model!=undefined, "failed to get model " + model_id + " from collection " + collection_name);
257 app.Router.showPreviousURL();
258 app.deleteDialog(model);
262 createDetailHandler: function(detailName, collection_name, regionName, title) {
264 showModelId = function(model_id) {
265 $("#contentTitle").html(templateFromId("#xos-title-detail")({"title": title}));
267 collection = xos[collection_name];
268 model = collection.get(model_id);
269 if (model == undefined) {
270 app[regionName].show(new HTMLView({html: "failed to load object " + model_id + " from collection " + collection_name}));
272 detailViewClass = app[detailName];
273 detailView = new detailViewClass({model: model});
274 app[regionName].show(detailView);
275 detailView.showLinkedItems();
277 detailButtons = new XOSDetailButtonView({linkedView: detailView});
278 app["rightButtonPanel"].show(detailButtons);
284 /* error handling callbacks */
286 hideError: function() {
287 if (this.logWindowId) {
289 $(this.errorBoxId).hide();
290 $(this.successBoxId).hide();
294 showSuccess: function(result) {
295 result["statusclass"] = "success";
296 if (this.logTableId) {
297 this.appendLogWindow(result);
299 $(this.successBoxId).show();
300 $(this.successBoxId).html(_.template($(this.successTemplate).html())(result));
302 $(this.successCloseButtonId).unbind().bind('click', function() {
303 $(that.successBoxId).hide();
308 showError: function(result) {
309 result["statusclass"] = "failure";
310 if (this.logTableId) {
311 this.appendLogWindow(result);
312 this.popupErrorDialog(result.responseText);
314 // this is really old stuff
315 $(this.errorBoxId).show();
316 $(this.errorBoxId).html(_.template($(this.errorTemplate).html())(result));
318 $(this.errorCloseButtonId).unbind().bind('click', function() {
319 $(that.errorBoxId).hide();
324 showInformational: function(result) {
325 result["statusclass"] = "inprog";
326 if (this.logTableId) {
327 return this.appendLogWindow(result);
333 appendLogWindow: function(result) {
334 // compute a new logMessageId for this log message
335 logMessageId = "logMessage" + this.logMessageCount;
336 this.logMessageCount = this.logMessageCount + 1;
337 result["logMessageId"] = logMessageId;
339 logMessageTemplate=$("#xos-log-template").html();
340 assert(logMessageTemplate != undefined, "logMessageTemplate is undefined");
341 newRow = _.template(logMessageTemplate, result);
342 assert(newRow != undefined, "newRow is undefined");
344 if (result["infoMsgId"] != undefined) {
345 // We were passed the logMessageId of an informational message,
346 // and the caller wants us to replace that message with our own.
347 // i.e. replace an informational message with a success or an error.
348 $("#"+result["infoMsgId"]).replaceWith(newRow);
350 // Create a brand new log message rather than replacing one.
351 logTableBody = $(this.logTableId + " tbody");
352 logTableBody.prepend(newRow);
355 if (this.statusMsgId) {
356 $(this.statusMsgId).html( templateFromId("#xos-status-template")(result) );
359 limitTableRows(this.logTableId, 5);
364 saveError: function(model, result, xhr, infoMsgId) {
365 console.log("saveError");
366 result["what"] = "save " + model.modelName + " " + model.attributes.humanReadableName;
367 result["infoMsgId"] = infoMsgId;
368 this.showError(result);
371 saveSuccess: function(model, result, xhr, infoMsgId, addToCollection) {
372 console.log("saveSuccess");
373 if (model.addToCollection) {
374 console.log("addToCollection");
375 console.log(model.addToCollection);
376 model.addToCollection.add(model);
377 model.addToCollection.sort();
378 model.addToCollection = undefined;
380 result = {status: xhr.xhr.status, statusText: xhr.xhr.statusText};
381 result["what"] = "save " + model.modelName + " " + model.attributes.humanReadableName;
382 result["infoMsgId"] = infoMsgId;
383 this.showSuccess(result);
386 destroyError: function(model, result, xhr, infoMsgId) {
387 result["what"] = "destroy " + model.modelName + " " + model.attributes.humanReadableName;
388 result["infoMsgId"] = infoMsgId;
389 this.showError(result);
392 destroySuccess: function(model, result, xhr, infoMsgId) {
393 result = {status: xhr.xhr.status, statusText: xhr.xhr.statusText};
394 result["what"] = "destroy " + model.modelName + " " + model.attributes.humanReadableName;
395 result["infoMsgId"] = infoMsgId;
396 this.showSuccess(result);
399 /* end error handling callbacks */
401 destroyModel: function(model) {
402 //console.log("destroyModel"); console.log(model);
404 var infoMsgId = this.showInformational( {what: "destroy " + model.modelName + " " + model.attributes.humanReadableName, status: "", statusText: "in progress..."} );
406 model.destroy({error: function(model, result, xhr) { that.destroyError(model,result,xhr,infoMsgId);},
407 success: function(model, result, xhr) { that.destroySuccess(model,result,xhr,infoMsgId);}});
410 deleteDialog: function(model, afterDelete) {
412 assert(model!=undefined, "deleteDialog's model is undefined");
413 //console.log("deleteDialog"); console.log(model);
414 this.confirmDialog(null, null, function() {
415 //console.log("deleteConfirm"); console.log(model);
416 modelName = model.modelName;
417 that.destroyModel(model);
418 if (afterDelete=="list") {
419 that.navigate("list", modelName);
420 } else if (afterDelete) {
427 XOSButtonView = Marionette.ItemView.extend({
428 events: {"click button.btn-xos-save-continue": "submitContinueClicked",
429 "click button.btn-xos-save-leave": "submitLeaveClicked",
430 "click button.btn-xos-save-another": "submitAddAnotherClicked",
431 "click button.btn-xos-delete": "deleteClicked",
432 "click button.btn-xos-add": "addClicked",
433 "click button.btn-xos-refresh": "refreshClicked",
436 submitLeaveClicked: function(e) {
437 this.options.linkedView.submitLeaveClicked.call(this.options.linkedView, e);
440 submitContinueClicked: function(e) {
441 this.options.linkedView.submitContinueClicked.call(this.options.linkedView, e);
444 submitAddAnotherClicked: function(e) {
445 this.options.linkedView.submitAddAnotherClicked.call(this.options.linkedView, e);
448 submitDeleteClicked: function(e) {
449 this.options.linkedView.deleteClicked.call(this.options.linkedView, e);
452 addClicked: function(e) {
453 this.options.linkedView.addClicked.call(this.options.linkedView, e);
456 refreshClicked: function(e) {
457 this.options.linkedView.refreshClicked.call(this.options.linkedView, e);
461 XOSDetailButtonView = XOSButtonView.extend({ template: "#xos-savebuttons-template" });
462 XOSListButtonView = XOSButtonView.extend({ template: "#xos-listbuttons-template" });
466 app - MarionetteApplication
467 template - template (See XOSHelper.html)
470 XOSDetailView = Marionette.ItemView.extend({
473 viewInitializers: [],
475 events: {"click button.btn-xos-save-continue": "submitContinueClicked",
476 "click button.btn-xos-save-leave": "submitLeaveClicked",
477 "click button.btn-xos-save-another": "submitAddAnotherClicked",
478 "click button.btn-xos-delete": "deleteClicked",
479 "change input": "inputChanged"},
481 /* inputChanged is watching the onChange events of the input controls. We
482 do this to track when this view is 'dirty', so we can throw up a warning
483 if the user tries to change his slices without saving first.
486 initialize: function() {
487 this.on("saveSuccess", this.onSaveSuccess);
488 this.synchronous = false;
492 _.each(this.viewInitializers, function(initializer) {
497 saveSuccess: function(e) {
498 // always called after a save succeeds
501 afterSave: function(e) {
502 // if this.synchronous, then called after the save succeeds
503 // if !this.synchronous, then called after save is initiated
506 onSaveSuccess: function(e) {
508 if (this.synchronous) {
513 inputChanged: function(e) {
517 submitContinueClicked: function(e) {
518 console.log("saveContinue");
520 this.afterSave = function() { };
524 submitLeaveClicked: function(e) {
525 console.log("saveLeave");
527 if (this.options.noSubmitButton || this.noSubmitButton) {
531 this.afterSave = function() {
532 that.app.navigate("list", that.model.modelName);
537 submitAddAnotherClicked: function(e) {
538 console.log("saveAnother");
542 this.afterSave = function() {
543 console.log("addAnother afterSave");
544 that.app.navigate("add", that.model.modelName);
550 this.app.hideError();
551 var data = Backbone_Syphon.serialize(this);
553 var isNew = !this.model.id;
557 this.$el.find(".help-inline").remove();
559 /* although model.validate() is called automatically by
560 model.save, we call it ourselves, so we can throw up our
561 validation error before creating the infoMsg in the log
563 errors = this.model.xosValidate(data);
565 this.onFormDataInvalid(errors);
570 this.model.attributes.humanReadableName = "new " + this.model.modelName;
571 this.model.addToCollection = this.collection;
573 this.model.addToCollection = undefined;
576 var infoMsgId = this.app.showInformational( {what: "save " + this.model.modelName + " " + this.model.attributes.humanReadableName, status: "", statusText: "in progress..."} );
578 this.model.save(data, {error: function(model, result, xhr) { that.app.saveError(model,result,xhr,infoMsgId);},
579 success: function(model, result, xhr) { that.app.saveSuccess(model,result,xhr,infoMsgId);
580 that.trigger("saveSuccess");
584 if (!this.synchronous) {
589 deleteClicked: function(e) {
591 this.app.deleteDialog(this.model, "list");
594 tabClick: function(tabId, regionName) {
595 region = this.app[regionName];
596 if (this.currentTabRegion != undefined) {
597 this.currentTabRegion.$el.hide();
599 if (this.currentTabId != undefined) {
600 $(this.currentTabId).removeClass('active');
602 this.currentTabRegion = region;
603 this.currentTabRegion.$el.show();
605 this.currentTabId = tabId;
606 $(tabId).addClass('active');
609 showTabs: function(tabs) {
610 template = templateFromId("#xos-tabs-template", {tabs: tabs});
611 $("#tabs").html(template(tabs));
614 _.each(tabs, function(tab) {
615 var regionName = tab["region"];
616 var tabId = '#xos-nav-'+regionName;
617 $(tabId).bind('click', function() { that.tabClick(tabId, regionName); });
623 showLinkedItems: function() {
626 tabs.push({name: "details", region: "detail"});
628 makeFilter = function(relatedField, relatedId) {
629 return function(model) { return model.attributes[relatedField] == relatedId; }
633 for (relatedName in this.model.collection.relatedCollections) {
634 var relatedField = this.model.collection.relatedCollections[relatedName];
635 var relatedId = this.model.id;
636 regionName = "linkedObjs" + (index+1);
638 relatedListViewClassName = relatedName + "ListView";
639 assert(this.app[relatedListViewClassName] != undefined, relatedListViewClassName + " not found");
640 relatedListViewClass = this.app[relatedListViewClassName].extend({collection: xos[relatedName],
641 filter: makeFilter(relatedField, relatedId),
642 parentModel: this.model});
643 this.app[regionName].show(new relatedListViewClass());
644 if (this.app.hideTabsByDefault) {
645 this.app[regionName].$el.hide();
647 tabs.push({name: relatedName, region: regionName});
652 this.app["linkedObjs" + (index+1)].empty();
657 this.tabClick('#xos-nav-detail', 'detail');
660 onFormDataInvalid: function(errors) {
662 var markErrors = function(value, key) {
663 var $inputElement = self.$el.find("[name='" + key + "']");
664 var $inputContainer = $inputElement.parent();
665 //$inputContainer.find(".help-inline").remove();
666 var $errorEl = $("<span>", {class: "help-inline error", text: value});
667 $inputContainer.append($errorEl).addClass("error");
669 _.each(errors, markErrors);
672 templateHelpers: function() { return { modelName: this.model.modelName,
673 collectionName: this.model.collectionName,
674 addFields: this.model.addFields,
675 listFields: this.model.listFields,
676 detailFields: this.options.detailFields || this.detailFields || this.model.detailFields,
677 fieldDisplayNames: this.options.fieldDisplayNames || this.fieldDisplayNames || this.model.fieldDisplayNames || {},
678 foreignFields: this.model.foreignFields,
679 detailLinkFields: this.model.detailLinkFields,
680 inputType: this.model.inputType,
683 choices: this.options.choices || this.choices || this.model.choices || {},
684 helpText: this.options.helpText || this.helpText || this.model.helpText || {},
688 XOSDetailView_sliver = XOSDetailView.extend( {
689 events: $.extend(XOSDetailView.events,
690 {"change #field_deployment": "onDeploymentChange"}
694 // Note that this causes the selects to be updated a second time. The
695 // first time was when the template was originally invoked, and the
696 // selects will all have the full unfiltered set of candidates. Then
697 // onShow will fire, and we'll update them with the filtered values.
698 this.onDeploymentChange();
701 onDeploymentChange: function(e) {
702 var deploymentID = this.$el.find("#field_deployment").val();
704 //console.log("onDeploymentChange");
706 filterFunc = function(model) { for (index in xos.siteDeployments.models) {
707 site_deployment = xos.siteDeployments.models[index];
708 if (site_deployment.attributes.id == model.attributes.site_deployment) {
709 return (site_deployment.attributes.deployment == deploymentID);
713 // return (model.attributes.deployment==deploymentID); }
715 newSelect = idToSelect("node",
716 this.model.attributes.node,
717 this.model.foreignFields["node"],
721 this.$el.find("#field_node").html(newSelect);
723 filterFunc = function(model) { for (index in model.attributes.deployments) {
724 if (model.attributes.deployments[index] == deploymentID) return true;
728 newSelect = idToSelect("flavor",
729 this.model.attributes.flavor,
730 this.model.foreignFields["flavor"],
734 this.$el.find("#field_flavor").html(newSelect);
736 filterFunc = function(model) { for (index in model.attributes.deployments) {
737 if (model.attributes.deployments[index] == deploymentID) return true;
741 newSelect = idToSelect("image",
742 this.model.attributes.image,
743 this.model.foreignFields["image"],
747 this.$el.find("#field_image").html(newSelect);
752 This is for items that will be displayed as table rows.
754 app - MarionetteApplication
755 template - template (See XOSHelper.html)
758 XOSItemView = Marionette.ItemView.extend({
760 className: 'test-tablerow',
762 templateHelpers: function() { return { modelName: this.model.modelName,
763 collectionName: this.model.collectionName,
764 listFields: this.model.listFields,
765 addFields: this.model.addFields,
766 detailFields: this.model.detailFields,
767 foreignFields: this.model.foreignFields,
768 detailLinkFields: this.model.detailLinkFields,
769 inputType: this.model.inputType,
776 app - MarionetteApplication
777 childView - class of ItemView, probably an XOSItemView
778 template - template (see xosHelper.html)
779 collection - collection that holds these objects
780 title - title to display in template
783 XOSListView = FilteredCompositeView.extend({
784 childViewContainer: 'tbody',
787 events: {"click button.btn-xos-add": "addClicked",
788 "click button.btn-xos-refresh": "refreshClicked",
791 _fetchStateChange: function() {
792 if (this.collection.fetching) {
793 $("#xos-list-title-spinner").show();
795 $("#xos-list-title-spinner").hide();
799 addClicked: function(e) {
801 this.app.Router.navigate("add" + firstCharUpper(this.collection.modelName), {trigger: true});
804 refreshClicked: function(e) {
806 this.collection.refresh(refreshRelated=true);
809 initialize: function() {
810 this.listenTo(this.collection, 'change', this._renderChildren)
811 this.listenTo(this.collection, 'sort', function() { console.log("sort"); })
812 this.listenTo(this.collection, 'add', function() { console.log("add"); })
813 this.listenTo(this.collection, 'fetchStateChange', this._fetchStateChange);
815 // Because many of the templates use idToName(), we need to
816 // listen to the collections that hold the names for the ids
817 // that we want to display.
818 for (i in this.collection.foreignCollections) {
819 foreignName = this.collection.foreignCollections[i];
820 if (xos[foreignName] == undefined) {
821 console.log("Failed to find xos class " + foreignName);
823 this.listenTo(xos[foreignName], 'change', this._renderChildren);
824 this.listenTo(xos[foreignName], 'sort', this._renderChildren);
828 getAddChildHash: function() {
829 if (this.parentModel) {
830 parentFieldName = this.parentModel.relatedCollections[this.collection.collectionName];
831 parentFieldName = parentFieldName || "unknown";
833 /*parentFieldName = "unknown";
835 for (fieldName in this.collection.foreignFields) {
836 cname = this.collection.foreignFields[fieldName];
837 if (cname = this.collection.collectionName) {
838 parentFieldName = fieldName;
841 return "#addChild" + firstCharUpper(this.collection.modelName) + "/" + this.parentModel.modelName + "/" + parentFieldName + "/" + this.parentModel.id; // modelName, fieldName, id
847 templateHelpers: function() {
848 return { title: this.title,
849 addChildHash: this.getAddChildHash(),
850 foreignFields: this.collection.foreignFields,
851 listFields: this.collection.listFields,
852 detailLinkFields: this.collection.detailLinkFields, };
856 XOSDataTableView = Marionette.View.extend( {
857 el: '<div style="overflow: hidden">' +
858 '<h3 class="xos-list-title title_placeholder"></h3>' +
859 '<div class="header_placeholder"></div>' +
861 '<div class="footer_placeholder"></div>' +
866 events: {"click button.btn-xos-add": "addClicked",
867 "click button.btn-xos-refresh": "refreshClicked",
870 _fetchStateChange: function() {
871 if (this.collection.fetching) {
872 $("#xos-list-title-spinner").show();
874 $("#xos-list-title-spinner").hide();
878 addClicked: function(e) {
880 this.app.Router.navigate("add" + firstCharUpper(this.collection.modelName), {trigger: true});
883 refreshClicked: function(e) {
885 this.collection.refresh(refreshRelated=true);
889 initialize: function() {
890 $(this.el).find(".footer_placeholder").html( xosListFooterTemplate({addChildHash: this.getAddChildHash()}) );
891 $(this.el).find(".header_placeholder").html( xosListHeaderTemplate() );
893 this.listenTo(this.collection, 'fetchStateChange', this._fetchStateChange);
898 var fieldDisplayNames = view.options.fieldDisplayNames || view.fieldDisplayNames || {};
900 view.columnsByIndex = [];
901 view.columnsByFieldName = {};
902 _.each(this.collection.listFields, function(fieldName) {
903 inputType = view.options.inputType || view.inputType || {};
905 mSearchText = undefined;
906 sTitle = fieldName in fieldDisplayNames ? fieldDisplayNames[fieldName] : fieldNameToHumanReadable(fieldName);
908 if (fieldName=="backend_status") {
909 mRender = function(x,y,z) { return xosBackendStatusIconTemplate(z); };
912 } else if (fieldName in view.collection.foreignFields) {
913 var foreignCollection = view.collection.foreignFields[fieldName];
914 mSearchText = function(x) { return idToName(x, foreignCollection, "humanReadableName"); };
915 } else if (inputType[fieldName] == "spinner") {
916 mRender = function(x,y,z) { return xosDataTableSpinnerTemplate( {value: x, collectionName: view.collection.collectionName, fieldName: fieldName, id: z.id, app: view.app} ); };
918 if ($.inArray(fieldName, view.collection.detailLinkFields)>=0) {
919 var collectionName = view.collection.collectionName;
920 mRender = function(x,y,z) { return '<a href="#' + collectionName + '/' + z.id + '">' + x + '</a>'; };
922 thisColumn = {sTitle: sTitle, bSortable: bSortable, mData: fieldName, mRender: mRender, mSearchText: mSearchText};
923 view.columnsByIndex.push( thisColumn );
924 view.columnsByFieldName[fieldName] = thisColumn;
927 if (!view.noDeleteColumn) {
928 deleteColumn = {sTitle: "", bSortable: false, mRender: function(x,y,z) { return xosDeleteButtonTemplate({modelName: view.collection.modelName, id: z.id}); }, mData: function() { return "delete"; }};
929 view.columnsByIndex.push(deleteColumn);
930 view.columnsByFieldName["delete"] = deleteColumn;
933 oTable = $(this.el).find("table").dataTable( {
937 "bFilter": ! (view.options.disableFilter || view.disableFilter),
938 "bPaginate": ! (view.options.disablePaginate || view.disablePaginate),
939 "aoColumns": view.columnsByIndex,
941 fnServerData: function(sSource, aoData, fnCallback, settings) {
942 var compareColumns = function(sortCols, sortDirs, a, b) {
945 result = (a==b) ? 0 : ((a<b) ? -1 : 1);
946 if (sortDirs[0] == "desc") {
952 var searchMatch = function(row, sSearch) {
953 for (fieldName in row) {
954 if (fieldName in view.columnsByFieldName) {
956 value = row[fieldName].toString();
960 if (value.indexOf(sSearch) >= 0) {
968 //console.log(aoData);
970 // function used to populate the DataTable with the current
\r
971 // content of the collection
\r
972 var populateTable = function()
\r
974 //console.log("populatetable!");
\r
976 // clear out old row views
\r
981 iDisplayLength = 1000;
\r
984 _.each(aoData, function(param) {
\r
985 if (param.name == "sSortDir_0") {
\r
986 sortDirs = [param.value];
\r
987 } else if (param.name == "iSortCol_0") {
\r
988 sortCols = [view.columnsByIndex[param.value].mData];
\r
989 } else if (param.name == "iDisplayStart") {
\r
990 iDisplayStart = param.value;
\r
991 } else if (param.name == "iDisplayLength") {
\r
992 iDisplayLength = param.value;
\r
993 } else if (param.name == "sSearch") {
\r
994 sSearch = param.value;
\r
998 aaData = view.collection.toJSON();
\r
1000 // apply backbone filtering on the models
\r
1001 if (view.filter) {
\r
1002 aaData = aaData.filter( function(row) { model = {}; model.attributes = row; return view.filter(model); } );
\r
1005 var totalSize = aaData.length;
\r
1007 // turn the ForeignKey fields into human readable things
\r
1008 for (rowIndex in aaData) {
\r
1009 row = aaData[rowIndex];
\r
1010 for (fieldName in row) {
\r
1011 if (fieldName in view.columnsByFieldName) {
\r
1012 mSearchText = view.columnsByFieldName[fieldName].mSearchText;
\r
1013 if (mSearchText) {
\r
1014 row[fieldName] = mSearchText(row[fieldName]);
\r
1020 // apply datatables search
\r
1022 aaData = aaData.filter( function(row) { return searchMatch(row, sSearch); });
\r
1025 var filteredSize = aaData.length;
\r
1027 // apply datatables sort
\r
1028 aaData.sort(function(a,b) { return compareColumns(sortCols, sortDirs, a, b); });
\r
1030 // slice it for pagination
\r
1031 if (iDisplayLength >= 0) {
\r
1032 aaData = aaData.slice(iDisplayStart, iDisplayStart+iDisplayLength);
\r
1035 return fnCallback({iTotalRecords: totalSize,
\r
1036 iTotalDisplayRecords: filteredSize,
\r
1040 aoData.shift(); // ignore sEcho
1043 view.listenTo(view.collection, 'change', populateTable);
1044 view.listenTo(view.collection, 'add', populateTable);
1045 view.listenTo(view.collection, 'remove', populateTable);
1052 getAddChildHash: function() {
1053 if (this.parentModel) {
1054 parentFieldName = this.parentModel.relatedCollections[this.collection.collectionName];
1055 parentFieldName = parentFieldName || "unknown";
1057 /*parentFieldName = "unknown";
1059 for (fieldName in this.collection.foreignFields) {
1060 cname = this.collection.foreignFields[fieldName];
1061 if (cname = this.collection.collectionName) {
1062 parentFieldName = fieldName;
1065 return "#addChild" + firstCharUpper(this.collection.modelName) + "/" + this.parentModel.modelName + "/" + parentFieldName + "/" + this.parentModel.id; // modelName, fieldName, id
1073 idToName = function(id, collectionName, fieldName) {
1074 return xos.idToName(id, collectionName, fieldName);
1077 makeIdToName = function(collectionName, fieldName) {
1078 return function(id) { return idToName(id, collectionName, fieldName); }
1081 /* Constructs lists of <option> html blocks for items in a collection.
1083 selectedId = the id of an object that should be selected, if any
1084 collectionName = name of collection
1085 fieldName = name of field within models of collection that will be displayed
1088 idToOptions = function(selectedId, collectionName, fieldName, filterFunc) {
1090 for (index in xos[collectionName].models) {
1091 linkedObject = xos[collectionName].models[index];
1092 linkedId = linkedObject["id"];
1093 linkedName = linkedObject.attributes[fieldName];
1094 if (linkedId == selectedId) {
1095 selected = " selected";
1099 if ((filterFunc) && (!filterFunc(linkedObject))) {
1102 result = result + '<option value="' + linkedId + '"' + selected + '>' + linkedName + '</option>';
1107 /* Constructs an html <select> and the <option>s to go with it.
1109 variable = variable name to return to form
1110 selectedId = the id of an object that should be selected, if any
1111 collectionName = name of collection
1112 fieldName = name of field within models of collection that will be displayed
1115 idToSelect = function(variable, selectedId, collectionName, fieldName, readOnly, filterFunc) {
1117 readOnly = " readonly";
1121 result = '<select name="' + variable + '" id="field_' + variable + '"' + readOnly + '>' +
1122 idToOptions(selectedId, collectionName, fieldName, filterFunc) +
1127 choicesToOptions = function(selectedValue, choices) {
1129 for (index in choices) {
1130 choice = choices[index];
1131 displayName = choice[0];
1133 if (value == selectedValue) {
1134 selected = " selected";
1138 result = result + '<option value="' + value + '"' + selected + '>' + displayName + '</option>';
1143 choicesToSelect = function(variable, selectedValue, choices) {
1144 result = '<select name="' + variable + '" id="field_' + variable + '">' +
1145 choicesToOptions(selectedValue, choices) +