<commit_before>(function (ReportFilters, Errors, Calendar) {
    var App = {
        cfg: {
            sdate:         '#sdate',
            edate:         '#edate',
            legend:        '#legend',
            welcome:       '#welcome',
            loading:       '#loading',
            errorTarget:   '#error-container',
            errorTemplate: '#error',
            chart:         '#chart',
            filter:        '#initiatives',
            filterOptions: {
                url:                '../../lib/php/reportFilters.php',
                triggerForm:        '#initiatives',
                filterForm:         '#secondary-filters',
                locationsTemplate:  '#locations-template',
                activitiesTemplate: '#activities-template',
                locationsSelect:    '#locations',
                activitiesSelect:   '#activities'
            }
        },
        filters: null,
        init: function () {
            // Insert default dates
            this.insertDefaultDates();

            // Set initiative filter to default (for back button)
            $(this.cfg.filter).val('default');

            // Insert filter select boxes
            this.insertFilters();

            // Bind Events
            this.bindEvents();
        },
        /**
         * Inserts default dates into form
         */
        insertDefaultDates : function () {
            // Create dates for default date display
            var now = moment().format('YYYY-MM-DD'),
                then = moment().subtract('months', 6).format('YYYY-MM-DD');

            // Insert default dates into DOM
            $(this.cfg.sdate).val(then);
            $(this.cfg.edate).val(now);
        },
        /**
         * Initializes and inserts secondary filters
         */
        insertFilters: function () {
            var filters,
                self = this;

            if (this.filters === null) {
                this.filters = new ReportFilters(this.cfg.filterOptions);
            }

            filters = this.filters.init();

            filters.fail(function (e) {
                self.error(e);
            });
        },
        bindEvents: function () {
            var self = this;

            // Initialize datepicker
            $(self.cfg.sdate).datepicker({'format': 'yyyy-mm-dd', 'autoclose': 'true'});
            $(self.cfg.edate).datepicker({'format': 'yyyy-mm-dd', 'autoclose': 'true'});

            // Get chart data on submit
            $('body').on('submit', 'form', function (e) {
                var input = $(this).serializeArray();

                $.when(self.getData(input))
                    .then(self.processData.bind(self))
                    .then(self.drawChart.bind(self), self.error.bind(self));

                e.preventDefault();
            });

            // Initialize help popovers
            $('.suma-popover').popover({placement: 'bottom'});
        },
        getData: function (input) {
            var self = this;

            return $.ajax({
                url: 'results.php',
                data: input,
                beforeSend: function () {
                    $(self.cfg.loading).show();
                    $(self.cfg.legend).hide();
                    $(self.cfg.welcome).hide();
                    $(self.cfg.errorTarget).empty();
                    $('svg').remove();
                },
                success: function () {
                    $(self.cfg.legend).show();
                },
                complete: function () {
                    $(self.cfg.loading).hide();
                },
                timeout: 180000 // 3 mins
            });
        },
        error: function (e) {
            $(this.cfg.legend).hide();
            $(this.cfg.welcome).hide();

            // Log errors for debugging
            console.log('error statusText', e.statusText);
            console.log('error object', e);

            this.buildTemplate([{msg: Errors.getMsg(e.statusText)}], this.cfg.errorTemplate, this.cfg.errorTarget);
        },
        sortData: function (response) {
            return _.sortBy(
                _.map(response, function (count, date) {
                    return {
                        date: date,
                        count: count.count
                    };
                }),
                function (obj) {
                    return obj.date;
                }
            );
        },
        processData: function (response) {
            var dfd = $.Deferred();

            // Does response have enough values to draw meaningful graph?
            if (Object.keys(response.periodSum).length < 1) {
                dfd.reject({statusText: 'no data'});
            }

            dfd.resolve(this.sortData(response.periodSum));

            return dfd.promise();
        },
        drawChart: function (counts) {
            var chart,
                self = this;

            chart = Calendar();

            d3.select(self.cfg.chart)
                .datum(counts)
                .call(chart);
        },
        buildTemplate: function (items, templateId, targetId) {
            var html,
                json,
                template;

            // Insert list into object for template iteration
            json = {items: items};

            // Retrieve template from index.php (in script tag)
            html = $(templateId).html();

            // Compile template
            template = Handlebars.compile(html);

            // Populate template with data and insert into DOM
            $(targetId).prepend(template(json));
        }
    };

    // Start app on document ready
    $(document).ready(function () {
        App.init();
    });
}(ReportFilters, Errors, Calendar));
<commit_msg>extend error template<commit_after>(function (ReportFilters, Errors, Calendar) {
    var App = {
        cfg: {
            sdate:         '#sdate',
            edate:         '#edate',
            legend:        '#legend',
            welcome:       '#welcome',
            loading:       '#loading',
            errorTarget:   '#error-container',
            errorTemplate: '#error',
            chart:         '#chart',
            filter:        '#initiatives',
            filterOptions: {
                url:                '../../lib/php/reportFilters.php',
                triggerForm:        '#initiatives',
                filterForm:         '#secondary-filters',
                locationsTemplate:  '#locations-template',
                activitiesTemplate: '#activities-template',
                locationsSelect:    '#locations',
                activitiesSelect:   '#activities'
            }
        },
        filters: null,
        init: function () {
            // Insert default dates
            this.insertDefaultDates();

            // Set initiative filter to default (for back button)
            $(this.cfg.filter).val('default');

            // Insert filter select boxes
            this.insertFilters();

            // Bind Events
            this.bindEvents();
        },
        /**
         * Inserts default dates into form
         */
        insertDefaultDates : function () {
            // Create dates for default date display
            var now = moment().format('YYYY-MM-DD'),
                then = moment().subtract('months', 6).format('YYYY-MM-DD');

            // Insert default dates into DOM
            $(this.cfg.sdate).val(then);
            $(this.cfg.edate).val(now);
        },
        /**
         * Initializes and inserts secondary filters
         */
        insertFilters: function () {
            var filters,
                self = this;

            if (this.filters === null) {
                this.filters = new ReportFilters(this.cfg.filterOptions);
            }

            filters = this.filters.init();

            filters.fail(function (e) {
                self.error(e);
            });
        },
        bindEvents: function () {
            var self = this;

            // Initialize datepicker
            $(self.cfg.sdate).datepicker({'format': 'yyyy-mm-dd', 'autoclose': 'true'});
            $(self.cfg.edate).datepicker({'format': 'yyyy-mm-dd', 'autoclose': 'true'});

            // Get chart data on submit
            $('body').on('submit', 'form', function (e) {
                var input = $(this).serializeArray();

                $.when(self.getData(input))
                    .then(self.processData.bind(self))
                    .then(self.drawChart.bind(self), self.error.bind(self));

                e.preventDefault();
            });

            // Initialize help popovers
            $('.suma-popover').popover({placement: 'bottom'});
        },
        getData: function (input) {
            var self = this;

            return $.ajax({
                url: 'results.php',
                data: input,
                beforeSend: function () {
                    $(self.cfg.loading).show();
                    $(self.cfg.legend).hide();
                    $(self.cfg.welcome).hide();
                    $(self.cfg.errorTarget).empty();
                    $('svg').remove();
                },
                success: function () {
                    $(self.cfg.legend).show();
                },
                complete: function () {
                    $(self.cfg.loading).hide();
                },
                timeout: 180000 // 3 mins
            });
        },
        error: function (e) {
            $(this.cfg.legend).hide();
            $(this.cfg.welcome).hide();

            // Log errors for debugging
            console.log('error object', e);

            this.buildTemplate([{msg: Errors.getMsg(e.statusText)}], this.cfg.errorTemplate, this.cfg.errorTarget);
        },
        sortData: function (response) {
            return _.sortBy(
                _.map(response, function (count, date) {
                    return {
                        date: date,
                        count: count.count
                    };
                }),
                function (obj) {
                    return obj.date;
                }
            );
        },
        processData: function (response) {
            var dfd = $.Deferred();

            // Does response have enough values to draw meaningful graph?
            if (Object.keys(response.periodSum).length < 1) {
                dfd.reject({statusText: 'no data'});
            }

            dfd.resolve(this.sortData(response.periodSum));

            return dfd.promise();
        },
        drawChart: function (counts) {
            var chart,
                self = this;

            chart = Calendar();

            d3.select(self.cfg.chart)
                .datum(counts)
                .call(chart);
        },
        buildTemplate: function (items, templateId, targetId, empty) {
            var html,
                json,
                template;

            // Insert list into object for template iteration
            json = {items: items};

            // Retrieve template from index.php (in script tag)
            html = $(templateId).html();

            // Compile template
            template = Handlebars.compile(html);

            // Populate template with data and insert into DOM
            if (empty) {
                $(targetId).empty();
            }
            $(targetId).prepend(template(json));
        }
    };

    // Start app on document ready
    $(document).ready(function () {
        App.init();
    });
}(ReportFilters, Errors, Calendar));
<|endoftext|>


<commit_before>(function (Calendar) {
    var App = {
        init: function () {
            this.bindEvents();
        },
        bindEvents: function () {
            var self = this;

            $('body').on('submit', 'form', function (e) {
                var input = $(this).serializeArray();

                $.when(self.getData(input))
                    .then(self.processData.bind(self))
                    .then(self.drawChart, self.error);

                e.preventDefault();
            });
        },
        getData: function (input) {
            var self = this;

            return $.ajax({
                url: 'results.php',
                data: input,
                beforeSend: function () {
                    $('#loading').show();
                    $("#legend").hide();
                    $('svg').remove();
                },
                success: function () {
                    $("#legend").show();
                },
                complete: function () {
                    $('#loading').hide();
                }
            });
        },
        error: function (e) {
            $("#legend").hide();
            console.log("error: ", e);
        },
        // Sort data according to date
        sortData: function (a, b) {
            return new Date(a.date).getTime() - new Date(b.date).getTime();
        },
        processData: function (response) {
            var dfd = $.Deferred(),
                count,
                counts = [],
                self = this;

            // Does response have enough vlues to draw meaningful graph?
            if (Object.keys(response).length < 2) {
                dfd.reject('Not enough data.')
            }

            for (count in response) {
                if (response.hasOwnProperty(count)) {
                    counts.push({
                        date: count,
                        count: response[count]
                    });
                }
            }

            counts.sort(self.sortData);
            dfd.resolve(counts);

            return dfd.promise();
        },
        drawChart: function (counts) {
            var chart;
            chart = Calendar();

                d3.select('#chart')
                    .datum(counts)
                    .call(chart);
        }
    };

    // Start app on document ready
    $(document).ready(function () {
        App.init();
    });
}(Calendar));
<commit_msg>improve data processing and sorting<commit_after>(function (Calendar) {
    var App = {
        init: function () {
            this.bindEvents();
        },
        bindEvents: function () {
            var self = this;

            $('body').on('submit', 'form', function (e) {
                var input = $(this).serializeArray();

                $.when(self.getData(input))
                    .then(self.processData.bind(self))
                    .then(self.drawChart, self.error);

                e.preventDefault();
            });
        },
        getData: function (input) {
            var self = this;

            return $.ajax({
                url: 'results.php',
                data: input,
                beforeSend: function () {
                    $('#loading').show();
                    $("#legend").hide();
                    $('svg').remove();
                },
                success: function () {
                    $("#legend").show();
                },
                complete: function () {
                    $('#loading').hide();
                }
            });
        },
        error: function (e) {
            $("#legend").hide();
            console.log("error: ", e);
        },
        sortData: function (response) {
            return _.sortBy(
                _.map(response, function (count, date) {
                    return {
                        date: date,
                        count: count
                    };
                }),
                function (obj) {
                    return obj.date;
                }
            );
        },
        processData: function (response) {
            var dfd = $.Deferred();

            // Does response have enough vlues to draw meaningful graph?
            if (Object.keys(response).length < 2) {
                dfd.reject('Not enough data.');
            }

            dfd.resolve(this.sortData(response));

            return dfd.promise();
        },
        drawChart: function (counts) {
            var chart;
            chart = Calendar();

            d3.select('#chart')
                .datum(counts)
                .call(chart);
        }
    };

    // Start app on document ready
    $(document).ready(function () {
        App.init();
    });
}(Calendar));
<|endoftext|>


<commit_before>/*
 * The contents of this file are subject to the OpenMRS Public License
 * Version 2.0 (the "License"); you may not use this file except in
 * compliance with the License. You may obtain a copy of the License at
 * http://license.openmrs.org
 *
 * Software distributed under the License is distributed on an "AS IS"
 * basis, WITHOUT WARRANTY OF ANY KIND, either express or implied. See
 * the License for the specific language governing rights and
 * limitations under the License.
 *
 * Copyright (C) OpenHMIS.  All Rights Reserved.
 *
 */

(function() {
	'use strict';

	var app = angular.module('app.cashierBillFunctionsFactory', []);
	app.service('CashierBillFunctions', CashierBillFunctions);

	CashierBillFunctions.$inject = ['EntityFunctions', 'LineItemModel', '$filter'];

	function CashierBillFunctions(EntityFunctions, LineItemModel, $filter) {
		var service;

		service = {
			addMessageLabels: addMessageLabels,
			formatItemPrice: formatItemPrice,
			paymentWarningDialog: paymentWarningDialog,
			adjustBillWarningDialog: adjustBillWarningDialog,
			validateLineItems: validateLineItems,
			populateExistingLineItems: populateExistingLineItems,
			populatePayments: populatePayments,
			reOrderItemPrices: reOrderItemPrices,
			calculateTotalPayableAmount: calculateTotalPayableAmount,
			roundItemPrice: roundItemPrice,
			createPayment: createPayment,
			computeTotalPrice: computeTotalPrice,
		};

		return service;

		/**
		 * All message labels used in the UI are defined here
		 * @returns {{}}
		 */
		function addMessageLabels() {
			var messages = {};
			return messages;
		}

		function formatItemPrice(itemPrice) {
			var price = itemPrice.price;
			if (price !== undefined) {
				price = (Math.round(price * 100) / 100).toFixed(2);
			} else {
				price = '';
			}

			if (itemPrice.name === undefined || itemPrice.name === '' || itemPrice.name === null) {
				return price;
			} else {
				return price + ' (' + itemPrice.name + ')';
			}
		}

		function paymentWarningDialog($scope) {
			var dialog = emr.setupConfirmationDialog({
				selector: '#payment-warning-dialog',
				actions: {
					confirm: function() {
						$scope.isProcessPayment = true;
						$scope.$apply();
						if ($scope.uuid !== undefined) {
							$scope.postPayment();
						} else {
							$scope.saveOrUpdate();
						}
						dialog.close();
					},
					cancel: function() {
						dialog.close();
					}
				}
			});

			dialog.show();

			EntityFunctions.disableBackground();
		}

		function adjustBillWarningDialog($scope) {
			var dialog = emr.setupConfirmationDialog({
				selector: '#adjust-bill-warning-dialog',
				actions: {
					confirm: function() {
						$scope.isAdjustBill = true;
						$scope.$apply();
						$scope.saveOrUpdate();
						dialog.close();
					},
					cancel: function() {
						dialog.close();
					}
				}
			});

			dialog.show();

			EntityFunctions.disableBackground();
		}

		function validateLineItems(lineItems, validatedLineItems) {
			// validate line items
			var count = 0;
			var failed = false;
			for (var i = 0; i < lineItems.length; i++) {
				var lineItem = lineItems[i];
				if (lineItem.selected) {
					if (lineItem.item.name === undefined) {
						var errorMessage =
							emr.message("openhmis.cashier.bill.lineItems.error.invalidItem") + " - " + lineItem.item.toString();
						emr.errorAlert(errorMessage);
						lineItem.invalidEntry = true;
						failed = true;
						continue;
					}
					var requestLineItem = {
						item: lineItem.item.uuid,
						lineItemOrder: count++,
						price: lineItem.itemPrice.price,
						priceName: lineItem.itemPrice.name || "",
						priceUuid: lineItem.itemPrice.uuid,
						quantity: lineItem.itemQuantity
					};
					validatedLineItems.push(requestLineItem);
				} else if (lineItem.item !== "") {
					var errorMessage =
						emr.message("openhmis.cashier.bill.lineItems.error.invalidItem") + " - " + lineItem.item.toString();
					emr.errorAlert(errorMessage);
					lineItem.invalidEntry = true;
					failed = true;
				}
			}

			if (validatedLineItems.length == 0 && !failed) {
				emr.errorAlert("openhmis.cashier.bill.chooseItemErrorMessage");
			} else if (validatedLineItems.length > 0 && !failed) {
				return true;
			}

			return false;
		}

		function populateExistingLineItems(lineItems, populatedLineItems, $scope) {
			for (var i = 0; i < lineItems.length; i++) {
				var lineItem = lineItems[i];
				var itemSPrice = {
					name: lineItem.priceName,
					price: lineItem.price,
					uuid: lineItem.priceUuid
				};
				var lineItemModel = new LineItemModel(lineItem.item, lineItem.quantity, itemSPrice);
				lineItemModel.setSelected(true);
				lineItemModel.setTotal(lineItem.price * lineItem.quantity);
				populatedLineItems.push(lineItemModel);

				$scope.lineItem = lineItemModel;

				// load item details
				$scope.loadItemDetails(lineItem.item.uuid, lineItemModel);
			}

			$scope.computeTotalPrice();
		}

		function populatePayments(payments, populatedPayments) {
			if (payments !== undefined && payments.length > 0) {
				for (var i = 0; i < payments.length; i++) {
					var payment = {};
					var attributes = [];
					if (payments[i].attributes.length > 0) {
						for (var j = 0; j < payments[i].attributes.length; j++) {
							var attr = {};
							attr.attributeType = payments[i].attributes[j].attributeType.uuid;
							attr.value = payments[i].attributes[j].value;
							attributes.push(attr);
						}
					}
					payment.attributes = attributes;
					payment.amount = payments[i].amount;
					payment.amountTendered = payments[i].amountTendered;
					payment.instanceType = payments[i].instanceType.uuid;
					populatedPayments.push(payment);
				}
			}
		}

		function createPayment(paymentModeAttributes, attributes,
		                       amountTendered, amountDue, paymentModeUuid) {
			var requestPaymentAttributeTypes = [];
			if (EntityFunctions.validateAttributeTypes(
					paymentModeAttributes, attributes, requestPaymentAttributeTypes)) {
				var payment = {};
				payment.attributes = [];
				if (requestPaymentAttributeTypes.length > 0) {
					payment.attributes = requestPaymentAttributeTypes;
				}

				if (amountDue - amountTendered < 0) {
					payment.amount = Math.round(amountDue);
				} else {
					payment.amount = amountTendered;
				}
				payment.amountTendered = amountTendered;
				payment.instanceType = paymentModeUuid;
				return payment;
			}
			return null;
		}

		function reOrderItemPrices(lineItem, itemDetails) {
			var defaultPrice = itemDetails.defaultPrice;
			var index = -1;
			for (var i = 0; i < itemDetails.prices.length; i++) {
				var price = itemDetails.prices[i];
				if (price.uuid === defaultPrice.uuid) {
					index = lineItem.prices.indexOf(price);
				}
			}

			if (index !== -1) {
				lineItem.prices.splice(index, 1);
				lineItem.prices.unshift(defaultPrice);
			}
		}

		function calculateTotalPayableAmount(lineItems, roundingItem) {
			var totalPayableAmount = 0;
			for (var i = 0; i < lineItems.length; i++) {
				var lineItem = lineItems[i];
				if (lineItem.isSelected()) {
					totalPayableAmount += lineItem.getTotal();
				}
			}

			if (totalPayableAmount > 0 && roundingItem !== null && roundingItem !== undefined) {
				totalPayableAmount = roundItemPrice(
					totalPayableAmount, roundingItem.roundToNearest, roundingItem.roundingMode);
			}

			return totalPayableAmount;
		}

		function computeTotalPrice($scope) {
			$scope.totalChangeDue = 0;
			$scope.totalAmountDue = 0;

			// calculate amount for current items.
			if ($scope.STATUS === 'PENDING') {
				$scope.totalPayableAmount = calculateTotalPayableAmount(
					$scope.lineItems, $scope.roundingItem);
			} else {
				$scope.totalPayableAmount = calculateTotalPayableAmount($scope.lineItems);
				if ($scope.STATUS === 'ADJUSTED') {
					$scope.totalPayableAmount = roundItemPrice(
						$scope.totalPayableAmount, $scope.roundingItem.roundToNearest, $scope.roundingItem.roundingMode);
				}
			}

			// sum amount for previous line items.
			$scope.totalPayableAmount += calculateTotalPayableAmount($scope.previousLineItems);

			// calculate tendered amount
			$scope.totalAmountTendered = 0;
			if ($scope.currentPayments !== null) {
				for (var i = 0; i < $scope.currentPayments.length; i++) {
					$scope.totalAmountTendered += $scope.currentPayments[i].amountTendered;
				}
			}

			// sum previous payments.
			if ($scope.previousPayments !== null) {
				for (var i = 0; i < $scope.previousPayments.length; i++) {
					$scope.totalAmountTendered += $scope.previousPayments[i].amountTendered;
				}
			}

			// calculate change due
			if ($scope.totalPayableAmount > 0) {
				$scope.totalChangeDue = $scope.totalAmountTendered - $scope.totalPayableAmount;
			} else if ($scope.totalAmountTendered > 0) {
				$scope.totalChangeDue = $scope.totalAmountTendered;
			}

			// calculate amount due
			if ($scope.totalChangeDue < 0) {
				$scope.totalAmountDue = $scope.totalChangeDue * -1;
				$scope.totalChangeDue = 0;
			} else {
				$scope.totalAmountDue = 0;
			}

			// auto-fill payment amount.
			if ($scope.checkAutofillPaymentAmount && $scope.totalAmountDue !== 0) {
				$scope.amountTendered = $scope.totalAmountDue;
			}

			$scope.totalChangeDue = $filter('number')($scope.totalChangeDue, 2);
			$scope.totalAmountDue = $filter('number')($scope.totalAmountDue, 2);
			$scope.totalAmountTendered = $filter('number')($scope.totalAmountTendered, 2);
			$scope.totalPayableAmount = $filter('number')($scope.totalPayableAmount, 2);
		}

		function roundItemPrice(val, nearest, mode) {
			if (nearest === 0 || nearest === undefined || mode === undefined) {
				return val;
			}
			var factor = 1 / nearest;
			switch (mode) {
				case 'FLOOR':
					return Math.floor(val * factor) / factor;
				case 'CEILING':
					return Math.ceil(val * factor) / factor;
				default:
					return Math.round(val * factor) / factor;
			}
		}
	}
})();
<commit_msg>Fix rounding<commit_after>/*
 * The contents of this file are subject to the OpenMRS Public License
 * Version 2.0 (the "License"); you may not use this file except in
 * compliance with the License. You may obtain a copy of the License at
 * http://license.openmrs.org
 *
 * Software distributed under the License is distributed on an "AS IS"
 * basis, WITHOUT WARRANTY OF ANY KIND, either express or implied. See
 * the License for the specific language governing rights and
 * limitations under the License.
 *
 * Copyright (C) OpenHMIS.  All Rights Reserved.
 *
 */

(function() {
	'use strict';

	var app = angular.module('app.cashierBillFunctionsFactory', []);
	app.service('CashierBillFunctions', CashierBillFunctions);

	CashierBillFunctions.$inject = ['EntityFunctions', 'LineItemModel'];

	function CashierBillFunctions(EntityFunctions, LineItemModel) {
		var service;

		service = {
			addMessageLabels: addMessageLabels,
			formatItemPrice: formatItemPrice,
			paymentWarningDialog: paymentWarningDialog,
			adjustBillWarningDialog: adjustBillWarningDialog,
			validateLineItems: validateLineItems,
			populateExistingLineItems: populateExistingLineItems,
			populatePayments: populatePayments,
			reOrderItemPrices: reOrderItemPrices,
			calculateTotalPayableAmount: calculateTotalPayableAmount,
			roundItemPrice: roundItemPrice,
			createPayment: createPayment,
			computeTotalPrice: computeTotalPrice,
		};

		return service;

		/**
		 * All message labels used in the UI are defined here
		 * @returns {{}}
		 */
		function addMessageLabels() {
			var messages = {};
			return messages;
		}

		function formatItemPrice(itemPrice) {
			var price = itemPrice.price;
			if (price !== undefined) {
				price = price.toFixed(2);
			} else {
				price = '';
			}

			if (itemPrice.name === undefined || itemPrice.name === '' || itemPrice.name === null) {
				return price;
			} else {
				return price + ' (' + itemPrice.name + ')';
			}
		}

		function paymentWarningDialog($scope) {
			var dialog = emr.setupConfirmationDialog({
				selector: '#payment-warning-dialog',
				actions: {
					confirm: function() {
						$scope.isProcessPayment = true;
						$scope.$apply();
						if ($scope.uuid !== undefined) {
							$scope.postPayment();
						} else {
							$scope.saveOrUpdate();
						}
						dialog.close();
					},
					cancel: function() {
						dialog.close();
					}
				}
			});

			dialog.show();

			EntityFunctions.disableBackground();
		}

		function adjustBillWarningDialog($scope) {
			var dialog = emr.setupConfirmationDialog({
				selector: '#adjust-bill-warning-dialog',
				actions: {
					confirm: function() {
						$scope.isAdjustBill = true;
						$scope.$apply();
						$scope.saveOrUpdate();
						dialog.close();
					},
					cancel: function() {
						dialog.close();
					}
				}
			});

			dialog.show();

			EntityFunctions.disableBackground();
		}

		function validateLineItems(lineItems, validatedLineItems) {
			// validate line items
			var count = 0;
			var failed = false;
			for (var i = 0; i < lineItems.length; i++) {
				var lineItem = lineItems[i];
				if (lineItem.selected) {
					if (lineItem.item.name === undefined) {
						var errorMessage =
							emr.message("openhmis.cashier.bill.lineItems.error.invalidItem") + " - " + lineItem.item.toString();
						emr.errorAlert(errorMessage);
						lineItem.invalidEntry = true;
						failed = true;
						continue;
					}
					var requestLineItem = {
						item: lineItem.item.uuid,
						lineItemOrder: count++,
						price: lineItem.itemPrice.price,
						priceName: lineItem.itemPrice.name || "",
						priceUuid: lineItem.itemPrice.uuid,
						quantity: lineItem.itemQuantity
					};
					validatedLineItems.push(requestLineItem);
				} else if (lineItem.item !== "") {
					var errorMessage =
						emr.message("openhmis.cashier.bill.lineItems.error.invalidItem") + " - " + lineItem.item.toString();
					emr.errorAlert(errorMessage);
					lineItem.invalidEntry = true;
					failed = true;
				}
			}

			if (validatedLineItems.length == 0 && !failed) {
				emr.errorAlert("openhmis.cashier.bill.chooseItemErrorMessage");
			} else if (validatedLineItems.length > 0 && !failed) {
				return true;
			}

			return false;
		}

		function populateExistingLineItems(lineItems, populatedLineItems, $scope) {
			for (var i = 0; i < lineItems.length; i++) {
				var lineItem = lineItems[i];
				var itemPrice = {
					name: lineItem.priceName,
					price: lineItem.price,
					uuid: lineItem.priceUuid
				};
				var lineItemModel = new LineItemModel(lineItem.item, lineItem.quantity, itemPrice);
				lineItemModel.setSelected(true);
				lineItemModel.setTotal(lineItem.price * lineItem.quantity);
				populatedLineItems.push(lineItemModel);

				$scope.lineItem = lineItemModel;

				// load item details
				$scope.loadItemDetails(lineItem.item.uuid, lineItemModel);
			}

			$scope.computeTotalPrice();
		}

		function populatePayments(payments, populatedPayments) {
			if (payments !== undefined && payments.length > 0) {
				for (var i = 0; i < payments.length; i++) {
					var payment = {};
					var attributes = [];
					if (payments[i].attributes.length > 0) {
						for (var j = 0; j < payments[i].attributes.length; j++) {
							var attr = {};
							attr.attributeType = payments[i].attributes[j].attributeType.uuid;
							attr.value = payments[i].attributes[j].value;
							attributes.push(attr);
						}
					}
					payment.attributes = attributes;
					payment.amount = payments[i].amount;
					payment.amountTendered = payments[i].amountTendered;
					payment.instanceType = payments[i].instanceType.uuid;
					populatedPayments.push(payment);
				}
			}
		}

		function createPayment(paymentModeAttributes, attributes,
		                       amountTendered, amountDue, paymentModeUuid) {
			var requestPaymentAttributeTypes = [];
			if (EntityFunctions.validateAttributeTypes(
					paymentModeAttributes, attributes, requestPaymentAttributeTypes)) {
				var payment = {};
				payment.attributes = [];
				if (requestPaymentAttributeTypes.length > 0) {
					payment.attributes = requestPaymentAttributeTypes;
				}

				amountTendered = parseFloat(amountTendered);
				amountDue = parseFloat(amountDue);
				if (amountDue - amountTendered < 0) {
					payment.amount = amountDue;
				} else {
					payment.amount = amountTendered;
				}
				payment.amountTendered = amountTendered;
				payment.instanceType = paymentModeUuid;
				return payment;
			}
			return null;
		}

		function reOrderItemPrices(lineItem, itemDetails) {
			var defaultPrice = itemDetails.defaultPrice;
			var index = -1;
			for (var i = 0; i < itemDetails.prices.length; i++) {
				var price = itemDetails.prices[i];
				if (price.uuid === defaultPrice.uuid) {
					index = lineItem.prices.indexOf(price);
				}
			}

			if (index !== -1) {
				lineItem.prices.splice(index, 1);
				lineItem.prices.unshift(defaultPrice);
			}
		}

		function calculateTotalPayableAmount(lineItems, roundingItem) {
			var totalPayableAmount = 0;
			for (var i = 0; i < lineItems.length; i++) {
				var lineItem = lineItems[i];
				if (lineItem.isSelected()) {
					totalPayableAmount += lineItem.getTotal();
				}
			}

			if (totalPayableAmount > 0 && roundingItem !== null && roundingItem !== undefined) {
				totalPayableAmount = roundItemPrice(
					totalPayableAmount, roundingItem.roundToNearest, roundingItem.roundingMode);
			}

			return totalPayableAmount;
		}

		function computeTotalPrice($scope) {
			$scope.totalChangeDue = 0;
			$scope.totalAmountDue = 0;

			// calculate amount for current items.
			if ($scope.STATUS === 'PENDING') {
				$scope.totalPayableAmount = calculateTotalPayableAmount(
					$scope.lineItems, $scope.roundingItem);
			} else {
				$scope.totalPayableAmount = calculateTotalPayableAmount($scope.lineItems);
				if ($scope.STATUS === 'ADJUSTED') {
					$scope.totalPayableAmount = roundItemPrice(
						$scope.totalPayableAmount, $scope.roundingItem.roundToNearest, $scope.roundingItem.roundingMode);
				}
			}

			// sum amount for previous line items.
			$scope.totalPayableAmount += calculateTotalPayableAmount($scope.previousLineItems);

			// calculate tendered amount
			$scope.totalAmountTendered = 0;
			if ($scope.currentPayments !== null) {
				for (var i = 0; i < $scope.currentPayments.length; i++) {
					$scope.totalAmountTendered += $scope.currentPayments[i].amountTendered;
				}
			}

			// sum previous payments.
			if ($scope.previousPayments !== null) {
				for (var i = 0; i < $scope.previousPayments.length; i++) {
					$scope.totalAmountTendered += $scope.previousPayments[i].amountTendered;
				}
			}

			// calculate change due
			if ($scope.totalPayableAmount > 0) {
				$scope.totalChangeDue = $scope.totalAmountTendered - $scope.totalPayableAmount;
			} else if ($scope.totalAmountTendered > 0) {
				$scope.totalChangeDue = $scope.totalAmountTendered;
			}

			// calculate amount due
			if ($scope.totalChangeDue < 0) {
				$scope.totalAmountDue = $scope.totalChangeDue * -1;
				$scope.totalChangeDue = 0;
			} else {
				$scope.totalAmountDue = 0;
			}

			// auto-fill payment amount.
			if ($scope.checkAutofillPaymentAmount && $scope.totalAmountDue !== 0) {
				$scope.amountTendered = $scope.totalAmountDue;
			}

			$scope.totalChangeDue = $scope.totalChangeDue.toFixed(2);
			$scope.totalAmountDue = $scope.totalAmountDue.toFixed(2);
			$scope.totalAmountTendered = $scope.totalAmountTendered.toFixed(2);
			$scope.totalPayableAmount = $scope.totalPayableAmount.toFixed(2);
		}

		function roundItemPrice(val, nearest, mode) {
			if (nearest === 0 || nearest === undefined || mode === undefined) {
				return val;
			}
			var factor = 1 / nearest;
			switch (mode) {
				case 'FLOOR':
					return Math.floor(val * factor) / factor;
				case 'CEILING':
					return Math.ceil(val * factor) / factor;
				default:
					return Math.round(val * factor) / factor;
			}
		}
	}
})();
<|endoftext|>


<commit_before>// ==========================================================================
//                          DG.ContainerView
//
//  The top level view in a DG document.
//
//  Author:   William Finzer
//
//  Copyright (c) 2014 by The Concord Consortium, Inc. All rights reserved.
//
//  Licensed under the Apache License, Version 2.0 (the "License");
//  you may not use this file except in compliance with the License.
//  You may obtain a copy of the License at
//
//    http://www.apache.org/licenses/LICENSE-2.0
//
//  Unless required by applicable law or agreed to in writing, software
//  distributed under the License is distributed on an "AS IS" BASIS,
//  WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
//  See the License for the specific language governing permissions and
//  limitations under the License.
// ==========================================================================

sc_require('views/inspector/inspector_view');

/** @class

  DG.ContainerView is the superview for the component views in the document.

  @extends SC.View
*/
DG.ContainerView = SC.View.extend(
/** @scope DG.ContainerView.prototype */
  (function() {
    var kDocMargin = 16;
    return {
      classNames: 'dg-container-view'.w(),

      isResizable: YES,

      /**
       * To bring a component frontmost, we assign it this value and then increment the value.
       * We don't worry about running out of numbers.
       * @property{Number}
       */
      nextZIndex: function() {
        var tMaxZIndex = 1;
        this.get('componentViews').forEach( function( iView) {
          var tZIndex = iView.get('layout').zIndex;
          if( tZIndex)
              tMaxZIndex = Math.max( tMaxZIndex, tZIndex);
        });
        return tMaxZIndex + 1;
      }.property(),

      /**
       * @property {DG.InspectorView}
       */
      inspectorView: null,

      /**
       * These rectangles are in addition to rectangles occupied by componentViews. Typically,
       * they are reserved for the duration of an animation of a component to its final location, and
       * then given up.
       * @property { [{{left: {Number}, top: {Number}, width: {Number}, height: {Number}}}]
       */
      reservedRects: null,

      // We want the container to encompass the entire window or the
      // entire content, whichever is greater.
      layout: { left: 0, top: 0, minWidth: '100%', minHeight: '100%' },

      /**
        Indicates that this view's layout should never be considered fixed.
        A fixed layout has a fixed width and height and is unaffected by changes
        to the parent view's size/layout. This view sometimes has a "fixed" width
        and/or height in it layout (for scrolling purposes) but should always
        refresh its layout when its parent view changes. Therefore, we override
        this base class computed property to always return false, so that it can
        respond appropriately when its parent view changes.
        @returns  {Boolean} false -- this view's layout is never fixed
       */
      isFixedLayout: function() {
        return false;
      }.property(),

      init: function() {
        sc_super();
        this.reservedRects = [];
        this.set('inspectorView', DG.InspectorView.create( {
          componentContainer: this
        }));
        this.appendChild( this.get('inspectorView'));
      },

      /**
       * There may be child views other than DG.ComponentView. E.g. in one prototype of showing
       * the map in the background, the map view was a child but not a ComponentView.
       * In standalone mode, we don't return those with content DG.GameView
       * @property {Array of DG.ComponentView }
       */
      componentViews: function() {
        return this.get('childViews' ).filter( function (iChildView) {
          return (iChildView instanceof DG.ComponentView) &&
              !(DG.STANDALONE_MODE && iChildView.contentIsInstanceOf(DG.GameView));
        });
      }.property( 'childViews'),

      allComponentViews: function() {
        var tSavedMode = DG.STANDALONE_MODE;
        DG.STANDALONE_MODE = false; // temporarily
        var tViews = this.get('componentViews');
        DG.STANDALONE_MODE = tSavedMode;
        return tViews;
      }.property('childViews'),

      /**
       * @property {Array of Object }
       */
      tileMenuItems: function() {

        function componentViewToClassName(iView) {
          var tContentView = iView.get('contentView');
          if( tContentView) {
            switch (tContentView.constructor) {
              case DG.TableView:
                return 'tile-icon-table';
              case DG.CaseCardView:
                return 'tile-icon-card';
              case DG.GraphView:
                return 'tile-icon-graph';
              case DG.MapView:
                return 'tile-icon-map';
              case DG.SliderView:
                return 'tile-icon-slider';
              case DG.Calculator:
                return 'tile-icon-calc';
              case DG.TextView:
                return 'tile-icon-comment';
              case DG.GuideView:
                return 'tile-icon-guide';
              case DG.GameView:
                return 'tile-icon-dataTool';
              case DG.WebView:
              case SC.WebView:
                return 'tile-icon-mediaTool';
              default:
                return 'tile-icon-dataTool';
            }
          }
        }

        var tItems = [];
        this.get('componentViews').forEach( function( iComponentView) {
          if( iComponentView.get( 'isVisible')) {
            tItems.push({
              title: iComponentView.get('title'),
              target: iComponentView,
              action: 'maximizeAndSelect',
              icon: componentViewToClassName(iComponentView)
            });
          }
        });
        return tItems;
      }.property(),

      /**
        Computes/returns the bounding rectangle for the view.
       */
      updateFrame: function() {
        // Note that we're not providing scroll bars to scroll to left or above document
        var tWidth = 0, tHeight = 0;

        // Compute the content size as the bounding rectangle of the child views.
        this.get('childViews').forEach(
                          function( iView) {
                            var tLayout = iView.get('layout');
                            // Rarely, a layout will be missing the fields we need
                            // NB: Attempting to call get('frame') causes infinite recursion
                            tWidth = Math.max( tWidth, (tLayout.left || 0) + (tLayout.width || 0));
                            tHeight = Math.max( tHeight, (tLayout.top || 0) + (tLayout.height || 0));
                          });
        // Add a margin around the components as part of the content
        tWidth += kDocMargin;
        tHeight += kDocMargin;

        this.adjust({ width: tWidth, height: tHeight });
      },

      removeComponentView: function( iComponentView) {
        var tCloseAction = iComponentView.get('closeAction');
        if( tCloseAction) {
          tCloseAction.action.apply( tCloseAction.target, tCloseAction.args );
        }
        else {
          this.select(null);
          DG.currDocumentController().removeComponentAssociatedWithView( iComponentView);
          iComponentView.destroy();
        }
        this.updateFrame();
      },

      /**
       Removes all children from the parentView.

       @returns {SC.View} receiver
       */
      destroyAllChildren: function () {
        this.select(null);  // To close inspector
        var componentViews = this.get('allComponentViews');
        componentViews.forEach(function (iView) {
          if (iView && iView.willDestroy)
            iView.willDestroy();
          iView.destroy();
        });
        return this;
      },

      /**
       * @property{DG.ComponentView}
       */
      selectedChildView: null,

      /**
       * The given child view, if not minimized, will become the currently selected childView.
       * @param iChildView
       */
      select: function( iChildView) {
        var tCurrentSelected = this.get('selectedChildView'),
            tIsMinimized = iChildView && iChildView.get('isMinimized');
        if( iChildView) {
          this.bringToFront( iChildView);
        }
        if( iChildView !== tCurrentSelected && !tIsMinimized) {
          DG.globalEditorLock.commitCurrentEdit();
          if( tCurrentSelected)
            tCurrentSelected.set('isSelected', false);
          this.set('selectedChildView', iChildView);
          if( iChildView) {
            iChildView.set('isSelected', true);
          }
        }
      },

      /* bringToFront - The given child view will be placed at the end of the list, thus
        rendered last and appearing in front of all others.
      */
      bringToFront: function( iChildView) {
        if( iChildView.assignZ) {
          iChildView.assignZ(this.get('nextZIndex'));
          this.incrementProperty('nextZIndex');
        }
      },

      /* sendToBack - The given child view will be placed at the beginning of the list, thus
        rendered first and appearing behind all others.
      */
      sendToBack: function( iChildView) {
        var domThisElement = this.get('layer'),
            domChildElement = iChildView.get('layer'),
            scChildViews = this.get('childViews'),
            scChildViewCount = scChildViews.get('length'),
            i;

        // move the specified SC child view to the beginning of the child views
        // no need to check the first child, since it wouldn't need to be moved
        for( i = 1; i < scChildViewCount; ++i) {
          if(scChildViews[i] === iChildView) {
            // remove it from its current location
            scChildViews.splice(i, 1);
            // add it to the beginning of the array
            scChildViews.unshift(iChildView);
            break;
          }
        }

        // move the specified child DOM element before the first DOM child
        if(domThisElement.firstChild !== domChildElement) {
          // will automatically remove it from its current location, if necessary
          domThisElement.insertBefore(domChildElement, domThisElement.firstChild);
        }
      },

      /** positionNewComponent - It is assumed that the given view has not yet been added
        and that its layout has the desired width and height.
        We find a non-overlapping position for the view and place it there.
        @param{DG.ComponentView} - the view to be positioned
        @param {String} Default is 'top'
        @param {boolean} iPositionGameViews - true to set GameView positions as well as sizes
      */
      positionNewComponent: function( iView, iPosition, iPositionGameViews) {
        var this_ = this,
            tViewRect = iView.get( 'frame'),
            tDocRect = this.parentView.get('clippingFrame'),
            tFrameWithinParent = this.computeFrameWithParentFrame(),
            tOffset = { x: -tFrameWithinParent.x, y: -tFrameWithinParent.y},
            tViewRects = this.get('componentViews').map(function (iView) {
              return iView.get('isVisible') ? iView.get('frame') : {x: 0, y: 0, width: 0, height: 0};
            }),
            tReservedRects = this.get('reservedRects'),
            tLoc = DG.ViewUtilities.findEmptyLocationForRect(
                                      tViewRect,
                                      tDocRect,
                                      tOffset,
                                      tViewRects.concat( tReservedRects),
                                      iPosition);
        if (DG.KEEP_IN_BOUNDS_PREF) {
          var HORIZONTALBUFFER = 50,
              containerWidth = $('#codap').width();
          if ((tLoc.x + tViewRect.width + HORIZONTALBUFFER) > containerWidth) {
            tLoc.x = Math.max(0, containerWidth - tViewRect.width - HORIZONTALBUFFER);
          }
          var tDocView = this.parentView;
          while (!SC.none(tDocView.parentView.parentView)) {
            tDocView = tDocView.parentView;
          }
          var containerHeight = $('#codap').height() - tDocView.get('frame').y;
          if ((tLoc.y + tViewRect.height) > containerHeight) {
            tLoc.y = Math.max(0, containerHeight - tViewRect.height);
          }
        }
        var tFinalRect = { x: tLoc.x, y: tLoc.y, width: tViewRect.width, height: tViewRect.height},
            tOptions = { duration: 0.5, timing: 'ease-in-out'},
            tIsGameView = iView.get('contentView').constructor === DG.GameView;
        if( tIsGameView) {
          // As of 2016-10-26, tFinalRect was being passed to iView.adjust() directly for GameViews,
          // apparently with the intent that the game view would be moved to the rectangle specified.
          // The adjust() function takes a layout object with { left, right }, rather than a rectangle
          // with { x, y }, however, so since then the size of GameViews has been set but not their location.
          // Rather than changing that behavior at this point, we introduce the iPositionGameViews
          // parameter which allows clients aware of the issue to specify that they do want the
          // location of GameViews to be set as well.
          var newLayout = { width: tFinalRect.width, height: tFinalRect.height };
          if (iPositionGameViews) {
            newLayout.left = tFinalRect.x;
            newLayout.top = tFinalRect.y;
          }
          iView.adjust(newLayout);
        }
        else {
          tReservedRects.push(tFinalRect);
          this.invokeNext(function () {
            iView.adjust({
              left: DG.ViewUtilities.kGridSize, top: DG.ViewUtilities.kGridSize,
              width: 0, height: 0
            });
            iView.animate({left: tLoc.x, top: tLoc.y, width: tViewRect.width, height: tViewRect.height}, tOptions,
                function () {
                  // map component doesn't come out right without the following kludge
                  // Todo: Figure out how to do this with less kludge. Possibly install the contentView
                  // after the animation has completed? Or set the size of the contentView and simply
                  // expand onto it.
                  this.adjust('width', tViewRect.width + 1);
                  this.adjust('width', tViewRect.width);
                  this.adjust('height', tViewRect.height + 1);
                  this.adjust('height', tViewRect.height);
                  this.select();
                  this_.updateFrame();
                  tReservedRects.splice(tReservedRects.indexOf(tFinalRect), 1);
                  // beginEditing applies only to text component, but couldn't find a better place to put this
                  // Better would be to define something like 'didReachFinalPosition' as a generic component
                  this.didReachInitialPosition();
                });
          }.bind(this));
          iView.adjust({width: 0, height: 0});
        }
        return tFinalRect;
      },

      /** coverUpComponentViews - Request each component view to cover up its contents with a see-through layer.
       * We need to do this when we're dragging or resizing one component, so that the event handlers in components
       * we are passing over don't get in the way.
       * @param{String} either 'cover' or 'uncover'
      */
      coverUpComponentViews: function( iAction) {
        this.allComponentViews().forEach( function( iView) {
          if( iView.get('isVisible'))
            iView.cover( iAction);
        });
      },

      mouseDown: function( iEvent) {
        this.select(null);
        return true;
      },

      touchStart: function(iEvent) {
        return this.mouseDown( iEvent);
      }

    };  // object returned closure
  }()) // function closure
);

<commit_msg>[#169266259] Fix bug: standalong plugins loaded multiple times<commit_after>// ==========================================================================
//                          DG.ContainerView
//
//  The top level view in a DG document.
//
//  Author:   William Finzer
//
//  Copyright (c) 2014 by The Concord Consortium, Inc. All rights reserved.
//
//  Licensed under the Apache License, Version 2.0 (the "License");
//  you may not use this file except in compliance with the License.
//  You may obtain a copy of the License at
//
//    http://www.apache.org/licenses/LICENSE-2.0
//
//  Unless required by applicable law or agreed to in writing, software
//  distributed under the License is distributed on an "AS IS" BASIS,
//  WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
//  See the License for the specific language governing permissions and
//  limitations under the License.
// ==========================================================================

sc_require('views/inspector/inspector_view');

/** @class

  DG.ContainerView is the superview for the component views in the document.

  @extends SC.View
*/
DG.ContainerView = SC.View.extend(
/** @scope DG.ContainerView.prototype */
  (function() {
    var kDocMargin = 16;
    return {
      classNames: 'dg-container-view'.w(),

      isResizable: YES,

      /**
       * To bring a component frontmost, we assign it this value and then increment the value.
       * We don't worry about running out of numbers.
       * @property{Number}
       */
      nextZIndex: function() {
        var tMaxZIndex = 100;
        this.get('componentViews').forEach( function( iView) {
          var tZIndex = iView.get('layout').zIndex;
          if( tZIndex)
              tMaxZIndex = Math.max( tMaxZIndex, tZIndex);
        });
        return tMaxZIndex + 1;
      }.property(),
      lowerZIndex: function() {
        var tMinZIndex = 100;
        this.get('componentViews').forEach( function( iView) {
          var tZIndex = iView.get('layout').zIndex;
          if( tZIndex)
              tMinZIndex = Math.min( tMinZIndex, tZIndex);
        });
        return tMinZIndex - 1;
      }.property(),

      /**
       * @property {DG.InspectorView}
       */
      inspectorView: null,

      /**
       * These rectangles are in addition to rectangles occupied by componentViews. Typically,
       * they are reserved for the duration of an animation of a component to its final location, and
       * then given up.
       * @property { [{{left: {Number}, top: {Number}, width: {Number}, height: {Number}}}]
       */
      reservedRects: null,

      // We want the container to encompass the entire window or the
      // entire content, whichever is greater.
      layout: { left: 0, top: 0, minWidth: '100%', minHeight: '100%' },

      /**
        Indicates that this view's layout should never be considered fixed.
        A fixed layout has a fixed width and height and is unaffected by changes
        to the parent view's size/layout. This view sometimes has a "fixed" width
        and/or height in it layout (for scrolling purposes) but should always
        refresh its layout when its parent view changes. Therefore, we override
        this base class computed property to always return false, so that it can
        respond appropriately when its parent view changes.
        @returns  {Boolean} false -- this view's layout is never fixed
       */
      isFixedLayout: function() {
        return false;
      }.property(),

      init: function() {
        sc_super();
        this.reservedRects = [];
        this.set('inspectorView', DG.InspectorView.create( {
          componentContainer: this
        }));
        this.appendChild( this.get('inspectorView'));
      },

      /**
       * There may be child views other than DG.ComponentView. E.g. in one prototype of showing
       * the map in the background, the map view was a child but not a ComponentView.
       * In standalone mode, we don't return those with content DG.GameView
       * @property {Array of DG.ComponentView }
       */
      componentViews: function() {
        return this.get('childViews' ).filter( function (iChildView) {
          return (iChildView instanceof DG.ComponentView) &&
              !(DG.STANDALONE_MODE && iChildView.contentIsInstanceOf(DG.GameView));
        });
      }.property( 'childViews'),

      allComponentViews: function() {
        var tSavedMode = DG.STANDALONE_MODE;
        DG.STANDALONE_MODE = false; // temporarily
        var tViews = this.get('componentViews');
        DG.STANDALONE_MODE = tSavedMode;
        return tViews;
      }.property('childViews'),

      /**
       * @property {Array of Object }
       */
      tileMenuItems: function() {

        function componentViewToClassName(iView) {
          var tContentView = iView.get('contentView');
          if( tContentView) {
            switch (tContentView.constructor) {
              case DG.TableView:
                return 'tile-icon-table';
              case DG.CaseCardView:
                return 'tile-icon-card';
              case DG.GraphView:
                return 'tile-icon-graph';
              case DG.MapView:
                return 'tile-icon-map';
              case DG.SliderView:
                return 'tile-icon-slider';
              case DG.Calculator:
                return 'tile-icon-calc';
              case DG.TextView:
                return 'tile-icon-comment';
              case DG.GuideView:
                return 'tile-icon-guide';
              case DG.GameView:
                return 'tile-icon-dataTool';
              case DG.WebView:
              case SC.WebView:
                return 'tile-icon-mediaTool';
              default:
                return 'tile-icon-dataTool';
            }
          }
        }

        var tItems = [];
        this.get('componentViews').forEach( function( iComponentView) {
          if( iComponentView.get( 'isVisible')) {
            tItems.push({
              title: iComponentView.get('title'),
              target: iComponentView,
              action: 'maximizeAndSelect',
              icon: componentViewToClassName(iComponentView)
            });
          }
        });
        return tItems;
      }.property(),

      /**
        Computes/returns the bounding rectangle for the view.
       */
      updateFrame: function() {
        // Note that we're not providing scroll bars to scroll to left or above document
        var tWidth = 0, tHeight = 0;

        // Compute the content size as the bounding rectangle of the child views.
        this.get('childViews').forEach(
                          function( iView) {
                            var tLayout = iView.get('layout');
                            // Rarely, a layout will be missing the fields we need
                            // NB: Attempting to call get('frame') causes infinite recursion
                            tWidth = Math.max( tWidth, (tLayout.left || 0) + (tLayout.width || 0));
                            tHeight = Math.max( tHeight, (tLayout.top || 0) + (tLayout.height || 0));
                          });
        // Add a margin around the components as part of the content
        tWidth += kDocMargin;
        tHeight += kDocMargin;

        this.adjust({ width: tWidth, height: tHeight });
      },

      removeComponentView: function( iComponentView) {
        var tCloseAction = iComponentView.get('closeAction');
        if( tCloseAction) {
          tCloseAction.action.apply( tCloseAction.target, tCloseAction.args );
        }
        else {
          this.select(null);
          DG.currDocumentController().removeComponentAssociatedWithView( iComponentView);
          iComponentView.destroy();
        }
        this.updateFrame();
      },

      /**
       Removes all children from the parentView.

       @returns {SC.View} receiver
       */
      destroyAllChildren: function () {
        this.select(null);  // To close inspector
        var componentViews = this.get('allComponentViews');
        componentViews.forEach(function (iView) {
          if (iView && iView.willDestroy)
            iView.willDestroy();
          iView.destroy();
        });
        return this;
      },

      /**
       * @property{DG.ComponentView}
       */
      selectedChildView: null,

      /**
       * The given child view, if not minimized, will become the currently selected childView.
       * @param iChildView
       */
      select: function( iChildView) {
        var tCurrentSelected = this.get('selectedChildView'),
            tIsMinimized = iChildView && iChildView.get('isMinimized');
        if( iChildView) {
          this.bringToFront( iChildView);
        }
        if( iChildView !== tCurrentSelected && !tIsMinimized) {
          DG.globalEditorLock.commitCurrentEdit();
          if( tCurrentSelected)
            tCurrentSelected.set('isSelected', false);
          this.set('selectedChildView', iChildView);
          if( iChildView) {
            iChildView.set('isSelected', true);
          }
        }
      },

      /* bringToFront - The given child view will be placed at the end of the list, thus
        rendered last and appearing in front of all others.
      */
      bringToFront: function( iChildView) {
        if( iChildView.assignZ) {
          iChildView.assignZ(this.get('nextZIndex'));
        }
      },

      /* sendToBack - The given child view will be placed at the beginning of the list, thus
        rendered first and appearing behind all others.
      */
      sendToBack: function( iChildView) {
        if (iChildView.assignZ) {
          iChildView.assignZ(this.get('lowerZIndex'));
        }
      },

      /** positionNewComponent - It is assumed that the given view has not yet been added
        and that its layout has the desired width and height.
        We find a non-overlapping position for the view and place it there.
        @param{DG.ComponentView} - the view to be positioned
        @param {String} Default is 'top'
        @param {boolean} iPositionGameViews - true to set GameView positions as well as sizes
      */
      positionNewComponent: function( iView, iPosition, iPositionGameViews) {
        var this_ = this,
            tViewRect = iView.get( 'frame'),
            tDocRect = this.parentView.get('clippingFrame'),
            tFrameWithinParent = this.computeFrameWithParentFrame(),
            tOffset = { x: -tFrameWithinParent.x, y: -tFrameWithinParent.y},
            tViewRects = this.get('componentViews').map(function (iView) {
              return iView.get('isVisible') ? iView.get('frame') : {x: 0, y: 0, width: 0, height: 0};
            }),
            tReservedRects = this.get('reservedRects'),
            tLoc = DG.ViewUtilities.findEmptyLocationForRect(
                                      tViewRect,
                                      tDocRect,
                                      tOffset,
                                      tViewRects.concat( tReservedRects),
                                      iPosition);
        if (DG.KEEP_IN_BOUNDS_PREF) {
          var HORIZONTALBUFFER = 50,
              containerWidth = $('#codap').width();
          if ((tLoc.x + tViewRect.width + HORIZONTALBUFFER) > containerWidth) {
            tLoc.x = Math.max(0, containerWidth - tViewRect.width - HORIZONTALBUFFER);
          }
          var tDocView = this.parentView;
          while (!SC.none(tDocView.parentView.parentView)) {
            tDocView = tDocView.parentView;
          }
          var containerHeight = $('#codap').height() - tDocView.get('frame').y;
          if ((tLoc.y + tViewRect.height) > containerHeight) {
            tLoc.y = Math.max(0, containerHeight - tViewRect.height);
          }
        }
        var tFinalRect = { x: tLoc.x, y: tLoc.y, width: tViewRect.width, height: tViewRect.height},
            tOptions = { duration: 0.5, timing: 'ease-in-out'},
            tIsGameView = iView.get('contentView').constructor === DG.GameView;
        if( tIsGameView) {
          // As of 2016-10-26, tFinalRect was being passed to iView.adjust() directly for GameViews,
          // apparently with the intent that the game view would be moved to the rectangle specified.
          // The adjust() function takes a layout object with { left, right }, rather than a rectangle
          // with { x, y }, however, so since then the size of GameViews has been set but not their location.
          // Rather than changing that behavior at this point, we introduce the iPositionGameViews
          // parameter which allows clients aware of the issue to specify that they do want the
          // location of GameViews to be set as well.
          var newLayout = { width: tFinalRect.width, height: tFinalRect.height };
          if (iPositionGameViews) {
            newLayout.left = tFinalRect.x;
            newLayout.top = tFinalRect.y;
          }
          iView.adjust(newLayout);
        }
        else {
          tReservedRects.push(tFinalRect);
          this.invokeNext(function () {
            iView.adjust({
              left: DG.ViewUtilities.kGridSize, top: DG.ViewUtilities.kGridSize,
              width: 0, height: 0
            });
            iView.animate({left: tLoc.x, top: tLoc.y, width: tViewRect.width, height: tViewRect.height}, tOptions,
                function () {
                  // map component doesn't come out right without the following kludge
                  // Todo: Figure out how to do this with less kludge. Possibly install the contentView
                  // after the animation has completed? Or set the size of the contentView and simply
                  // expand onto it.
                  this.adjust('width', tViewRect.width + 1);
                  this.adjust('width', tViewRect.width);
                  this.adjust('height', tViewRect.height + 1);
                  this.adjust('height', tViewRect.height);
                  this.select();
                  this_.updateFrame();
                  tReservedRects.splice(tReservedRects.indexOf(tFinalRect), 1);
                  // beginEditing applies only to text component, but couldn't find a better place to put this
                  // Better would be to define something like 'didReachFinalPosition' as a generic component
                  this.didReachInitialPosition();
                });
          }.bind(this));
          iView.adjust({width: 0, height: 0});
        }
        return tFinalRect;
      },

      /** coverUpComponentViews - Request each component view to cover up its contents with a see-through layer.
       * We need to do this when we're dragging or resizing one component, so that the event handlers in components
       * we are passing over don't get in the way.
       * @param{String} either 'cover' or 'uncover'
      */
      coverUpComponentViews: function( iAction) {
        this.allComponentViews().forEach( function( iView) {
          if( iView.get('isVisible'))
            iView.cover( iAction);
        });
      },

      mouseDown: function( iEvent) {
        this.select(null);
        return true;
      },

      touchStart: function(iEvent) {
        return this.mouseDown( iEvent);
      }

    };  // object returned closure
  }()) // function closure
);

<|endoftext|>


<commit_before>/**
 * Developer: Denis Krasnovsky
 * Date: 19.01.2016
 * Copyright: 2009-2016 Comindware®
 *       All Rights Reserved
 *
 * THIS IS UNPUBLISHED PROPRIETARY SOURCE CODE OF Comindware
 *       The copyright notice above does not evidence any
 *       actual or intended publication of such source code.
 */

/* global define, require, Handlebars, Backbone, Marionette, $, _, Localizer */

define(['core/libApi',
        'core/list/listApi',
        'core/utils/utilsApi',
        '../models/SearchMoreModel',
        '../models/DefaultReferenceModel'],
    function (lib, list, utils, SearchMoreModel, DefaultReferenceModel) {
        'use strict';

        var DemoReferenceCollections = Backbone.Collection.extend({
            model: DefaultReferenceModel
        });

        return Marionette.Controller.extend({
            initialize:function(){
                this.collection = list.factory.createWrappedCollection(new DemoReferenceCollections([]),{ delayedAdd:false });
            },

            fetch:function(options){
                this.totalCount = 123123;
                this.options = options;

                   if (this.options.text) {
                    var filterText = this.options.text.trim().toUpperCase();
                    if (filterText) {
                        this.collection.filter(function (model) {
                            if (model instanceof SearchMoreModel) {
                                return true;
                            }
                            var text = model.get('text');
                            if (!text) {
                                return false;
                            }
                            return text.toUpperCase().indexOf(filterText) !== -1;
                        });
                    } else {
                        this.collection.filter(null);
                    }
                } else {
                    this.collection.filter(null);
                }

                return new Promise.resolve();
            },

            objectGenerator: function(names){
                for(var i = 0; i<= names.length; i++ ){
                    if(!_.contains(names,'reference.'+i)){
                        return {
                            id:'reference.'+ i,
                            text: 'test'+i
                        };
                    }
                }
            },

            addNewItem: function () {
                var model;

                if(this.collection.length>1){
                    var names = [];
                    this.collection.models.filter(function(model){
                        if(model.id){
                         names.push(model.id);
                        return true;
                    } else {
                            return false;
                        }
                    });

                    var newItem = this.objectGenerator(names);
                     model = new DefaultReferenceModel(newItem);
                }  else {
                    model = new DefaultReferenceModel({
                        id: 'reference.0',
                        text: 'test'
                    });
                }

               this.collection.add(model);
               this.collection.select(model);

            },

            navigate: function (model) {
                utils.helpers.throwError('Not Implemented.', 'NotImplementedError');
            }
        });

    });
<commit_msg>Fixed formating<commit_after>/**
 * Developer: Denis Krasnovsky
 * Date: 19.01.2016
 * Copyright: 2009-2016 Comindware®
 *       All Rights Reserved
 *
 * THIS IS UNPUBLISHED PROPRIETARY SOURCE CODE OF Comindware
 *       The copyright notice above does not evidence any
 *       actual or intended publication of such source code.
 */

/* global define, require, Handlebars, Backbone, Marionette, $, _, Localizer */

define(['core/libApi',
        'core/list/listApi',
        'core/utils/utilsApi',
        '../models/SearchMoreModel',
        '../models/DefaultReferenceModel'],
    function (lib, list, utils, SearchMoreModel, DefaultReferenceModel) {
        'use strict';

        var DemoReferenceCollections = Backbone.Collection.extend({
            model: DefaultReferenceModel
        });

        return Marionette.Controller.extend({
            initialize:function(){
                this.collection = list.factory.createWrappedCollection(new DemoReferenceCollections([]),
                    { delayedAdd:false });
            },

            fetch:function(options){
                this.totalCount = 123123;
                this.options = options;

                   if (this.options.text) {
                    var filterText = this.options.text.trim().toUpperCase();
                    if (filterText) {
                        this.collection.filter(function (model) {
                            if (model instanceof SearchMoreModel) {
                                return true;
                            }
                            var text = model.get('text');
                            if (!text) {
                                return false;
                            }
                            return text.toUpperCase().indexOf(filterText) !== -1;
                        });
                    } else {
                        this.collection.filter(null);
                    }
                } else {
                    this.collection.filter(null);
                }

                return new Promise.resolve();
            },

            objectGenerator: function(names){
                for(var i = 0; i<= names.length; i++ ){
                    if(!_.contains(names,'reference.'+i)){
                        return {
                            id:'reference.'+ i,
                            text: 'test'+i
                        };
                    }
                }
            },

            addNewItem: function () {
                var model;

                if(this.collection.length>1){
                    var names = [];
                    this.collection.models.filter(function(model){
                        if(model.id){
                         names.push(model.id);
                        return true;
                    } else {
                            return false;
                        }
                    });

                    var newItem = this.objectGenerator(names);
                     model = new DefaultReferenceModel(newItem);
                }  else {
                    model = new DefaultReferenceModel({
                        id: 'reference.0',
                        text: 'test'
                    });
                }

               this.collection.add(model);
               this.collection.select(model);
            },

            navigate: function (model) {
                utils.helpers.throwError('Not Implemented.', 'NotImplementedError');
            }
        });
    });
<|endoftext|>


<commit_before>var VisioLink = {
    openVisio: function(from, id, withVideo) {
        Notification.incomingAnswer();
        var idUrl = id ? '/' + id : '';
        var page = withVideo ? 'visio' : 'visioaudio';

        if (typeof Android !== 'undefined') {
            Android.openVisio(BASE_URI + '?' + page + '/' + from + idUrl);
        } else {
            VisioLink.window = window.open('?' + page + '/' + from + idUrl, '', 'width=600,height=400,status=0,titlebar=0,toolbar=0,menubar=0');
        }
    }
}
<commit_msg>Another small fix for Android<commit_after>var VisioLink = {
    openVisio: function(from, id, withVideo) {
        Notification.incomingAnswer();
        var idUrl = id ? '/' + id : '';
        var page = withVideo ? 'visio' : 'visioaudio';

        if (typeof Android !== 'undefined') {
            Android.openVisio(BASE_HOST + '/' + '?' + page + '/' + from + idUrl);
        } else {
            VisioLink.window = window.open('?' + page + '/' + from + idUrl, '', 'width=600,height=400,status=0,titlebar=0,toolbar=0,menubar=0');
        }
    }
}
<|endoftext|>


<commit_before>const fs = require("fs");
const { parse } = require("json2csv");
const moment = require("moment");
const {
  db,
  LIST_ARTICLES,
  CASE_BY_ID,
  METHOD_BY_ID,
  ORGANIZATION_BY_ID,
} = require("./db.js");

function sanitizeUserName(name) {
  let editedName = name;
  // if the user name is an email address, remove email domain part
  const atSymbolIndex = name.indexOf("@");
  if (atSymbolIndex > 0) {
    editedName = name.substr(0, atSymbolIndex);
  }
  return editedName;
}

const orderedCaseFields = [
  "id",
  "type",
  "title",
  "url",
  "description",
  "featured",
  "post_date",
  "updated_date",
  "creator_id",
  "creator_name",
  "creator_profile_url",
  "last_updated_by_id",
  "last_updated_by_name",
  "last_updated_by_profile_url",
  "original_language",
  "address1",
  "address2",
  "city",
  "province",
  "postal_code",
  "country",
  "latitude",
  "longitude",
  "is_component_of_id",
  "is_component_of_title",
  "is_component_of_url",
  "specific_methods_tools_techniques_titles",
  "has_components_titles",
  "general_issues",
  "specific_topics",
  "scope_of_influence",
  "start_date",
  "end_date",
  "ongoing",
  "time_limited",
  "purposes",
  "approaches",
  "public_spectrum",
  "number_of_participants",
  "open_limited",
  "recruitment_method",
  "targeted_participants",
  "method_types",
  "tools_techniques_types",
  "legality",
  "facilitators",
  "facilitator_training",
  "facetoface_online_or_both",
  "participants_interactions",
  "learning_resources",
  "decision_methods",
  "if_voting",
  "insights_outcomes",
  "primary_organizer_id",
  "primary_organizer_title",
  "primary_organizer_url",
  "organizer_types",
  "funder",
  "funder_types",
  "staff",
  "volunteers",
  "impact_evidence",
  "change_types",
  "implementers_of_change",
  "formal_evaluation",
  "body",
  "files_count",
  "links_count",
  "photos_count",
  "videos_count",
  "audio_count",
  "evaluation_reports_count",
  "evaluation_links_count",
];

const orderedMethodFields = [
  "id",
  "type",
  "title",
  "url",
  "description",
  "featured",
  "post_date",
  "updated_date",
  "creator_id",
  "creator_name",
  "creator_profile_url",
  "last_updated_by_id",
  "last_updated_by_name",
  "last_updated_by_profile_url",
  "original_language",
  "facilitators",
  "facetoface_online_or_both",
  "public_spectrum",
  "open_limited",
  "recruitment_method",
  "level_polarization",
  "level_complexity",
  "method_types",
  "number_of_participants",
  "scope_of_influence",
  "participants_interactions",
  "decision_methods",
  "if_voting",
  "purpose_method",
  "body",
  "photos_count",
  "files_count",
  "videos_count",
  "links_count",
  "audio_count",
];

const orderedOrganizationFields = [
  "id",
  "type",
  "title",
  "url",
  "description",
  "featured",
  "post_date",
  "updated_date",
  "creator_id",
  "creator_name",
  "creator_profile_url",
  "last_updated_by_id",
  "last_updated_by_name",
  "last_updated_by_profile_url",
  "original_language",
  "address1",
  "address2",
  "city",
  "province",
  "postal_code",
  "country",
  "latitude",
  "longitude",
  "sector",
  "scope_of_influence",
  "type_method",
  "type_tool",
  "specific_topics",
  "general_issues",
  "specific_methods_tools_techniques",
  "body",
  "photos_count",
  "files_count",
  "videos_count",
  "links_count",
  "audio_count",
];

const orderedFieldsByType = {
  case: orderedCaseFields,
  method: orderedMethodFields,
  organization: orderedOrganizationFields,
};

function convertToIdTitleUrlFields(entry, field) {
  if (entry[field]) {
    entry[`${field}_id`] = entry[field].id;
    entry[`${field}_title`] = entry[field].title;
    entry[`${field}_url`] = `https://participedia.net/${entry[field].type}/${
      entry[field].id
    }`;
  }
  return entry;
}

async function createCSVDataDump(type, results = []) {
  var entries = results;

  if (type === 'thing') {
    entries = await db.many(LIST_ARTICLES, {
      type: type + "s",
      lang: "en",
    });
  }

  const sqlForType = {
    case: CASE_BY_ID,
    method: METHOD_BY_ID,
    organization: ORGANIZATION_BY_ID,
  };

  const fullEntries = [];
  await Promise.all(
    entries.map(async article => {
      let articleType = type == 'thing' ? article.type : type;
      if (articleType !== 'collection') {
        const params = {
          type: articleType,
          view: "view",
          articleid: article.id,
          lang: "en",
          userid: null,
        };
        const articleRow = await db.one(sqlForType[articleType], params);

        fullEntries.push(articleRow.results);
      }
    })
  );

  const editedEntries = fullEntries.map(entry => {
    let editedEntry = Object.assign({}, entry);
    // add article url
    editedEntry.url = `https://participedia.net/${editedEntry.type}/${
      editedEntry.id
    }`;

    // strip html from body
    editedEntry.body = editedEntry.body.replace(/<\/?[^>]+(>|$)/g, " ");

    // max characters for an excel cell is 32766 so trimming
    // the body length so excel doesn't throw errors
    // https://support.office.com/en-us/article/excel-specifications-and-limits-1672b34d-7043-467e-8e27-269d656771c3
    const MAX_CHAR_LENGTH = 32766;
    if (editedEntry.body && editedEntry.body.length > MAX_CHAR_LENGTH) {
      editedEntry.body = editedEntry.body.substring(0, MAX_CHAR_LENGTH);
    }

    // add creator and last_updated_by name and profile url
    if (editedEntry.creator) {
      editedEntry.creator_id = editedEntry.creator.user_id;
      editedEntry.creator_name = sanitizeUserName(editedEntry.creator.name);
      editedEntry.creator_profile_url = `https://participedia.net/user/${
        editedEntry.creator.user_id
      }`;
    }

    if (editedEntry.last_updated_by) {
      editedEntry.last_updated_by_id = editedEntry.last_updated_by.user_id;
      editedEntry.last_updated_by_name = sanitizeUserName(
        editedEntry.last_updated_by.name
      );
      editedEntry.last_updated_by_profile_url = `https://participedia.net/user/${
        editedEntry.last_updated_by.user_id
      }`;
    }

    // make sure all date fields are in the same format, ISO 8601
    const dateFields = ["post_date", "updated_date", "start_date", "end_date"];
    dateFields.forEach(field => {
      editedEntry[field] = moment(editedEntry[field]).toISOString();
    });

    // convert primary_organizer and is_component_of into three new fields (id, title, url)
    if (editedEntry.type === "case") {
      editedEntry = convertToIdTitleUrlFields(editedEntry, "is_component_of");
      editedEntry = convertToIdTitleUrlFields(editedEntry, "primary_organizer");
    }

    // convert specific_methods_tools_techniques into three new fields (id, title, url)
    if (editedEntry.type === "organization") {
      editedEntry = convertToIdTitleUrlFields(
        editedEntry,
        "specific_methods_tools_techniques"
      );
    }

    // add counts for media, links and other detailed list fields
    const countFields = [
      "files",
      "links",
      "photos",
      "videos",
      "audio",
      "evaluation_reports",
      "evaluation_links",
    ];
    countFields.forEach(field => {
      if (editedEntry[field]) {
        editedEntry[`${field}_count`] = editedEntry[field].length;
      } else {
        editedEntry[`${field}_count`] = 0;
      }
    });

    // convert specific_methods_tools_techniques, has_components to list of titles
    const articlesToListOfTitles = [
      "has_components",
      "specific_methods_tools_techniques",
    ];
    articlesToListOfTitles.forEach(field => {
      if (editedEntry[field]) {
        editedEntry[`${field}_titles`] = editedEntry[field]
          .map(item => item.title)
          .join(", ");
      }
    });

    // convert simple arrays to strings
    const simpleArrayFields = [
      "general_issues",
      "specific_topics",
      "implementers_of_change",
      "change_types",
      "funder_types",
      "organizer_types",
      "insights_outcomes",
      "if_voting",
      "decision_methods",
      "learning_resources",
      "participants_interactions",
      "tools_techniques_types",
      "method_types",
      "targeted_participants",
      "approaches",
      "purposes",
      "number_of_participants",
      "scope_of_influence",
      "purpose_method",
      "type_method",
      "type_tool",
    ];
    simpleArrayFields.forEach(field => {
      if (editedEntry[field]) {
        editedEntry[field] = editedEntry[field].toString().replace(/,/g, ", ");
      }
    });

    // reorder fields
    const orderOfFields = orderedFieldsByType[editedEntry.type];

    const orderedEntry = {};
    orderOfFields.forEach(field => {
      orderedEntry[field] = editedEntry[field];
    });

    return orderedEntry;
  });

  const fields = Object.keys(editedEntries[0]);

  const opts = { fields };

  const csv = parse(editedEntries, opts);

  const filePath = `./public/participedia-data-${type}s.csv`;

  fs.writeFileSync(filePath, csv);

  return filePath;
}

module.exports = createCSVDataDump;
<commit_msg>Set date fields from ISO to YYYY-MM-DD<commit_after>const fs = require("fs");
const { parse } = require("json2csv");
const moment = require("moment");
const {
  db,
  LIST_ARTICLES,
  CASE_BY_ID,
  METHOD_BY_ID,
  ORGANIZATION_BY_ID,
} = require("./db.js");

function sanitizeUserName(name) {
  let editedName = name;
  // if the user name is an email address, remove email domain part
  const atSymbolIndex = name.indexOf("@");
  if (atSymbolIndex > 0) {
    editedName = name.substr(0, atSymbolIndex);
  }
  return editedName;
}

const orderedCaseFields = [
  "id",
  "type",
  "title",
  "url",
  "description",
  "featured",
  "post_date",
  "updated_date",
  "creator_id",
  "creator_name",
  "creator_profile_url",
  "last_updated_by_id",
  "last_updated_by_name",
  "last_updated_by_profile_url",
  "original_language",
  "address1",
  "address2",
  "city",
  "province",
  "postal_code",
  "country",
  "latitude",
  "longitude",
  "is_component_of_id",
  "is_component_of_title",
  "is_component_of_url",
  "specific_methods_tools_techniques_titles",
  "has_components_titles",
  "general_issues",
  "specific_topics",
  "scope_of_influence",
  "start_date",
  "end_date",
  "ongoing",
  "time_limited",
  "purposes",
  "approaches",
  "public_spectrum",
  "number_of_participants",
  "open_limited",
  "recruitment_method",
  "targeted_participants",
  "method_types",
  "tools_techniques_types",
  "legality",
  "facilitators",
  "facilitator_training",
  "facetoface_online_or_both",
  "participants_interactions",
  "learning_resources",
  "decision_methods",
  "if_voting",
  "insights_outcomes",
  "primary_organizer_id",
  "primary_organizer_title",
  "primary_organizer_url",
  "organizer_types",
  "funder",
  "funder_types",
  "staff",
  "volunteers",
  "impact_evidence",
  "change_types",
  "implementers_of_change",
  "formal_evaluation",
  "body",
  "files_count",
  "links_count",
  "photos_count",
  "videos_count",
  "audio_count",
  "evaluation_reports_count",
  "evaluation_links_count",
];

const orderedMethodFields = [
  "id",
  "type",
  "title",
  "url",
  "description",
  "featured",
  "post_date",
  "updated_date",
  "creator_id",
  "creator_name",
  "creator_profile_url",
  "last_updated_by_id",
  "last_updated_by_name",
  "last_updated_by_profile_url",
  "original_language",
  "facilitators",
  "facetoface_online_or_both",
  "public_spectrum",
  "open_limited",
  "recruitment_method",
  "level_polarization",
  "level_complexity",
  "method_types",
  "number_of_participants",
  "scope_of_influence",
  "participants_interactions",
  "decision_methods",
  "if_voting",
  "purpose_method",
  "body",
  "photos_count",
  "files_count",
  "videos_count",
  "links_count",
  "audio_count",
];

const orderedOrganizationFields = [
  "id",
  "type",
  "title",
  "url",
  "description",
  "featured",
  "post_date",
  "updated_date",
  "creator_id",
  "creator_name",
  "creator_profile_url",
  "last_updated_by_id",
  "last_updated_by_name",
  "last_updated_by_profile_url",
  "original_language",
  "address1",
  "address2",
  "city",
  "province",
  "postal_code",
  "country",
  "latitude",
  "longitude",
  "sector",
  "scope_of_influence",
  "type_method",
  "type_tool",
  "specific_topics",
  "general_issues",
  "specific_methods_tools_techniques",
  "body",
  "photos_count",
  "files_count",
  "videos_count",
  "links_count",
  "audio_count",
];

const orderedFieldsByType = {
  case: orderedCaseFields,
  method: orderedMethodFields,
  organization: orderedOrganizationFields,
};

function convertToIdTitleUrlFields(entry, field) {
  if (entry[field]) {
    entry[`${field}_id`] = entry[field].id;
    entry[`${field}_title`] = entry[field].title;
    entry[`${field}_url`] = `https://participedia.net/${entry[field].type}/${
      entry[field].id
    }`;
  }
  return entry;
}

async function createCSVDataDump(type, results = []) {
  var entries = results;

  if (type === 'thing') {
    entries = await db.many(LIST_ARTICLES, {
      type: type + "s",
      lang: "en",
    });
  }

  const sqlForType = {
    case: CASE_BY_ID,
    method: METHOD_BY_ID,
    organization: ORGANIZATION_BY_ID,
  };

  const fullEntries = [];
  await Promise.all(
    entries.map(async article => {
      let articleType = type == 'thing' ? article.type : type;
      if (articleType !== 'collection') {
        const params = {
          type: articleType,
          view: "view",
          articleid: article.id,
          lang: "en",
          userid: null,
        };
        const articleRow = await db.one(sqlForType[articleType], params);

        fullEntries.push(articleRow.results);
      }
    })
  );

  const editedEntries = fullEntries.map(entry => {
    let editedEntry = Object.assign({}, entry);
    // add article url
    editedEntry.url = `https://participedia.net/${editedEntry.type}/${
      editedEntry.id
    }`;

    // strip html from body
    editedEntry.body = editedEntry.body.replace(/<\/?[^>]+(>|$)/g, " ");

    // max characters for an excel cell is 32766 so trimming
    // the body length so excel doesn't throw errors
    // https://support.office.com/en-us/article/excel-specifications-and-limits-1672b34d-7043-467e-8e27-269d656771c3
    const MAX_CHAR_LENGTH = 32766;
    if (editedEntry.body && editedEntry.body.length > MAX_CHAR_LENGTH) {
      editedEntry.body = editedEntry.body.substring(0, MAX_CHAR_LENGTH);
    }

    // add creator and last_updated_by name and profile url
    if (editedEntry.creator) {
      editedEntry.creator_id = editedEntry.creator.user_id;
      editedEntry.creator_name = sanitizeUserName(editedEntry.creator.name);
      editedEntry.creator_profile_url = `https://participedia.net/user/${
        editedEntry.creator.user_id
      }`;
    }

    if (editedEntry.last_updated_by) {
      editedEntry.last_updated_by_id = editedEntry.last_updated_by.user_id;
      editedEntry.last_updated_by_name = sanitizeUserName(
        editedEntry.last_updated_by.name
      );
      editedEntry.last_updated_by_profile_url = `https://participedia.net/user/${
        editedEntry.last_updated_by.user_id
      }`;
    }

    // make sure all date fields are in the same format, ISO 8601
    const dateFields = ["post_date", "updated_date", "start_date", "end_date"];
    dateFields.forEach(field => {
      editedEntry[field] = moment(editedEntry[field]).isValid() ? moment(editedEntry[field]).format("YYYY-MM-DD") : "";
    });

    // convert primary_organizer and is_component_of into three new fields (id, title, url)
    if (editedEntry.type === "case") {
      editedEntry = convertToIdTitleUrlFields(editedEntry, "is_component_of");
      editedEntry = convertToIdTitleUrlFields(editedEntry, "primary_organizer");
    }

    // convert specific_methods_tools_techniques into three new fields (id, title, url)
    if (editedEntry.type === "organization") {
      editedEntry = convertToIdTitleUrlFields(
        editedEntry,
        "specific_methods_tools_techniques"
      );
    }

    // add counts for media, links and other detailed list fields
    const countFields = [
      "files",
      "links",
      "photos",
      "videos",
      "audio",
      "evaluation_reports",
      "evaluation_links",
    ];
    countFields.forEach(field => {
      if (editedEntry[field]) {
        editedEntry[`${field}_count`] = editedEntry[field].length;
      } else {
        editedEntry[`${field}_count`] = 0;
      }
    });

    // convert specific_methods_tools_techniques, has_components to list of titles
    const articlesToListOfTitles = [
      "has_components",
      "specific_methods_tools_techniques",
    ];
    articlesToListOfTitles.forEach(field => {
      if (editedEntry[field]) {
        editedEntry[`${field}_titles`] = editedEntry[field]
          .map(item => item.title)
          .join(", ");
      }
    });

    // convert simple arrays to strings
    const simpleArrayFields = [
      "general_issues",
      "specific_topics",
      "implementers_of_change",
      "change_types",
      "funder_types",
      "organizer_types",
      "insights_outcomes",
      "if_voting",
      "decision_methods",
      "learning_resources",
      "participants_interactions",
      "tools_techniques_types",
      "method_types",
      "targeted_participants",
      "approaches",
      "purposes",
      "number_of_participants",
      "scope_of_influence",
      "purpose_method",
      "type_method",
      "type_tool",
    ];
    simpleArrayFields.forEach(field => {
      if (editedEntry[field]) {
        editedEntry[field] = editedEntry[field].toString().replace(/,/g, ", ");
      }
    });

    // reorder fields
    const orderOfFields = orderedFieldsByType[editedEntry.type];

    const orderedEntry = {};
    orderOfFields.forEach(field => {
      orderedEntry[field] = editedEntry[field];
    });

    return orderedEntry;
  });

  const fields = Object.keys(editedEntries[0]);

  const opts = { fields };

  const csv = parse(editedEntries, opts);

  const filePath = `./public/participedia-data-${type}s.csv`;

  fs.writeFileSync(filePath, csv);

  return filePath;
}

module.exports = createCSVDataDump;
<|endoftext|>