$.Window('AppDev.UI.FeedbackWindow', {}, {
    init: function(options) {
        // Initialize the base $.Window object
        this._super({
            title: $.formatString('feedbackTitle', AD.Defaults.application),
            autoOpen: true,
            createParams: {
                layout: 'vertical'
            }
        });
    },
    
    // Create the child views
    create: function() {
        var _this = this;
        var textFont = {fontSize: 14};
        this.add(Ti.UI.createLabel({
            left: AD.UI.padding,
            top: AD.UI.padding,
            width: AD.UI.useableScreenWidth,
            height: Ti.UI.SIZE,
            font: textFont,
            text: $.formatString('feedbackText', AD.Defaults.application, AD.Defaults.version, AD.Platform.osName, Ti.Platform.version)
        }));
        this.add(Ti.UI.createLabel({
            left: AD.UI.padding,
            top: AD.UI.padding,
            width: AD.UI.useableScreenWidth,
            height: Ti.UI.SIZE,
            font: textFont,
            color: 'red',
            textid: 'feedbackWarning'
        }));
        
        // Create the suggestions and bug report buttons
        var buttonPadding = AD.UI.padding * 2;
        var buttonWidth = AD.UI.screenWidth / 2 - buttonPadding - AD.UI.padding;
        var $buttonView = this.add($.View.create(Ti.UI.createView({
            left: buttonPadding,
            top: AD.UI.padding * 2,
            right: buttonPadding,
            height: AD.UI.buttonHeight
        })));
        var suggestionsButton = $buttonView.add(Ti.UI.createButton({
            left: 0,
            top: 0,
            width: buttonWidth,
            height: AD.UI.buttonHeight,
            titleid: 'suggestion'
        }));
        suggestionsButton.addEventListener('click', function(event) {
            _this.feedback({
                titleId: 'suggestion',
                templateId: 'suggestionTemplate'
            });
        });
        var bugReportButton = $buttonView.add(Ti.UI.createButton({
            right: 0,
            top: 0,
            width: buttonWidth,
            height: AD.UI.buttonHeight,
            titleid: 'bugReport'
        }));
        bugReportButton.addEventListener('click', function(event) {
            _this.feedback({
                titleId: 'bugReport',
                templateId: 'bugReportTemplate',
                formatValues: [AD.Defaults.version, AD.Platform.osName, Ti.Platform.version]
            });
        });
    },
    
    // Display an email dialog to allow the user to give feedback
    feedback: function(templateData) {
        // Localize the title
        var title = AD.localize(templateData.titleId);
        var formatValues = [AD.Defaults.application];
        if (templateData.formatValues) {
            formatValues = formatValues.concat(templateData.formatValues);
        }
        var emailDialog = Ti.UI.createEmailDialog({
            toRecipients: [AD.Defaults.feedbackAddress],
            subject: $.formatString('feedbackSubject', AD.Defaults.application, title.toLowerCase()),
            messageBody: $.formatString.apply($, [templateData.templateId].concat(formatValues))
        });
        emailDialog.open();
    }
});
module.exports = $.Window('AppDev.UI.ChooseOptionWindow', {
    actions: [{
        title: 'add',
        callback: 'addOption',
        enabled: function() {
            return this.options.editable;
        },
        rightNavButton: true,
        showAsAction: true,
        icon: '/images/ic_action_new.png'
    }, {
        callback: function() {
            // Fill the array with the titles of all the selected rows
            var selected = [];
            this.getRows().forEach(function(row) {
                if (row.hasCheck) {
                    var option = row.option;
                    if (option && this.Model) {
                        // Load the true model from the model cache
                        // Because the option is a property of a Ti.UI.TableViewRow instance, it is
                        // passed through the Titanium proxy and no longer refers to the original model
                        option = this.Model.cache.getById(option.getId());
                    }
                    selected.push(option);
                }
            }, this);
            
            if (this.options.multiselect) {
                this.dfd.resolve(selected);
            }
            else if (selected.length === 0) {
                // Nothing is selected
                this.dfd.reject();
            }
            else {
                // Use the first (and only) selected option
                this.dfd.resolve(selected[0]);
            }
        },
        menuItem: false,
        onClose: true
    }],
    
    defaults: {
        multiselect: false,
        filter: {},
        initial: null
    },
    
    // Convert a multivalue object used to store contact information and convert it to an array of options useable by ChooseOptionWindow
    multivalueToOptionsArray: function(multivalue) {
        var options = [];
        $.each(multivalue, function(label, values) {
            values.forEach(function(value, index) {
                options.push({
                    title: label + (index === 0 ? '' : ' '+(index+1)) + ': ' + value,
                    value: value,
                    id: label+':'+index
                });
            });
        });
        return options;
    }
}, {
    init: function(options) {
        // If 'init' is called via this._super(...) in a derived class, make sure that the new options are added to this.options
        $.extend(true, this.options, options);
        
        this.Model = this.options.Model;
        if (typeof this.Model === 'string') {
            // "Model" can also be a string representing the name of the model class
            this.Model = AD.Models[this.Model];
        }
        if (this.Model && this.Model.cache && !this.options.options) {
            // Automatically generate the options from the model instance cache
            this.options.options = this.Model.cache.query(this.options.filter);
        }
        
        // Initialize the base $.Window object
        this._super({
            title: $.formatString('chooseOptionTitle', AD.localize(this.options.groupName+(this.options.multiselect ? 's' : '')).toLowerCase()),
            autoOpen: true
        });
    },
    
    // Create the options table view
    create: function() {
        // Create the options table
        var _this = this;
        var optionsTable = Ti.UI.createTableView();
        optionsTable.addEventListener('click', function(event) {
            // An option row was clicked
            var row = _this.select(event.row.id);
            if (!_this.options.multiselect) {
                _this.dfd.resolve(row.option);
            }
        });
        this.add('optionsTable', optionsTable);
        
        if (this.options.editable) {
            optionsTable.editable = true;
            optionsTable.addEventListener('delete', this.proxy(function(event) {
                // Re-index the rows to maintain the integrity of their indices
                this.getRows().forEach(function(row, index) {
                    row.index = index;
                });
                --this.rowCount;
                
                // Remove the option from the options array and notify the caller of the removal
                var deletedIndex = event.rowData.index;
                var deletedOption = this.options.options.splice(deletedIndex, 1)[0];
                if (this.Model) {
                    // The option is also a model instance, so destroy it
                    deletedOption.destroy();
                }
                this.onOptionsUpdate();
            }));
        }
    },
    
    // Initialize the child views
    initialize: function() {
        // Create rows for each of the options
        var tableData = this.options.options.map(this.createRow, this);
        this.getChild('optionsTable').data = tableData;
        
        if (this.options.multiselect) {
            // Select all initially selected options
            this.options.initial.forEach(this.select, this);
        }
        else {
            // Select the initially selected option
            this.select(this.options.initial);
        }
    },
    
    // Select the row with the given id
    select: function(id) {
        var rows = this.getRows();
        var row = null;
        // Find the row with the specified id
        $.each(rows, function(index, currentRow) {
            if (currentRow.id === id) {
                row = currentRow;
                return false; // stop iterating
            }
        });
        if (row) {
            if (!this.options.multiselect) {
                // Unselect the other rows
                rows.forEach(function(row) {
                    row.hasCheck = false;
                });
            }
            // Toggle the selection state of the row
            row.hasCheck = !row.hasCheck;
        }
        return row;
    },
    
    // Display the AddOptionWindow
    addOption: function() {
        var $winAddOption = this.createWindow('ChooseOptionWindow.AddOptionWindow', {
            groupName: this.options.groupName
        });
        var Model = this.Model;
        var addOptionDfd;
        if (Model) {
            addOptionDfd = $.Deferred();
            $winAddOption.getDeferred().done(function(optionLabel) {
                // Create a new model instance to represent this option
                var newOption = new Model();
                newOption.attr(Model.labelKey, optionLabel);
                newOption.save().then(function() {
                    addOptionDfd.resolve(newOption);
                }, addOptionDfd.reject);
            });
        }
        else {
            addOptionDfd = $winAddOption.getDeferred();
        }
        addOptionDfd.done(this.proxy(function(newOption) {
            // Add the new option row to the table and select it
            var newRow = this.createRow(newOption);
            this.getChild('optionsTable').appendRow(newRow);
            this.select(newRow.id);
            
            // This is unnecessary when options are model instances because the model cache is maintained
            if (!this.Model) {
                // Add the option to the options array and notify the caller of the addition
                this.options.options.push(newOption);
                this.onOptionsUpdate();
            }
        }));
    },
    
    // Alert the caller to the modification of the options list
    onOptionsUpdate: function() {
        if ($.isFunction(this.options.onOptionsUpdate)) {
            this.options.onOptionsUpdate(this.options.options);
        }
    },
    
    rowCount: 0,
    // Return a row data structure representing the option
    createRow: function(option) {
        if (typeof option === 'string') {
            // Convert simple string options to full option objects
            option = {
                title: option,
                value: option,
                id: option
            };
        }
        var row = {
            option: option,
            height: AD.UI.tableViewRowHeight,
            hasCheck: false
        };
        if (this.Model) {
            row.title = option.attr(this.Model.labelKey);
            row.id = option.getId();
        }
        else {
            row.title = option.title;
            row.id = option.id || this.rowCount;
        }
        row.index = option.index = this.rowCount++;
        return Ti.UI.createTableViewRow(row);
    },
    
    // Return an array of the rows in the table
    getRows: function() {
        var section = this.getChild('optionsTable').data[0];
        return section ? (section.rows || []) : [];
    }
});
module.exports = $.Window('AppDev.UI.AppGroupsWindow', {
    dependencies: ['AddGroupWindow'],
    actions: [{
        title: 'add',
        callback: function() {
            // Create a new group
            this.createWindow('AddGroupWindow');
        },
        rightNavButton: true,
        showAsAction: true,
        icon: '/images/ic_action_new.png'
    }]
}, {
    init: function(options) {
        // Initialize the base $.Window object
        this._super({
            title: 'groupsTitle'
        });
        
        this.smartBind(AD.Models.Group, 'created', function(event, group) {
            // Simulate a selection to open the newly-created group
            var $groupsTable = this.get$Child('groupsTable');
            $groupsTable.onSelect(group);
        });
    },
    
    // Create the child views
    create: function() {
        var $groupsTable = new GroupTable({
            $window: this
        });
        this.add('groupsTable', $groupsTable);
    }
});
module.exports = $.Window('AppDev.UI.SortOrderWindow', {
    actions: [{
        callback: function() {
            if (this.modified) {
                // Rebuild the order array
                var rows = this.getChild('fieldsTable').data[0].rows;
                var order = rows.map(function(row) {
                    return row.field;
                });
                this.dfd.resolve(order);
            }
            else {
                // The sort order was not modified, so do nothing
                this.dfd.reject({});
            }
        },
        menuItem: false,
        onClose: true
    }]
}, {
    init: function(options) {
        this.fields = $.indexArray(this.options.fields, 'field');
        this.order = this.options.order;
        this.modified = false;
        
        // Initialize the base $.Window object
        this._super({
            title: 'sortOrderTitle',
            autoOpen: true
        });
    },
    
    // Create the options table view
    create: function() {
        var tableData = this.order.map(function(fieldName) {
            var fieldTitle = this.fields[fieldName].label;
            return {
                title: AD.localize(fieldTitle),
                field: fieldName
            };
        }, this);
        
        var _this = this;
        if (!AD.Platform.isiOS) {
            var swapRows = function(index1, index2) {
                var rows = fieldsTable.data[0].rows;
                var row1 = rows[index1];
                var row2 = rows[index2];
                if (!row1 || !row2) {
                    // Not a valid row (probably out of bounds), so abort
                    return;
                }
        
                // Swap the location of the rows in the array
                rows[index1] = row2;
                rows[index2] = row1;
        
                // Swap the rows' indexes
                row1.index = index2;
                row2.index = index1;
                rows.forEach(function(row, index) {
                    row.index = index;
                });
        
                // Update the table with the new rows
                fieldsTable.setData(rows);
                
                // The sort order has been modified
                _this.modified = true;
            };
            
            tableData = tableData.map(function(data, index) {
                var row = Ti.UI.createTableViewRow({
                    height: AD.UI.tableViewRowHeight,
                    field: data.field,
                    index: index
                });
                row.add(Ti.UI.createLabel({
                    left: AD.UI.padding,
                    top: 0,
                    width: Ti.UI.SIZE,
                    height: Ti.UI.SIZE,
                    text: data.title,
                    font: AD.UI.Fonts.header
                }));
                var moveContainer = Ti.UI.createView({
                    right: 0,
                    top: AD.UI.padding,
                    bottom: AD.UI.padding,
                    width: Ti.UI.SIZE,
                    height: Ti.UI.SIZE,
                    layout: 'horizontal'
                });
                row.add(moveContainer);
                
                var moveDown = Ti.UI.createImageView({
                    right: AD.UI.padding * 3,
                    image: '/images/arrow-down.png'
                });
                moveDown.addEventListener('click', function(event) {
                    var index = row.index;
                    swapRows(index, index + 1);
                });
                moveContainer.add(moveDown);
                
                var moveUp = Ti.UI.createImageView({
                    right: AD.UI.padding * 3,
                    image: '/images/arrow-up.png'
                });
                moveUp.addEventListener('click', function(event) {
                    var index = row.index;
                    swapRows(index, index - 1);
                });
                moveContainer.add(moveUp);
                
                return row;
            });
        }
        
        var fieldsTable = Ti.UI.createTableView({
            data: tableData
        });
        this.add('fieldsTable', fieldsTable);
        
        if (AD.Platform.isiOS) {
            // iOS supports moveable table rows natively
            fieldsTable.moving = true;
            fieldsTable.addEventListener('move', function() {
                // The sort order has been modified
                _this.modified = true;
            });
        }
    }
});
module.exports = $.Window('AppDev.UI.AddGroupWindow', {
    setup: function() {
    },
    dependencies: ['ChooseOptionWindow', 'Checkbox'],
    
    fieldDefinitions: {},
    fieldViewHeight: AD.UI.buttonHeight + AD.UI.padding,
    
    actions: [{
        title: 'save',
        callback: 'save',
        rightNavButton: true,
        onClose: true,
        showAsAction: true,
        icon: '/images/ic_action_save.png'
    }, {
        title: 'cancel',
        callback: 'cancel', // special pre-defined callback to reject the deferred
        leftNavButton: true
    }]
}, {
    init: function(options) {
        var fieldDefinitions = this.constructor.fieldDefinitions;
        var defineField = function(fieldName, fieldData) {
            // Add a boolean property to quickly check the type of a field
            // For example, fieldData.isChoice === true
            fieldData.fieldName = fieldName;
            fieldData['is'+$.capitalize(fieldData.type)] = true;
            fieldDefinitions[fieldName] = fieldData;
        };
        // Define each of the supported group fields
        defineField('tags', {
            name: 'tags',
            type: 'multichoice',
            Model: 'Tag',
            params: {
                groupName: 'tag',
                editable: true
            }
        });
        defineField('year_id', {
            name: 'year',
            type: 'choice',
            Model: 'Year'
        });
        defineField('campus_uuid', {
            name: 'campus',
            type: 'choice',
            Model: 'Campus',
            params: {
                editable: true
            },
            onUpdate: function() {
                this.updateSteps();
            }
        });
        
        AD.Models.Step.cache.getArray().forEach(function(step) {
            defineField('steps '+step.getId(), {
                name: step.getLabel(),
                step: step,
                type: 'bool'
            });
        });
        
        // If existingGroup is a 'truthy' value, we are editing, otherwise we are adding
        this.adding = this.options.existingGroup ? false : true;
        
        // Create a new local group model if necessary
        this.group = this.adding ? new AD.Models.Group({
            group_name: '',
            group_filter: {}
        }) : this.options.existingGroup;
        
        // This object holds the values of all the group fields
        this.fields = {};
        
        // Initialize the base $.Window object
        this._super({
            title: this.adding ? 'addGroup' : 'editGroup',
            autoOpen: true,
            focusedChild: this.adding ? 'name' : null,
            createParams: {
                layout: 'vertical'
            }
        });
    },
    
    // Create each of the form fields
    create: function() {
        // Create the name field and label
        this.add(Ti.UI.createLabel({
            left: AD.UI.padding,
            top: AD.UI.padding,
            width: Ti.UI.SIZE,
            height: Ti.UI.SIZE,
            textid: 'groupName'
        }));
        var name = this.add('name', Ti.UI.createTextField({
            left: AD.UI.padding,
            top: AD.UI.padding,
            width: AD.UI.useableScreenWidth,
            height: AD.UI.textFieldHeight,
            borderStyle: Ti.UI.INPUT_BORDERSTYLE_ROUNDED,
            value: ''
        }));
        
        // Create the scrollable group fields container
        var $fieldsView = this.add('fieldsView', $.View.create(Ti.UI.createScrollView({
            left: 0,
            top: AD.UI.padding,
            width: AD.UI.screenWidth,
            height: Ti.UI.FILL,
            layout: 'vertical',
            scrollType: 'vertical',
            contentHeight: 'auto',
            showVerticalScrollIndicator: true
        })));
        $fieldsView.getView().addEventListener('click', function() {
            // Hide the name keyboard
            name.blur();
        });
        
        // Create a field row for each group field
        var filter = this.group.attr('group_filter');
        $.each(this.constructor.fieldDefinitions, this.proxy(function(name, definition) {
            var value = findProperty(filter, name);
            var enabled = typeof value !== 'undefined';
            var title = value || AD.Localize('unspecified');
            if (value && definition.isChoice) {
                // "value" refers to the primary key of the model, so lookup the associated model instance
                var model = AD.Models[definition.Model].cache.getById(value);
                if (model) {
                    title = model.getLabel();
                }
                else {
                    // This model instance does not exist anymore
                    enabled = false;
                }
            }
            else if (definition.isMultichoice) {
                title = AD.Localize('unspecified');
            }
            var field = this.fields[name] = {
                enabled: enabled,
                value: enabled && value,
                title: title
            };
            this.createRow(field, definition);
        }));
    },
    
    createRow: function(field, fieldDefinition) {
        var _this = this;
        var onUpdate = function() {
            if ($.isFunction(fieldDefinition.onUpdate)) {
                fieldDefinition.onUpdate.call(_this, field.value);
            }
        };
        
        // Create the field row container
        var $fieldRow = $.View.create(Ti.UI.createView({
            left: 0,
            top: 0,
            width: AD.UI.screenWidth,
            height: this.constructor.fieldViewHeight,
        }));
        
        // Create the checkbox to toggle whether this field is included in the group
        var $enabledCheckbox = $fieldRow.add('enabled', new AD.UI.Checkbox({
            createParams: {
                left: AD.UI.padding,
                top: AD.UI.padding / 2
            },
            value: field.enabled
        }));
        $enabledCheckbox.addEventListener('change', function(event) {
            // Enable/disable the row based on the value of the checkbox
            var enabled = field.enabled = event.value;
            $fieldRow.get$Child('value').setEnabled(enabled);
        });
        
        // Create the field name label
        var nameLabel = $fieldRow.add(Ti.UI.createLabel({
            left: AD.UI.Checkbox.defaultSize + AD.UI.padding * 2,
            top: 0,
            height: Ti.UI.FILL,
            text: AD.Localize(fieldDefinition.name),
            font: AD.UI.Fonts.medium
        }));
        
        var $valueView = null;
        
        if (fieldDefinition.isBool) {
            // Create the checkbox to toggle the value of this field
            var $valueCheckbox = $valueView = new AD.UI.Checkbox({
                createParams: {
                    right: AD.UI.padding,
                    top: AD.UI.padding / 2
                },
                enabled: field.enabled,
                value: field.value
            });
            $valueCheckbox.addEventListener('change', function(event) {
                // Set the value of this field
                field.value = event.value;
                onUpdate();
            });
            $enabledCheckbox.addEventListener('change', function(event) {
                var enabled = event.value;
                if (!enabled) {
                    // Uncheck the value checkbox
                    $valueCheckbox.setValue(false);
                }
            });
        }
        else if (fieldDefinition.isChoice || fieldDefinition.isMultichoice) {
            var valueButtonWidth = 120;

            var $conditionCheckbox = null;
            if (fieldDefinition.isMultichoice) {
                var conditions = AD.Models.Contact.filterConditions;
                // Create the condition (any/all) checkbox
                $conditionCheckbox = new AD.UI.Checkbox({
                    createParams: {
                        right: AD.UI.padding * 2 + valueButtonWidth,
                        top: AD.UI.padding / 2
                    },
                    overlayText: AD.Localize('all').toUpperCase(),
                    enabled: field.enabled,
                    value: field.value.condition === conditions[1]
                });
                $conditionCheckbox.addEventListener('change', function(event) {
                    field.value.condition = conditions[event.value ? 1 : 0];
                    onUpdate();
                });
                $fieldRow.add('condition', $conditionCheckbox);
            }
            
            var _this = this;
            
            var valueButton = Ti.UI.createButton({
                right: AD.UI.padding,
                top: AD.UI.padding / 2,
                width: valueButtonWidth,
                height: AD.UI.buttonHeight,
                title: field.enabled ? field.title : ''
            });
            $valueView = $.View.create(valueButton);
            valueButton.addEventListener('click', function() {
                // Assume that all choices are model instances
                var params = $.extend({
                    groupName: fieldDefinition.name,
                    Model: fieldDefinition.Model,
                }, fieldDefinition.params);
                if (fieldDefinition.isChoice) {
                    // This is a single choice field
                    params.initial = field.value;
                    var $winChooseOption = _this.createWindow('ChooseOptionWindow', params);
                    $winChooseOption.getDeferred().done(function(option) {
                        // An option was chosen, so set the value of the field in the filter
                        field.value = option ? option.getId() : null;
                        valueButton.title = option ? option.getLabel() : AD.Localize('unspecified');
                        onUpdate();
                    });
                }
                else {
                    // This is a multi choice field
                    params.initial = field.value.ids || [];
                    var $winChooseOptions = _this.createWindow('ChooseOptionsWindow', params);
                    $winChooseOptions.getDeferred().done(function(options) {
                        // An option was chosen, so set the value of the field in the filter
                        field.value.ids = $.Model.getIds(options);
                        onUpdate();
                    });
                }
            });
            $enabledCheckbox.addEventListener('change', function(event) {
                var enabled = event.value;
                field.value = enabled && fieldDefinition.isMultichoice ? {
                    ids: [],
                    condition: 'OR'
                } : null;
                onUpdate();
                
                // Reset the button's text
                valueButton.title = enabled ? AD.Localize('unspecified') : '';

                if ($conditionCheckbox) {
                    $conditionCheckbox.setEnabled(enabled);
                    if (!enabled) {
                        $conditionCheckbox.setValue(false);
                    }
                }
            });
        }
        
        nameLabel.right = $valueView.getView().width + AD.UI.padding * 2;

        $valueView.setEnabled(field.enabled);
        $fieldRow.fieldDefinition = fieldDefinition;
        $fieldRow.add('value', $valueView);

        // The row has the same name as the fieldname of the column in the database
        this.get$Child('fieldsView').add(fieldDefinition.fieldName, $fieldRow);
    },

    // Set the initial contents of the form fields
    initialize: function() {
        this.getChild('name').value = this.group.attr('group_name');
        this.updateSteps();
    },
    
    // Show the steps fields that are associated with the selected campus and hide the others
    updateSteps: function() {
        var _this = this;
        var group_campus_uuid = this.fields.campus_uuid.value;
        $.each(this.get$Child('fieldsView').children, function(fieldName, fieldView) {
            // If this field is a step, its fieldDefinition will have a
            // step property referring to the field's associated step model
            var step = fieldView.get$View().fieldDefinition.step;
            if (step) {
                var step_campus_uuid = step.attr('campus_uuid');
                var isVisible = step_campus_uuid === null || step_campus_uuid === group_campus_uuid;
                fieldView.visible = isVisible;
                fieldView.height = isVisible ? _this.constructor.fieldViewHeight : 0;
            }
        });
    },
    
    // Save the current group
    save: function() {
        if (!this.getChild('name').value) {
            alert(AD.Localize('invalidGroupName'));
            return false;
        }
        
        // Build the filter object that will be stringified and inserted into the database
        var valid = true;
        var fieldDefinitions = this.constructor.fieldDefinitions;
        var filter = {};
        $.each(this.fields, function(fieldName, fieldData) {
            var fieldValue = fieldData.value;
            var fieldDefinition = fieldDefinitions[fieldName];
            if (fieldData.enabled) {
                var errorTemplate = null;
                if (fieldDefinition.isChoice && fieldValue === null) {
                    errorTemplate = 'invalidOptionChoice';
                }
                else if (fieldDefinition.isMultichoice && fieldValue.ids.length === 0) {
                    errorTemplate = 'invalidOptionMultichoice';
                }
                else if (fieldDefinition.isMultichoice && fieldValue.condition === 'AND' && fieldValue.ids.length <= 1) {
                    errorTemplate = 'invalidOptionMultichoiceAnd';
                }
                
                if (errorTemplate) {
                    // This field is not valid, so display the error message
                    alert($.formatString(errorTemplate, fieldDefinition.name.toLowerCase()));
                    valid = false;
                    return false; // stop looping
                }
            }
            if (fieldData.enabled) {
                findProperty(filter, fieldName, fieldData.value);
            }
        });
        
        if (valid) {
            // Update the group model
            this.group.attrs({
                group_name: this.getChild('name').value,
                group_filter: filter
            });
            this.group.save();
            this.dfd.resolve(this.group);
        }
        return valid;
    }
});
module.exports = $.Window('AppDev.UI.ImportContactsWindow', {
    dependencies: ['ChooseOptionWindow', 'ChooseContactsWindow', 'AddContactWindow'],
    
    fields: [{
        name: 'campus'
    }, {
        name: 'year'
    }, {
        name: 'tags',
        icon: '/images/tags.png'
    }],
    actions: [{
        title: 'cancel',
        callback: 'cancel',
        leftNavButton: true,
        onClose: true
    }]
}, {
    init: function(options) {
        this.contacts = [];
        
        this.campus_uuid = null;
        this.year_id = 1;
        this.tags = []; // an array of Tag model instances
        
        // Initialize the base $.Window object
        this._super({
            title: 'importContactsTitle',
            autoOpen: true,
            createParams: {
                layout: 'vertical'
            }
        });
    },
    
    // Create each of the form fields
    create: function() {
        // Explanatory text label
        this.add(Ti.UI.createLabel({
            left: AD.UI.padding,
            top: AD.UI.padding,
            width: Ti.UI.SIZE,
            height: Ti.UI.SIZE,
            textid: 'importHelp',
            font: AD.UI.Fonts.mediumSmall
        }));
        
        // Create the contacts label and choose contacts button
        var $contactsView = this.add($.View.create(Ti.UI.createView({
            top: AD.UI.padding,
            width: AD.UI.screenWidth,
            height: AD.UI.buttonHeight
        })));
        this.record('contactsLabel', $contactsView.add(Ti.UI.createLabel({
            left: AD.UI.padding,
            top: 0,
            width: Ti.UI.SIZE,
            height: Ti.UI.FILL
        })));
        var chooseButton = $contactsView.add(Ti.UI.createButton({
            right: AD.UI.padding,
            top: 0,
            width: 120,
            height: AD.UI.buttonHeight,
            titleid: 'unspecified'
        }));
        chooseButton.addEventListener('click', this.proxy('chooseContacts'));
        
        var _this = this;

        // Create the fields container
        var fieldsView = this.add(Ti.UI.createView({
            left: AD.UI.padding,
            top: AD.UI.padding,
            width: Ti.UI.SIZE,
            height: Ti.UI.SIZE,
            layout: 'vertical'
        }));
        // Create the campus, year, and tag fields
        var labelWidth = 80;
        var fieldHeight = AD.UI.buttonHeight;
        this.constructor.fields.forEach(function(field, index) {
            var fieldView = Ti.UI.createView({
                left: 0,
                top: AD.UI.padding,
                width: Ti.UI.SIZE,
                height: fieldHeight
            });

            var changeCallback = function() {
                // Calculate the names of the change and update field functions
                // changeFieldFuncName === 'changeYear' and updateFieldFuncName === 'updateYear', for example
                var changeFieldFuncName = 'change'+$.capitalize(field.name);
                var updateFieldFuncName = 'update'+$.capitalize(field.name);
                _this[changeFieldFuncName]().done(function() {
                    // After the field is changed, update its associated UI
                    _this[updateFieldFuncName]();
                });
            };
            var valueField = null;
            if (field.icon) {
                // Create an icon to identify the field
                fieldView.add(Ti.UI.createImageView({
                    left: AD.UI.padding,
                    width: Ti.UI.SIZE,
                    height: Ti.UI.SIZE,
                    image: field.icon
                }));
                
                // Create a label that can be clicked to change the field value
                valueField = Ti.UI.createLabel({
                    left: AD.UI.padding * 2 + 25,
                    right: AD.UI.padding,
                    top: 0,
                    height: Ti.UI.FILL,
                    font: AD.UI.Fonts.mediumSmall
                });
                fieldView.addEventListener('click', changeCallback);
            }
            else {
                // Create a static label to identify the field
                fieldView.add(Ti.UI.createLabel({
                    left: 0,
                    width: labelWidth,
                    height: Ti.UI.SIZE,
                    textid: field.name
                }));
                
                // Create a button that can be clicked to change the field value
                var chooseButton = valueField = Ti.UI.createButton({
                    left: labelWidth + AD.UI.padding,
                    top: 0,
                    width: 120,
                    height: AD.UI.buttonHeight
                });
                chooseButton.addEventListener('click', changeCallback);
            }
            fieldView.add(this.record(field.name, valueField));

            fieldsView.add(fieldView);
        }, this);
        
        // Create the import button
        var importButton = this.add('importButton', Ti.UI.createButton({
            top: AD.UI.padding * 2,
            center: { x: AD.UI.screenWidth / 2 }, // horizontally centered
            width: 120,
            height: AD.UI.buttonHeight,
            titleid: 'importTitle'
        }));
        importButton.addEventListener('click', function() {
            _this.validate().done(function() {
                _this.import();
            });
        });
        
        // Create the import progress bar, which is initially hidden
        this.add('importProgress', Ti.UI.createProgressBar({
            top: -AD.UI.buttonHeight, // display on top of the import button
            center: { x: AD.UI.screenWidth / 2 }, // horizontally centered
            width: AD.UI.screenWidth * 0.75,
            height: 40,
            font: { fontSize: 14, fontWeight: 'bold' },
            message: '',
            visible: false
        }));
    },
    
    // Set the initial contents of the form fields
    initialize: function() {
        // Initialize the fields by calling updateCampus, updateYear, etc.
        this.constructor.fields.forEach(function(field) {
            this['update'+$.capitalize(field.name)]();
        }, this);
        
        this.updateContactsView();
    },
    
    // Choose which contacts to import
    chooseContacts: function() {
        var _this = this;
        this.createWindow('ChooseContactsWindow', {
            contacts: this.contacts
        }).getDeferred().done(function(contacts) {
            _this.contacts = contacts;
            _this.updateContactsView();
        });
    },
    
    // Handlers for setting the campus, year, and tags applied to the imported contacts
    changeCampus: function() {
        var _this = this;
        // Allow the user to choose the contacts' campus
        return this.createWindow('ChooseOptionWindow', {
            groupName: 'campus',
            Model: 'Campus',
            initial: this.campus_uuid,
            editable: true
        }).getDeferred().done(function(campus) {
            // A campus was chosen
            _this.campus_uuid = campus ? campus.getId() : null;
        });
    },
    changeYear: function() {
        var _this = this;
        // Allow the user to choose the contacts' year
        return this.createWindow('ChooseOptionWindow', {
            groupName: 'year',
            Model: 'Year',
            initial: this.year_id
        }).getDeferred().done(function(year) {
            // A year was chosen
            _this.year_id = year.getId();
        });
    },
    changeTags: function() {
        var _this = this;
        // Allow the user to choose the contacts' associated tags
        return this.createWindow('ChooseOptionsWindow', {
            groupName: 'tag',
            Model: 'Tag',
            initial: $.Model.getIds(this.tags),
            editable: true
        }).getDeferred().done(function(tags) {
            _this.tags = tags;
        });
    },
    
    // Update the scrollable view that contains the names of the contacts
    updateContactsView: function() {
        this.getChild('contactsLabel').text = $.formatString('importingContacts', this.contacts.length);
    },

    // Update the field labels
    updateCampus: function() {
        this.getChild('campus').title = this.campus_uuid ? AD.Models.Campus.cache.getById(this.campus_uuid).getLabel() : AD.Localize('unspecified');
    },
    updateYear: function() {
        this.getChild('year').title = AD.Models.Year.cache.getById(this.year_id).getLabel();
    },
    updateTags: function() {
        this.getChild('tags').text = $.Model.getLabels(this.tags).join(', ') || AD.Localize('none');
    },
    
    // Validate the contacts
    validate: function() {
        var warnDfd = $.Deferred();
        var missingFields = [];
        if (!this.campus_uuid) {
            missingFields.push('campus');
        }
        if (this.year_id === 1) {
            missingFields.push('year');
        }
        if (this.contacts.length === 0) {
            alert(AD.Localize('importNoContacts'));
            warnDfd.reject();
        }
        else if (missingFields.length > 0) {
            var warning = $.formatString('importWarning', missingFields.join(' '+AD.Localize('or')+' '));
            AD.UI.yesNoAlert(warning).then(warnDfd.resolve, warnDfd.reject);
        }
        else {
            warnDfd.resolve();
        }
        return warnDfd.promise();
    },
    
    // Import the contacts
    'import': function() {
        // Hide the import button
        this.getChild('importButton').visible = false;
        
        // Initialize and show the progress bar
        var importProgress = this.getChild('importProgress');
        importProgress.visible = true;
        importProgress.value = 0;
        importProgress.min = 0;
        importProgress.max = this.contacts.length - 1;
        
        // Import each contact
        this.contacts.forEach(function(contact, index) {
            // Update the progress bar
            importProgress.value = index;
            importProgress.message = $.formatString('importStatus', index + 1, this.contacts.length);
            
            var contactModel = AD.UI.AddContactWindow.createContact({
                contact_recordId: contact.recordId,
                campus_uuid: this.campus_uuid,
                year_id: this.year_id,
            });
            var tags = this.tags;
            contactModel.save().done(function() {
                // Set the tags AFTER saving the contact so that contact_uuid will be available
                contactModel.setTags(tags);
            });
        }, this);
        this.dfd.resolve();
    }
});
module.exports = $.Window('AppDev.UI.LoginWindow', {}, {
    init: function(options) {
        // Initialize the base $.Window object
        this._super({
            title: 'login',
            modal: true,
            focusedChild: 'username'
        });
    },
    
    // Create the child views
    create: function() {
        // Create the user ID label and text field
        this.add(Ti.UI.createLabel({
            left: AD.UI.padding,
            top: AD.UI.padding,
            width: Ti.UI.SIZE,
            height: Ti.UI.SIZE,
            textid: 'userId'
        }));
        this.add('username', Ti.UI.createTextField({
            left: 110,
            top: AD.UI.padding,
            width: 180,
            height: AD.UI.textFieldHeight,
            hintText: AD.localize('userId'),
            autocorrect: false,
            autocapitalization: Ti.UI.TEXT_AUTOCAPITALIZATION_NONE,
            borderStyle: Ti.UI.INPUT_BORDERSTYLE_ROUNDED
        }));
        
        // Create the password label and text field
        this.add(Ti.UI.createLabel({
            left: AD.UI.padding,
            top: 70,
            width: Ti.UI.SIZE,
            height: Ti.UI.SIZE,
            textid: 'password'
        }));
        var passwordField = this.add('password', Ti.UI.createTextField({
            left: 110,
            top: 70,
            width: 180,
            height: AD.UI.textFieldHeight,
            passwordMask: true,
            hintText: AD.localize('password'),
            borderStyle: Ti.UI.INPUT_BORDERSTYLE_ROUNDED
        }));
        passwordField.addEventListener('return', this.proxy('submit'));
        
        // Create the cancel and submit buttons
        var buttonWidth = AD.UI.useableScreenWidth * 0.4;
        var cancel = this.add(Ti.UI.createButton({
            left: AD.UI.padding,
            top: 120,
            width: buttonWidth,
            height: AD.UI.buttonHeight,
            titleid: 'cancel'
        }));
        cancel.addEventListener('click', this.dfd.reject);
        var submit = this.add('submit', Ti.UI.createButton({
            right: AD.UI.padding,
            top: 120,
            width: buttonWidth,
            height: AD.UI.buttonHeight,
            titleid: 'submit'
        }));
        submit.addEventListener('click', this.proxy('submit'));
    },
    
    // Called when the user submits their login credentials
    submit: function() {
        // Disable the submit button until the credentials have been validated to prevent the user
        // from clicking it multiple times when the validation procedure is not instantaneous
        var submitButton = this.getChild('submit');
        submitButton.enabled = false;
        
        var _this = this;
        var username = this.getChild('username').value;
        var password = this.getChild('password').value;
        var validateDfd = this.options.validateCredentials(username, password);
        validateDfd.done(function(valid) {
            submitButton.enabled = true;
            
            if (valid) {
                _this.dfd.resolve({
                    username: username,
                    password: password
                });
            }
            else {
                alert('Invalid credentials');
            }
        });
    }
});
module.exports = $.Window('AppDev.UI.GoogleAuthWindow', {}, {
    init: function(options) {
        // Initialize the base $.Window object
        this._super({
            title: 'login',
            autoOpen: true
        });
    },
    
    // Create the child views
    create: function() {
        var _this = this;
        
        // Create the WebView that will authenticate the user with Google
        var webview = this.add('webview', Ti.UI.createWebView({
            url: AD.Comm.HTTP.makeURL('https://accounts.google.com/o/oauth2/auth', {
                response_type: 'code',
                client_id: GoogleAPIs.client_id,
                redirect_uri: GoogleAPIs.redirect_uri,
                scope: this.options.scope
            })
        }));
        webview.addEventListener('load', function() {
            var approvalPageURL = 'https://accounts.google.com/o/oauth2/approval';
            if (webview.evalJS('location.origin+location.pathname') === approvalPageURL) {
                // This is the approval page, so read the authorization code
                var code = webview.evalJS('document.getElementById("code").value');
                console.log('code: '+code);
                _this.getDeferred().resolve(code);
            }
        });
    }
});
Example #9
0
var LoginWindow = module.exports = $.Window('AppDev.UI.LoginWindow', {}, {
    init: function(options) {
        // Initialize the base $.Window object
        this._super({
            title: 'login',
            modal: true,
            focusedChild: 'user'
        });
    },

    // Create the child views
    create: function() {
        var _this = this;
        
        // Create the user ID label and text field
        this.add(Ti.UI.createLabel({
            left: AD.UI.padding,
            top: AD.UI.padding,
            width: Ti.UI.SIZE,
            height: Ti.UI.SIZE,
            textid: 'userId'
        }));
        this.add('user', Ti.UI.createTextField({
            left: 110,
            top: AD.UI.padding,
            width: 180,
            height: AD.UI.textFieldHeight,
            hintText: AD.Localize('userId'),
            autocorrect: false,
            autocapitalization: Ti.UI.TEXT_AUTOCAPITALIZATION_NONE,
            borderStyle: Ti.UI.INPUT_BORDERSTYLE_ROUNDED
        }));
        
        // Create the password label and text field
        this.add(Ti.UI.createLabel({
            left: AD.UI.padding,
            top: 70,
            width: Ti.UI.SIZE,
            height: Ti.UI.SIZE,
            textid: 'password'
        }));
        var passwordField = this.add('password', Ti.UI.createTextField({
            left: 110,
            top: 70,
            width: 180,
            height: AD.UI.textFieldHeight,
            passwordMask: true,
            hintText: AD.Localize('password'),
            borderStyle: Ti.UI.INPUT_BORDERSTYLE_ROUNDED
        }));
        passwordField.addEventListener('return', this.proxy('onSubmit'));
        
        // Create the submit and cancel buttons
        var buttonWidth = AD.UI.useableScreenWidth * 0.4;
        var submit = this.add(Ti.UI.createButton({
            left: AD.UI.padding,
            top: 120,
            width: buttonWidth,
            height: AD.UI.buttonHeight,
            titleid: 'submit'
        }));
        submit.addEventListener('click', this.proxy('onSubmit'));
        var cancel = this.add(Ti.UI.createButton({
            right: AD.UI.padding,
            top: 120,
            width: buttonWidth,
            height: AD.UI.buttonHeight,
            titleid: 'cancel'
        }));
        cancel.addEventListener('click', this.proxy('close'));
    },
    
    // Called when the user submits their login credentials
    onSubmit: function() {
        // Gather the login data
        var loginData = {
            userID: this.getChild('user').value,
            pWord: Ti.Utils.md5HexDigest(this.getChild('password').value) // MD5 hash the user's password
        };
        
        // Send login request to the server
        Ti.API.log('Attempting to login as {'+loginData.userID+', '+loginData.pWord+'}');
        AD.ServiceJSON.post({
            params: loginData,
            url: '/service/site/login/authenticate',
            success: this.proxy(function(data) {
                Ti.API.log('Login succeeded!');
                
                this.close();
                
                // Call the onLogin callback if it was provided to the "open" call
                if ($.isFunction(this.onLogin)) {
                    this.onLogin();
                }
            }),
            failure: function(data) {
                Ti.API.log('Login failed!');
            }
        });
    },
    
    // Override the default window open function
    open: function(onLogin) {
        this.onLogin = onLogin;
        
        // Clear out the input fields
        this.getChild('user').value = '';
        this.getChild('password').value = '';
        return this._super.apply(this, arguments);
    }
});
Example #10
0
module.exports = $.Window('AppDev.UI.ChooseContactWindow', {
    actions: [{
        title: 'cancel',
        callback: 'cancel', // special pre-defined callback to reject the deferred
        rightNavButton: true,
        backButton: true
    }]
}, {
    init: function(options) {
        // Build an array of all the record id's of all contacts already in the database
        var usedRecordIds = [];
        AD.Models.Contact.cache.getArray().forEach(function(contact) {
            usedRecordIds.push(contact.contact_recordId);
        });
        
        // Load all the contacts from the user's database
        var contactsData = this.contactsData = [];
        Ti.Contacts.getAllPeople().forEach(function(contact) {
            var contactRecordId = contact.recordId || contact.id;
            if (!options.filterExisting || usedRecordIds.indexOf(contactRecordId) === -1) {
                // This contact is not already in the database 
                contactsData.push({
                    title: contact.getFullName(),
                    contact: contact
                });
            }
        });
        
        // Initialize the base $.Window object
        this._super({
            title: 'chooseContactTitle',
            autoOpen: true,
            modal: true
        });
    },
    
    // Create contact table view
    create: function() {
        var _this = this;
        
        // Create the contacts table
        var contactTable = this.add('contactTable', Ti.UI.createTableView({
            data: this.contactsData
        }));
        contactTable.addEventListener('click', function(event) {
            _this.dfd.resolve(event.rowData.contact);
        });
    }
});
Example #11
0
module.exports = $.Window('AppDev.UI.AddContactWindow', {
    setup: function() {
        // When this class is created, initialize the static fields object
        this.fields.forEach(function(field) {
            // The database field name defaults to contact_ prepended to the field's name
            field.field = field.field || 'contact_'+field.name;
            
            field.callback = field.callback || 'change'+$.capitalize(field.name);
            field.labelId = field.label || field.name+'Label';
        });
    },
    dependencies: ['ChooseOptionWindow'],
    
    // Return the first value in the multivalue dictionary with a name in priorities
    getDefaultFromMultivalue: function(multivalue, priorities) {
        var highestPriority = { value: null, id: null };
        priorities.forEach(function(fieldName) {
            var values = multivalue[fieldName];
            if (values && values.length > 0) {
                // Use the first value
                highestPriority = {
                    value: values[0],
                    id: fieldName+':0'
                };
            }
        });
        return highestPriority;
    },
    
    // Return a new contact model instance
    createContact: function(attrs) {
        var localContact = attrs.contact_recordId === null ? null : Ti.Contacts.getPersonByID(attrs.contact_recordId);
        var firstName = '', lastName = '', nickname = '', defaultPhone = {value: null, id: null}, defaultEmail = {value: null, id: null}, note = '';
        if (localContact) {
            firstName = localContact.firstName || '';
            lastName = localContact.lastName || '';
            nickname = localContact.nickname || '';
            if (AD.Platform.isAndroid) {
                // Android does not allow access to the firstName, lastName, or nickname properties, so attempt to guess them
                var nameParts = localContact.fullName.split(' ');
                firstName = firstName || nameParts[0];
                lastName = lastName || nameParts[nameParts.length - 1];
                nickname = nickname || firstName;
            }
            defaultPhone = this.getDefaultFromMultivalue(localContact.getPhone(), ['iPhone', 'mobile', 'home']);
            defaultEmail = this.getDefaultFromMultivalue(localContact.getEmail(), ['home', 'work']);
            note = localContact.note || '';
        }
        var defaultYear = 1;
        
        // Populate the contact model fields with the new contact's information
        var baseAttrs = {
            viewer_id: AD.Viewer.viewer_id,
            contact_firstName: firstName,
            contact_lastName: lastName,
            contact_nickname: nickname,
            contact_campus: '',
            year_id: defaultYear,
            contact_phone: defaultPhone.value,
            contact_phoneId: defaultPhone.id,
            contact_email: defaultEmail.value,
            contact_emailId: defaultEmail.id,
            contact_notes: note
        };
        $.each(AD.Models.Contact.steps, function(stepName, stepFieldName) {
            baseAttrs[stepFieldName] = null;
        });
        var mergedAttrs = $.extend(baseAttrs, attrs);
        mergedAttrs.year_label = AD.Models.Year.cache.getById(mergedAttrs.year_id).year_label;
        return new AD.Models.Contact(mergedAttrs);
    },
    
    fields: [
        {name: 'firstName', type: 'text'},
        {name: 'lastName', type: 'text'},
        {name: 'campus', type: 'choice'},
        {name: 'year', type: 'choice', field: 'year_label'},
        {name: 'phone', type: 'choice/text', keyboardType: Ti.UI.KEYBOARD_PHONE_PAD, autocapitalization: Ti.UI.TEXT_AUTOCAPITALIZATION_NONE},
        {name: 'email', type: 'choice/text', keyboardType: Ti.UI.KEYBOARD_EMAIL, autocapitalization: Ti.UI.TEXT_AUTOCAPITALIZATION_NONE},
        {name: 'notes', type: 'text', multiline: true}
    ],
    
    years: AD.Models.Year.cache.getArray().map(function(model) { return model.year_label; }),
    actions: [{
        title: 'save',
        callback: 'save',
        rightNavButton: true
    }, {
        callback: function() {
            if (this.operation === 'edit') {
                // Changes to contacts are automatically saved during editing
                this.save();
            }
            else {
                // Closing the window cancels the add or create operation
                this.dfd.reject();
            }
        },
        menuItem: false,
        onClose: true,
        backButton: true
    }]
}, {
    init: function(options) {
        var _this = this;
        
        this.operation = options.operation;
        var getContactDfd = $.Deferred();
        
        // This handler must be attached before the handler that calls this.initialize in $.Window
        getContactDfd.done(this.proxy(function(contactData) {
            this.inAddressBook = contactData.localContact ? true : false;
            
            // Build the fields array which is the same as the static fields array, with types expanded
            this.fields = this.constructor.fields.map(function(field) {
                // Expand the type property
                var types = field.type.split('/');
                var type = types[(this.inAddressBook || types.length === 1) ? 0 : 1];
                
                // Proxy the choice callback
                var callback = null;
                if (type === 'choice') {
                    callback = this.proxy(field.callback);
                }
                
                // Clone the field to prevent aliasing
                return $.extend({}, field, {type: type, callback: callback});
            }, this);
            
            this.contact = contactData.contact;
            this.localContact = contactData.localContact;
            this.window.title = AD.Localize(this.operation+'Contact');
            this.open();
        }));
        
        // Initialize the base $.Window object
        // Pass in deferreds to delay the execution of this.create and this.initialize until a contact is chosen
        this._super({
            tab: options.tab,
            createDfd: getContactDfd.promise(),
            initializeDfd: getContactDfd.promise(),
            createParams: {
                layout: 'vertical'
            }
        });
        
        if (this.operation === 'import') {
            // Load an existing contact from the user's address book
            var chooseContactDfd = $.Deferred();
            Titanium.Contacts.showContacts({
                canceled: chooseContactDfd.reject,
                selectedPerson: function(event) {
                    chooseContactDfd.resolve(event.person);
                }
            });
            chooseContactDfd.done(this.proxy(function(selectedContact) {
                var contactRecordId = selectedContact.recordId || selectedContact.id; // recordId on iOS and id on Android
                var existingContacts = AD.Models.Contact.cache.query({contact_recordId: contactRecordId});
                var contact = null;
                if (existingContacts.length > 0) {
                    if (existingContacts.length > 1) {
                        Ti.API.warn('Found multiple contacts with the same recordId!');
                    }
                    // A contact was chosen that already exists, so edit the contact
                    contact = existingContacts[0];
                    this.operation = 'edit';
                }
                else {
                    contact = this.constructor.createContact({contact_recordId: contactRecordId});
                }
                getContactDfd.resolve({
                    contact: contact,
                    localContact: selectedContact
                });
            })).fail(this.dfd.reject); // Cancel the add contact operation
        }
        else if (this.operation === 'edit') {
            // Load the existing contact from the address book
            var recordId = options.existingContact.contact_recordId;
            var localContact = recordId === null ? null : Ti.Contacts.getPersonByID(recordId);
            getContactDfd.resolve({
                contact: options.existingContact,
                localContact: localContact
            });
        }
        else if (this.operation === 'create') {
            // Create a contact model not tied to an address book entry
            var contact = this.constructor.createContact({contact_recordId: null});
            getContactDfd.resolve({
                contact: contact,
                localContact: null
            });
        }
    },
    
    // Create each of the form fields
    create: function() {
        var labelWidth = AD.Platform.isiPhone ? 80 : 60;
        var rowHeight = 40;
        
        var focusedTextField = null;
        var hideKeyboard = function() {
            if (focusedTextField) {
                // Unfocus the previously selected text field to hide the keyboard
                focusedTextField.blur();
                focusedTextField = null;
            }
        };

        // Scrollable container that will hold the field rows on non-iPhone platforms
        var table = Ti.UI.createScrollView({
            top: 0,
            left: 0,
            width: AD.UI.screenWidth,
            height: Ti.UI.FILL,
            layout: 'vertical',
            scrollType: 'vertical',
            contentHeight: 'auto',
            showVerticalScrollIndicator: true
        });
        
        // Create the form fields
        // On iPhone, attempt to mimic the built-in Contacts app

        // Create each of the field views
        var rows = [];
        this.fields.forEach(function(field, index) {
            // Create the field row container, a table view row on iPhone and a generic view on other platforms
            var fieldRow = AD.Platform.isiPhone ? Ti.UI.createTableViewRow({}) : Ti.UI.createView({
                left: AD.UI.padding,
                right: 0,
                top: 0
            });
            fieldRow.height = rowHeight;
            fieldRow.index = index;
            
            // Create the field name label
            var label = Ti.UI.createLabel({
                left: 0,
                width: labelWidth,
                height: Ti.UI.SIZE,
                text: AD.Localize(field.name)
            });
            fieldRow.add(label);
            if (AD.Platform.isiPhone) {
                label.applyProperties({
                    text: label.text.toLowerCase(),
                    textAlign: 'right',
                    color: AD.UI.systemBlueColor,
                    font: {fontSize: 15, fontWeight: 'bold'} // medium-small bold
                });
            }
            
            var fieldValue = this.contact.attr(field.field);
            var fieldView = null;
            if (field.type === 'choice') {
                // Create the value label
                if (AD.Platform.isiPhone) {
                    fieldView = Ti.UI.createLabel({
                        left: labelWidth + AD.UI.padding,
                        width: Ti.UI.FILL,
                        height: Ti.UI.FILL,
                        text: fieldValue
                    });
                }
                else {
                    fieldView = Ti.UI.createButton({
                        left: labelWidth + AD.UI.padding,
                        right: AD.UI.padding,
                        center: { y: rowHeight / 2 },
                        height: AD.UI.buttonHeight,
                        title: fieldValue || AD.Localize('unspecified')
                    });
                    // When a choice row is clicked, call the callback that will presumably allow the user to choose a value
                    fieldView.addEventListener('click', field.callback);
                }
            }
            else if (field.type === 'text') {
                if (field.multiline === true) {
                    fieldView = Ti.UI.createTextArea({
                        left: labelWidth + AD.UI.padding,
                        right: AD.UI.padding,
                        height: Ti.UI.FILL,
                        font: AD.UI.Fonts.small,
                        suppressReturn: false
                    });
                    // Make the row taller to accommodate the text area
                    fieldRow.height *= 3;
                }
                else {
                    fieldView = Ti.UI.createTextField({
                        left: labelWidth + AD.UI.padding,
                        right: AD.UI.padding,
                        center: { y: rowHeight / 2 },
                        height: AD.UI.textFieldHeight
                    });
                }
                
                fieldView.value = fieldValue;
                
                if (field.keyboardType) {
                    fieldView.keyboardType = field.keyboardType;
                }
                if (field.autocapitalization) {
                    fieldView.autocapitalization = field.autocapitalization;
                }
            }
            
            fieldView.addEventListener('focus', function() {
                // Keep track of which text field (or text area) is currently selected
                focusedTextField = fieldView;
            });
            
            // Add the field to the row
            fieldRow.add(this.record(field.labelId, fieldView));
            rows.push(fieldRow);
            
            if (!AD.Platform.isiPhone) {
                table.add(fieldRow);
            }
        }, this);
        
        // On iPhone, hideKeyboard does not work when called from the window click
        // handler, so workaround by calling hideKeybaord from the table click handler
        if (AD.Platform.isiPhone) {
            // Create the fields table that holds the year, phone number, and email address fields
            var iPhoneTable = this.add(Ti.UI.createTableView({
                data: rows,
                style: Ti.UI.iPhone.TableViewStyle.GROUPED
            }));
            iPhoneTable.addEventListener('click', this.proxy(function(event) {
                hideKeyboard();
                
                var field = this.fields[event.row.index];
                if (field.type === 'choice') {
                    // When a choice row is clicked, call the callback that will presumably allow the user to choose a value
                    field.callback();
                }
            }));
        }
        else {
            this.add(table);
            
            // Click anywhere on the window to hide the keyboard
            this.window.addEventListener('click', function(event) {
                hideKeyboard();
            });
            
            // Create the save button on the screen
            var saveButton = Ti.UI.createButton({
                left: AD.UI.padding,
                top: AD.UI.padding,
                width: AD.UI.useableScreenWidth,
                height: AD.UI.buttonHeight * 1.5,
                titleid: 'save'
            });
            table.add(saveButton);
            saveButton.addEventListener('click', this.proxy('save'));
        }
    },
    
    // Set the initial contents of the form fields
    initialize: function() {
        var localContact = this.localContact;
        this.phoneNumbers = localContact && AD.UI.ChooseOptionWindow.multivalueToOptionsArray(localContact.getPhone());
        this.emailAddresses = localContact && AD.UI.ChooseOptionWindow.multivalueToOptionsArray(localContact.getEmail());
    },
    
    // Handlers for allowing the user to change the contact's campus, year, phone number, and e-mail address
    changeCampus: function() {
        // Allow the user to set the contact's campus
        var campuses = AD.PropertyStore.get('campuses');
        var $winChooseCampus = new AD.UI.ChooseOptionWindow({
            tab: this.tab,
            groupName: 'campus',
            initial: campuses.indexOf(this.contact.contact_campus),
            options: campuses,
            editable: true,
            onOptionsUpdate: function(campusesNew) {
                campuses = campusesNew;
                AD.PropertyStore.set('campuses', campusesNew);
            }
        });
        $winChooseCampus.getDeferred().done(this.proxy(function(campusName) {
            // A campus was chosen
            this.contact.attr('contact_campus', campusName.label);
            var campusLabel = this.getChild('campusLabel');
            campusLabel.text = campusLabel.title = campusName.label;
        }));
    },
    changeYear: function() {
        // Allow the user to choose the year of this contact
        var $winChooseYear = new AD.UI.ChooseOptionWindow({
            tab: this.tab,
            groupName: 'year',
            initial: this.contact.year_id - 1,
            options: this.constructor.years
        });
        $winChooseYear.getDeferred().done(this.proxy(function(yearData) {
            // A year was chosen
            this.contact.attr('year_id', yearData.index + 1);
            this.contact.attr('year_label', yearData.label);
            var yearLabel = this.getChild('yearLabel');
            yearLabel.text = yearLabel.title = yearData.label;
        }));
    },
    changePhone: function() {
        // Allow the user to choose the phone number to associate with this contact
        var $winChoosePhone = new AD.UI.ChooseOptionWindow({
            tab: this.tab,
            groupName: 'phone',
            initial: this.contact.contact_phoneId,
            options: this.phoneNumbers
        });
        $winChoosePhone.getDeferred().done(this.proxy(function(phoneNumber) {
            // A phone number was chosen
            this.contact.attr('contact_phone', phoneNumber.value);
            this.contact.attr('contact_phoneId', phoneNumber.id);
            var phoneLabel = this.getChild('phoneLabel');
            phoneLabel.text = phoneLabel.title = phoneNumber.value;
        }));
    },
    changeEmail: function() {
        // Allow the user to choose the email address to associate with this contact
        var $winChooseEmail = new AD.UI.ChooseOptionWindow({
            tab: this.tab,
            groupName: 'email',
            initial: this.contact.contact_emailId,
            options: this.emailAddresses
        });
        $winChooseEmail.getDeferred().done(this.proxy(function(emailAddress) {
            // An email address was chosen
            this.contact.attr('contact_email', emailAddress.value);
            this.contact.attr('contact_emailId', emailAddress.id);
            var emailLabel = this.getChild('emailLabel');
            emailLabel.text = emailLabel.title = emailAddress.value;
        }));
    },
    
    // Update the contact model and close the window
    save: function() {
        // Read the values of the text fields
        this.fields.forEach(function(field) {
            if (field.type === 'text') {
                this.contact.attr(field.field, this.children[field.labelId].value);
            }
        }, this);
        this.dfd.resolve(this.contact);
        if (this.options.autoSave !== false) {
            // Create/update the contact's record in the database unless
            // explicitly prevented by the autoSave option being set to false
            this.contact.save();
        }
    }
});
var StringPromptWindow = module.exports = $.Window('AppDev.UI.StringPromptWindow', {
    actions: [{
        title: 'cancel',
        callback: 'cancel',
        leftNavButton: true,
        onClose: true,
        enabled: function() {
            return this.options.cancelable;
        }
    }, {
        callback: 'onSubmit',
        menuItem: false,
        onClose: true
    }],
    defaults: {
        title: 'stringPromptDefaultTitle',
        message: 'stringPromptDefaultMessage',
        initial: '',
        keyboardType: Ti.UI.KEYBOARD_DEFAULT,
        doneText: 'done',
        cancelable: true,
        modal: true,
        // Called when validating string input
        // Return an object with the 'valid' field set to the validity of the input string
        // Optionally set the 'reason' field to the reason why the input is invalid
        validateCallback: function(input) {
            // By default, all input strings are valid
            return { valid: true };
        }
    }
}, {
    init: function(options) {
        $.extend(true, this.options, options);
        
        // Initialize the base $.Window object
        this._super({
            createParams: {
                layout: 'vertical'
            },
            title: this.options.title,
            focusedChild: 'string',
            autoOpen: true
        });
    },
    
    // Create child views
    create: function() {
        this.add('messageLabel', Ti.UI.createLabel({
            left: AD.UI.padding,
            top: AD.UI.padding,
            width: AD.UI.useableScreenWidth,
            height: Ti.UI.SIZE,
            font: AD.UI.Fonts.small,
            text: AD.localize(this.options.message)
        }));
        var string = this.add('string', Ti.UI.createTextField({
            left: AD.UI.padding,
            top: AD.UI.padding,
            width: AD.UI.useableScreenWidth,
            height: AD.UI.textFieldHeight,
            value: this.options.initial,
            font: AD.UI.Fonts.small,
            autocorrect: false,
            autocapitalization: Ti.UI.TEXT_AUTOCAPITALIZATION_NONE,
            keyboardType: this.options.keyboardType,
            borderStyle: Ti.UI.INPUT_BORDERSTYLE_ROUNDED
        }));
        var doneButton = this.add('done', Ti.UI.createButton({
            top: AD.UI.padding,
            center: {x: AD.UI.screenWidth / 2},
            width: 80,
            height: AD.UI.buttonHeight,
            titleid: this.options.doneText
        }));
        var onSubmit = this.proxy('onSubmit'); // avoid creating two proxies of the same function
        doneButton.addEventListener('click', onSubmit);
        string.addEventListener('return', onSubmit);
        this.add('status', Ti.UI.createLabel({
            left: AD.UI.padding,
            top: AD.UI.padding * 2,
            width: AD.UI.useableScreenWidth,
            height: Ti.UI.SIZE,
            font: AD.UI.Fonts.small
        }));
    },
    
    onSubmit: function() {
        var value = this.getChild('string').value;
        var validity = this.options.validateCallback.call(this, value);
        if (validity.valid === true) {
            this.dfd.resolve(value);
        }
        else if (validity.valid === false) {
            this.getChild('status').text = AD.localize(validity.reason || 'stringPromptInvalidInput');
        }
        else {
            throw 'Invalid "valid" field returned by validateCallback: ['+validity.valid+']!';
        }
        return validity.valid;
    }
});