Skip to content
Snippets Groups Projects
services.js 38.8 KiB
Newer Older
  • Learn to ignore specific revisions
  • Cam Saul's avatar
    Cam Saul committed
    'use strict';
    /*jslint browser:true */
    /*global _*/
    /* Services */
    
    
    var CorvusServices = angular.module('corvus.services', ['http-auth-interceptor', 'ipCookie', 'corvus.core.services']);
    
    Cam Saul's avatar
    Cam Saul committed
    
    
    CorvusServices.factory('AppState', ['$rootScope', '$routeParams', '$q', '$location', '$timeout', 'ipCookie', 'Session', 'User', 'Organization', 'PermissionViolation',
        function($rootScope, $routeParams, $q, $location, $timeout, ipCookie, Session, User, Organization, PermissionViolation) {
    
    Cam Saul's avatar
    Cam Saul committed
            // this is meant to be a global service used for keeping track of our overall app state
            // we fire 2 events as things change in the app
            // 1. appstate:user
            // 2. appstate:organization
    
    
    Cam Saul's avatar
    Cam Saul committed
            var service = {
    
                model: {
    
    Cam Saul's avatar
    Cam Saul committed
                    currentUser: null,
                    currentOrgSlug: null,
    
    Cam Saul's avatar
    Cam Saul committed
                },
    
                init: function() {
    
    
                    if (!initPromise) {
                        var deferred = $q.defer();
                        initPromise = deferred.promise;
    
                        // just make sure we grab the current user
    
                        service.refreshCurrentUser().then(function(user) {
    
    Cam Saul's avatar
    Cam Saul committed
                },
    
    
                    service.model.currentUser = null;
                    service.model.currentOrgSlug = null;
                    service.model.currentOrg = null;
    
                    // clear any existing session cookies if they exist
                    ipCookie.remove('metabase.SESSION_ID');
                },
    
                setCurrentOrgCookie: function(slug) {
                    var isSecure = ($location.protocol() === "https") ? true : false;
    
    Cam Saul's avatar
    Cam Saul committed
                    ipCookie('metabase.CURRENT_ORG', slug, {
                        path: '/',
                        secure: isSecure
                    });
    
    Cam Saul's avatar
    Cam Saul committed
                refreshCurrentUser: function() {
    
    Cam Saul's avatar
    Cam Saul committed
                    // this is meant to be called once on app startup
    
                    var userRefresh = User.current(function(result) {
    
    Cam Saul's avatar
    Cam Saul committed
                        service.model.currentUser = result;
    
    
                        // add isMember(orgSlug) method to the object
    
                        service.model.currentUser.isMember = function(orgSlug) {
                            return this.org_perms.some(function(org_perm) {
    
                                return org_perm.organization.slug === orgSlug;
                            });
                        };
    
                        // add isAdmin(orgSlug) method to the object
    
                        service.model.currentUser.isAdmin = function(orgSlug) {
                            return this.org_perms.some(function(org_perm) {
    
                                return org_perm.organization.slug === orgSlug && org_perm.admin;
                            }) || this.is_superuser;
                        };
    
                        // add memberOf() method to the object enumerating Organizations user is member of
    
                        service.model.currentUser.memberOf = function() {
                            return this.org_perms.map(function(org_perm) {
    
                                return org_perm.organization;
                            });
                        };
    
                        // add adminOf() method to the object enumerating Organizations user is admin of
    
                        service.model.currentUser.adminOf = function() {
                            return this.org_perms.filter(function(org_perm) {
    
                                return org_perm.admin;
    
                                return org_perm.organization;
                            });
                        };
    
    
                        // apply a convenience variable indicating if the user is a member of multiple orgs
                        service.model.currentUser.is_multi_org = (service.model.currentUser.memberOf().length > 1);
    
    
    Cam Saul's avatar
    Cam Saul committed
                        $rootScope.$broadcast('appstate:user', result);
    
    Cam Saul's avatar
    Cam Saul committed
                        console.log('unable to get current user', error);
                    });
    
                    // NOTE: every time we refresh the user we update our current promise to ensure that
                    //       we can guarantee we've resolved the current user
                    currentUserPromise = userRefresh.$promise;
    
                    return currentUserPromise;
    
    Cam Saul's avatar
    Cam Saul committed
                switchOrg: function(org_slug) {
    
    Cam Saul's avatar
    Cam Saul committed
                        'slug': org_slug
                    }, function(org) {
    
                        service.model.currentOrgSlug = org.slug;
                        service.model.currentOrg = org;
                        $rootScope.$broadcast('appstate:organization', service.model.currentOrg);
    
    Cam Saul's avatar
    Cam Saul committed
                    }, function(error) {
    
                // This function performs whatever state cleanup and next steps are required when a user tries to access
                // something they are not allowed to.
    
    Cam Saul's avatar
    Cam Saul committed
                invalidAccess: function(user, url, message) {
    
                    service.model.currentOrgSlug = null;
                    service.model.currentOrg = null;
    
    
    Cam Saul's avatar
    Cam Saul committed
                    PermissionViolation.create({
                        'user': user.id,
                        'url': url
                    });
    
    Cam Saul's avatar
    Cam Saul committed
                routeChanged: function(event) {
    
                    // establish our application context based on the route (URI)
                    // valid app contexts are: 'setup', 'auth', 'org', 'org-admin', 'site-admin', 'other', or 'unknown'
                    var routeContext;
                    if ($location.path().indexOf('/auth/') === 0) {
                        routeContext = 'auth';
                    } else if ($location.path().indexOf('/setup/') === 0) {
                        routeContext = 'setup';
                    } else if ($location.path().indexOf('/superadmin/') === 0) {
                        routeContext = 'site-admin';
                    } else if ($routeParams.orgSlug) {
                        // couple of options when within an org
    
    Cam Saul's avatar
    Cam Saul committed
                        if ($location.path().indexOf('/' + $routeParams.orgSlug + '/admin/') === 0) {
    
                            routeContext = 'org-admin';
                        } else {
                            routeContext = 'org';
                        }
                    } else {
                        routeContext = 'other';
                    }
    
                    // if the context of the app has changed due to this route change then send out an event
                    if (service.model.appContext !== routeContext) {
                        service.model.appContext = routeContext;
                        $rootScope.$broadcast('appstate:context-changed', service.model.appContext);
                    }
    
    
                    // this code is here to ensure that we have resolved our currentUser BEFORE we execute any other
                    // code meant to establish app context based on the current route
    
                    currentUserPromise.then(function(user) {
    
                        service.routeChangedImpl(event);
    
                },
    
                routeChangedImpl: function(event) {
    
    Cam Saul's avatar
    Cam Saul committed
                    // whenever we have a route change (including initial page load) we need to establish some context
    
    
                    // if we don't have a current user then the only sensible destination is the login page
                    if (!service.model.currentUser) {
                        // make sure we clear out any current state just to be safe
    
                        if ($location.path().indexOf('/auth/') !== 0 && $location.path().indexOf('/setup/') !== 0) {
    
                            // if the user is asking for a url outside of /auth/* then send them to login page
                            // otherwise we will let the user continue on to their requested page
                            $location.path('/auth/login');
                        }
    
    
                    var onSuperadminPage = $location.path().indexOf('/superadmin/') === 0;
    
    
    Cam Saul's avatar
    Cam Saul committed
                    // NOTE: if you try to do this outside this event you'll run into issues where $routeParams is not set.
                    //       so that's why we explicitly wait until we know when $routeParams will be available
    
                    if (onSuperadminPage) {
                        // the user is trying to change to a superuser page
    
                        if (!service.model.currentUser.is_superuser) {
                            service.invalidAccess(service.model.currentUser, $location.url(), "user is not a superuser!!!");
                            return;
                        }
    
                    } else if ($routeParams.orgSlug) {
    
    Cam Saul's avatar
    Cam Saul committed
                        // the url is telling us what Organization we are working in
    
                        // PERMISSIONS CHECK!!  user must be member of this org to proceed
    
                        // Making convenience vars so it's easier to scan conditions for correctness
                        var isSuperuser = service.model.currentUser.is_superuser;
    
                        var isOrgMember = service.model.currentUser.isMember($routeParams.orgSlug);
                        var isOrgAdmin = service.model.currentUser.isAdmin($routeParams.orgSlug);
    
    Cam Saul's avatar
    Cam Saul committed
                        var onAdminPage = $location.path().indexOf('/' + $routeParams.orgSlug + '/admin') === 0;
    
    
                        if (!isSuperuser && !isOrgMember) {
                            service.invalidAccess(service.model.currentUser, $location.url(), "user is not authorized for this org!!!");
    
                        } else if (onAdminPage && !isSuperuser && !isOrgAdmin) {
                            service.invalidAccess(service.model.currentUser, $location.url(), "user is not an admin for this org!!!");
    
    Cam Saul's avatar
    Cam Saul committed
                        if (service.model.currentOrgSlug != $routeParams.orgSlug) {
    
                            // we just navigated to a new organization
    
    Cam Saul's avatar
    Cam Saul committed
                            this.switchOrg($routeParams.orgSlug);
    
    Cam Saul's avatar
    Cam Saul committed
                            service.model.currentOrgSlug = $routeParams.orgSlug;
    
                            service.setCurrentOrgCookie(service.model.currentOrgSlug);
    
    Cam Saul's avatar
    Cam Saul committed
                        }
    
                        // if we get here it just means we navigated somewhere within the existing org, so do nothing
    
                    } else if (!service.model.currentOrgSlug) {
                        // the url doesn't tell us what Organization this is, so lets try a different approach
    
                        // Check to see if the user has a current org cookie var set
                        var currentOrgFromCookie = ipCookie('metabase.CURRENT_ORG');
    
    Cam Saul's avatar
    Cam Saul committed
                        if (currentOrgFromCookie) {
    
    Cam Saul's avatar
    Cam Saul committed
                            var orgsWithSlug = service.model.currentUser.org_perms.filter(function(org_perm) {
    
                                return org_perm.organization.slug == currentOrgFromCookie;
                            });
    
    Cam Saul's avatar
    Cam Saul committed
                            if (orgsWithSlug.length > 0) {
    
                                var currentOrgPerm = orgsWithSlug[0];
                                service.model.currentOrg = currentOrgPerm.organization;
                                service.model.currentOrgSlug = service.model.currentOrg.slug;
                                service.setCurrentOrgCookie(service.model.currentOrgSlug);
                                $rootScope.$broadcast('appstate:organization', service.model.currentOrg);
                                return;
                            }
                        }
                        // Otherwise fall through and set the current org to the first org a user is a member of
    
                        if (service.model.currentUser.org_perms.length > 0) {
                            service.model.currentOrg = service.model.currentUser.org_perms[0].organization;
                            service.model.currentOrgSlug = service.model.currentOrg.slug;
    
                            service.setCurrentOrgCookie(service.model.currentOrgSlug);
    
                            $rootScope.$broadcast('appstate:organization', service.model.currentOrg);
                        } else {
                            // TODO: this is a real issue.  we have a user with no organizations.  where do we send them?
    
    Cam Saul's avatar
    Cam Saul committed
                        }
                    }
                }
            };
    
            // listen for all route changes so that we can update organization as appropriate
            $rootScope.$on('$routeChangeSuccess', service.routeChanged);
    
    
            // login just took place, so lets force a refresh of the current user
            $rootScope.$on("appstate:login", function(event, session_id) {
    
            });
    
            // logout just took place, do some cleanup
            $rootScope.$on("appstate:logout", function(event, session_id) {
    
                // clear out any current state
    
    
                // NOTE that we don't really care about callbacks in this case
                Session.delete({
                    'session_id': session_id
                });
            });
    
            // NOTE: the below events are generated from the http-auth-interceptor which listens on our $http calls
            //       and intercepts calls that result in a 401 or 403 so that we can handle them here.  You must be
            //       careful to consider the implications of this because any endpoint that returns a 401/403 can
            //       have its call stack interrupted now and handled here instead of its normal callback sequence.
    
    
            // $http interceptor received a 401 response
    
            $rootScope.$on("event:auth-loginRequired", function() {
    
                // this is effectively just like a logout, we want to reset everything to a base state, then force login
                service.clearState();
    
    
                // this is ridiculously stupid.  we have to wait (300ms) for the cookie to actually be set in the browser :(
                $timeout(function() {
                    $location.path('/auth/login');
                }, 300);
    
            });
    
            // $http interceptor received a 403 response
            $rootScope.$on("event:auth-forbidden", function() {
                $location.path("/unauthorized");
            });
    
    
    Cam Saul's avatar
    Cam Saul committed
            return service;
        }
    ]);
    
    CorvusServices.service('CorvusCore', ['$resource', 'User', function($resource, User) {
        this.perms = [{
            'id': 0,
            'name': 'Private'
        }, {
            'id': 1,
            'name': 'Others can read'
        }, {
            'id': 2,
            'name': 'Others can read and modify'
        }];
    
        this.permName = function(permId) {
            if (permId >= 0 && permId <= (this.perms.length - 1)) {
                return this.perms[permId].name;
            }
            return null;
        };
    
        this.charts = [{
            'id': 'scalar',
            'name': 'Scalar'
        }, {
            'id': 'table',
            'name': 'Table'
        }, {
            'id': 'pie',
            'name': 'Pie Chart'
        }, {
            'id': 'bar',
            'name': 'Bar Chart'
        }, {
            'id': 'line',
            'name': 'Line Chart'
        }, {
            'id': 'area',
            'name': 'Area Chart'
        }, {
            'id': 'timeseries',
            'name': 'Time Series'
        }, {
            'id': 'pin_map',
            'name': 'Pin Map'
        }, {
            'id': 'country',
            'name': 'World Heatmap'
        }, {
            'id': 'state',
            'name': 'State Heatmap'
        }];
    
        this.chartName = function(chartId) {
            for (var i = 0; i < this.charts.length; i++) {
                if (this.charts[i].id == chartId) {
                    return this.charts[i].name;
                }
            }
            return null;
        };
    
        this.table_entity_types = [{
            'id': null,
            'name': 'None'
        }, {
            'id': 'person',
            'name': 'Person'
        }, {
            'id': 'event',
            'name': 'Event'
        }, {
            'id': 'photo',
            'name': 'Photo'
        }, {
            'id': 'place',
            'name': 'Place'
        }, {
            'id': 'evt-cohort',
            'name': 'Cohorts-compatible Event'
        }];
    
        this.tableEntityType = function(typeId) {
            for (var i = 0; i < this.table_entity_types.length; i++) {
                if (this.table_entity_types[i].id == typeId) {
                    return this.table_entity_types[i].name;
                }
            }
            return null;
        };
    
        this.field_special_types = [{
            'id': null,
            'name': 'None'
        }, {
            'id': 'avatar',
            'name': 'Avatar Image URL'
        }, {
            'id': 'category',
            'name': 'Category'
        }, {
            'id': 'city',
            'name': 'City'
        }, {
            'id': 'country',
            'name': 'Country'
        }, {
            'id': 'desc',
            'name': 'Description'
        }, {
            'id': 'fk',
            'name': 'Foreign Key'
        }, {
            'id': 'id',
            'name': 'Entity Key'
        }, {
            'id': 'image',
            'name': 'Image URL'
        }, {
            'id': 'json',
            'name': 'Field containing JSON'
        }, {
            'id': 'latitude',
            'name': 'Latitude'
        }, {
            'id': 'longitude',
            'name': 'Longitude'
        }, {
            'id': 'name',
            'name': 'Entity Name'
        }, {
            'id': 'number',
            'name': 'Number'
        }, {
            'id': 'state',
            'name': 'State'
        }, {
            'id': 'url',
            'name': 'URL'
        }, {
            'id': 'zip_code',
            'name': 'Zip Code'
        }];
    
    Cam Saul's avatar
    Cam Saul committed
        this.field_field_types = [{
            'id': 'info',
            'name': 'Information'
        }, {
            'id': 'metric',
            'name': 'Metric'
        }, {
            'id': 'dimension',
            'name': 'Dimension'
        }];
    
        this.boolean_types = [{
            'id': true,
            'name': 'Yes'
        }, {
            'id': false,
            'name': 'No'
    
    Cam Saul's avatar
    Cam Saul committed
    
        this.fieldSpecialType = function(typeId) {
            for (var i = 0; i < this.field_special_types.length; i++) {
                if (this.field_special_types[i].id == typeId) {
                    return this.field_special_types[i].name;
                }
            }
            return null;
        };
    
        this.builtinToChart = {
            'latlong_heatmap': 'll_heatmap'
        };
    
        this.getTitleForBuiltin = function(viewtype, field1Name, field2Name) {
            var builtinToTitleMap = {
                'state': 'State Heatmap',
                'country': 'Country Heatmap',
                'pin_map': 'Pin Map',
                'heatmap': 'Heatmap',
                'cohorts': 'Cohorts',
                'latlong_heatmap': 'Lat/Lon Heatmap'
            };
    
            var title = builtinToTitleMap[viewtype];
            if (field1Name) {
                title = title.replace("{0}", field1Name);
            }
            if (field2Name) {
                title = title.replace("{1}", field2Name);
            }
    
            return title;
        };
    
        this.createLookupTables = function(table) {
            // Create lookup tables (ported from ExploreTableDetailData)
    
            table.fields_lookup = {};
            _.each(table.fields, function(field) {
                table.fields_lookup[field.id] = field;
                field.operators_lookup = {};
                _.each(field.valid_operators, function(operator) {
                    field.operators_lookup[operator.name] = operator;
                });
            });
    
            table.aggregation_lookup = {};
            _.each(table.aggregation_options, function(agg) {
                table.aggregation_lookup[agg.short] = agg;
            });
        };
    
        // this just makes it easier to access the current user
        this.currentUser = User.current;
    
    
        // The various DB engines we support <3
        // TODO - this should probably come back from the API, no?
    
        //
        // NOTE:
        // A database's connection details is stored in a JSON map in the field database.details.
        // Originially, this map was expected to contain a single key called 'conn_str' that combined all of a database's connection details.
        // In real life, both the backend and frontend need access to the individual values, and have implemented complicated logic to parse conn_str.
        // Thus, we are moving towards saving the connection details in a 'new-style' broken-out map, instead of as 'legacy' map containing just a combined conn_str.
        //
        // Until this is fully supported by the backend(s), we can save the connection details with both the 'new-style' broken-out values, and the combined conn_str
        // to ensure backwards-compatibility. Until this transition is complete, however, we'll still need to handle legacy maps containing just 'conn_str'.
        //
    
        // *  name         - human-facing name to use for this DB engine
        // *  buildDetails - take a 'new-style' details map and add 'conn_str' for backwards compatibility, if needed
        // *  parseDetails - take a details map and parse 'conn_str' if it's a legacy map. Otherwise we can return the map as-is
    
        // *  fields       - array of available fields to display when a user adds/edits a DB of this type. Each field should be a dict of the format below:
        //
        // FIELD DICT FORMAT:
        // *  displayName          - user-facing name for the Field
        // *  fieldName            - name used for the field in a database details dict
        // *  transform            - function to apply to this value before passing to the API, such as 'parseInt'. (default: none)
        // *  placeholder          - placeholder value that should be used in text input for this field (default: none)
        // *  placeholderIsDefault - if true, use the value of 'placeholder' as the default value of this field if none is specified (default: false)
        //                           (if you set this, don't set 'required', or user will still have to add a value for the field)
        // *  required             - require the user to enter a value for this field? (default: false)
        // *  choices              - array of possible values for this field. If provided, display a button toggle instead of a text input.
        //                           Each choice should be a dict of the format below: (optional)
        //
        // CHOICE DICT FORMAT:
        // *  name            - User-facing name for the choice.
        // *  value           - Value to use for the choice in the database connection details dict.
        // *  selectionAccent - What accent type should be applied to the field when its value is chosen? Either 'active' (currently green), or 'danger' (currently red).
    
        this.ENGINES = {
            postgres: {
                name: 'Postgres',
                fields: [{
                    displayName: "Host",
                    fieldName: "host",
                    placeholder: "localhost",
    
                    placeholderIsDefault: true
    
                }, {
                    displayName: "Port",
                    fieldName: "port",
    
                    placeholderIsDefault: true
    
                }, {
                    displayName: "Database name",
                    fieldName: "dbname",
                    placeholder: "birds_of_the_world",
                    required: true
                }, {
                    displayName: "Database username",
                    fieldName: "user",
                    placeholder: "What username do you use to login to the database?",
                    required: true
                }, {
                    displayName: "Database password",
    
                    placeholder: "*******"
                }, {
                    displayName: "Use a secure connection (SSL)?",
                    fieldName: "ssl",
                    choices: [{
    
    Allen Gilliland's avatar
    Allen Gilliland committed
                        name: 'Yes',
    
                        value: true,
                        selectionAccent: 'active'
                    }, {
    
    Allen Gilliland's avatar
    Allen Gilliland committed
                        name: 'No',
    
                        value: false,
                        selectionAccent: 'danger'
                    }]
                }],
                parseDetails: function(details) {
    
                    // Check for new-style details
                    if (details.dbname) return details;
    
                    // Otherwise parse legacy details
    
                    var map = {
                        ssl: details.ssl
                    };
                    details.conn_str.split(' ').forEach(function(val) {
                        var split = val.split('=');
                        if (split.length === 2) {
                            map[split[0]] = split[1];
                        }
                    });
                    return map;
                },
                buildDetails: function(details) {
    
                    // add conn_str for backwards-compatibility
    
                    details.conn_str =
                        "host=" + details.host +
                        " port=" + details.port +
                        " dbname=" + details.dbname +
                        " user=" + details.user +
                        (details.pass ? (" password=" + details.pass) : '');
    
                }
            },
            h2: {
                name: 'H2',
                fields: [{
                    displayName: "Connection String",
    
                    placeholder: "file:/Users/camsaul/bird_sightings/toucans;AUTO_SERVER=TRUE"
                }],
                parseDetails: function(details) {
    
                    // Check for new-style details
    
    
                    // Otherwise parse legacy details
    
                    // add conn_str for backwards-compatibility
    
                }
            },
            mongo: {
                name: 'MongoDB',
                fields: [{
                    displayName: "Host",
                    fieldName: "host",
                    placeholder: "localhost",
    
                    placeholderIsDefault: true
    
                }, {
                    displayName: "Port",
                    fieldName: "port",
    
                    placeholder: "27017"
                }, {
                    displayName: "Database name",
                    fieldName: "dbname",
                    placeholder: "carrierPigeonDeliveries",
                    required: true
                }, {
                    displayName: "Database username",
                    fieldName: "user",
                    placeholder: "What username do you use to login to the database?"
                }, {
                    displayName: "Database password",
                    fieldName: "pass",
                    placeholder: "******"
                }],
                parseDetails: function(details) {
    
                    // check for new-style details
                    if (details.dbname) return details;
    
                    // otherwise parse legacy details
    
                    var regex = /^mongodb:\/\/(?:([^@:]+)(?::([^@:]+))?@)?([^\/:@]+)(?::([\d]+))?\/([^\/]+)$/gm, // :scream:
                        matches = regex.exec(details.conn_str);
                    return {
                        user: matches[1],
                        pass: matches[2],
                        host: matches[3],
                        port: matches[4],
                        dbname: matches[5]
                    };
                },
                buildDetails: function(details) {
    
                    // add conn_str for backwards-compatibility
    
                    var connStr = "mongodb://";
                    if (details.user) {
                        connStr += details.user;
                        if (details.pass) {
                            connStr += ":" + details.pass;
                        }
                        connStr += "@";
                    }
                    connStr += details.host;
                    if (details.port) {
                        connStr += ":" + details.port;
                    }
                    connStr += "/" + details.dbname;
    
    
                    details.conn_str = connStr;
                    return details;
    
    
        // Prepare database details before being sent to the API.
        // This includes applying 'transform' functions and adding default values where applicable.
        this.prepareDatabaseDetails = function(details) {
            if (!details.engine) throw "Missing key 'engine' in database request details; please add this as API expects it in the request body.";
    
            // iterate over each field definition
            this.ENGINES[details.engine].fields.forEach(function(field) {
                var fieldName = field.fieldName;
    
                // set default value if applicable
                if (!details[fieldName] && field.placeholderIsDefault) {
                    details[fieldName] = field.placeholder;
                }
    
                // apply transformation function if applicable
                if (details[fieldName] && field.transform) {
                    details[fieldName] = field.transform(details[fieldName]);
                }
            });
    
            return details;
        };
    
    Cam Saul's avatar
    Cam Saul committed
    }]);
    
    CorvusServices.service('CorvusAlert', [function() {
        this.alerts = [];
    
        this.closeAlert = function(index) {
            this.alerts.splice(index, 1);
        };
    
        this.alertInfo = function(message) {
            this.alerts.push({
                type: 'success',
                msg: message
            });
        };
    
        this.alertError = function(message) {
            this.alerts.push({
                type: 'danger',
                msg: message
            });
        };
    }]);
    
    CorvusServices.factory("transformRequestAsFormPost", [function() {
        return (transformRequest);
    
        function transformRequest(data, getHeaders) {
            var headers = getHeaders();
            headers["Content-Type"] = "application/x-www-form-urlencoded; charset=utf-8";
            headers["X-CSRFToken"] = data.csrfmiddlewaretoken;
            return (serializeData(data));
        }
    
        function serializeData(data) {
    
            // If this is not an object, defer to native stringification.
            if (!angular.isObject(data)) {
                return ((data === null) ? "" : data.toString());
            }
    
            var buffer = [];
    
            // Serialize each key in the object.
            for (var name in data) {
                if (!data.hasOwnProperty(name)) {
                    continue;
                }
    
                var value = data[name];
                buffer.push(
                    encodeURIComponent(name) +
                    "=" +
                    encodeURIComponent((value === null) ? "" : value)
                );
            }
    
            // Serialize the buffer and clean it up for transportation.
            var source = buffer
                .join("&")
                .replace(/%20/g, "+");
    
            return (source);
        }
    
    }]);
    
    CorvusServices.service('CorvusFormService', function() {
        // Registered form controllers
        var formControllers = [];
    
        this.errorMessages = {
            required: 'This field is required',
            email: 'Not a valid email address',
            password_verify: 'Passwords must match'
        };
    
        this.setFormErrors = function(formName, errors) {
            var formController = formControllers[formName];
            if (typeof formController == "undefined") {
                throw ("ERROR: unknown form name: " + formName + "; cannot continue");
            }
    
            Object.keys(errors).forEach(function(fieldName) {
                if (typeof formController.form[fieldName] == "undefined") {
                    console.error("ERROR submitting form; error does not map to a valid field name: " + fieldName + ": " + errors[fieldName]);
                    formController.formStatus = formController.formStatus + "; " + fieldName + ": " + errors[fieldName];
                    return;
                }
                formController.form[fieldName].$dirty = true;
                formController.form[fieldName].$setValidity('serverSideValidation', false);
                formController.setErrorsFor(fieldName, errors[fieldName]);
            });
        };
    
        // Registers form controller by form name
        this.register = function(formName, formController) {
            formControllers[formName] = formController;
        };
    
        this.getFormController = function(formName) {
            var formController = formControllers[formName];
            if (typeof formController == "undefined") {
                throw ("ERROR: unknown form name: " + formName + "; cannot continue");
            }
    
            return formController;
        };
    
        this.clearServerSideValidationErrors = function(formName) {
            var formController = formControllers[formName];
            if (typeof formController == "undefined") {
                throw ("ERROR: unknown form name: " + formName + "; cannot continue");
            }
    
            _.each(formController.form, function(field) {
                if (field.hasOwnProperty("$setValidity")) {
                    field.$setValidity("serverSideValidation", true);
                }
            });
        };
    
        this.submitSuccessCallback = function(formName, successMessage) {
            var formController = formControllers[formName];
            if (typeof formController == "undefined") {
                throw ("ERROR: unknown form name: " + formName + "; cannot continue");
            }
    
            this.clearServerSideValidationErrors(formName);
            formController.formStatus = successMessage;
            formController.form.$setPristine();
            formController.saveModel();
        };
    
        this.submitFailedCallback = function(formName, err, failedMessage) {
            var formController = formControllers[formName];
            if (typeof formController == "undefined") {
                throw ("ERROR: unknown form name: " + formName + "; cannot continue");
            }
            formController.formStatus = failedMessage;
    
            this.setFormErrors(formName, err);
        };
    
    
    });
    
    
    // User Services
    var CoreServices = angular.module('corvus.core.services', ['ngResource', 'ngCookies']);
    
    
    CoreServices.factory('Session', ['$resource', '$cookies', function($resource, $cookies) {
        return $resource('/api/session/', {}, {
            create: {
                method: 'POST',
    
    Cam Saul's avatar
    Cam Saul committed
                ignoreAuthModule: true // this ensures a 401 response doesn't trigger another auth-required event
    
    Cam Saul's avatar
    Cam Saul committed
                method: 'DELETE'
    
            },
            forgot_password: {
                url: '/api/session/forgot_password',
                method: 'POST',
                headers: {
                    'X-CSRFToken': function() {
                        return $cookies.csrftoken;
                    }
                }
            },
            reset_password: {
                url: '/api/session/reset_password',
                method: 'POST',
                headers: {
                    'X-CSRFToken': function() {
                        return $cookies.csrftoken;
                    }
                }
    
    Cam Saul's avatar
    Cam Saul committed
    CoreServices.factory('User', ['$resource', '$cookies', function($resource, $cookies) {
        return $resource('/api/user/:userId', {}, {
            list: {
                url: '/api/user/',
                method: 'GET',
                isArray: true
            },
            current: {
                url: '/api/user/current/',
                method: 'GET',
    
    Cam Saul's avatar
    Cam Saul committed
                ignoreAuthModule: true // this ensures a 401 response doesn't trigger another auth-required event
    
    Cam Saul's avatar
    Cam Saul committed
            },
            get: {
                url: '/api/user/:userId',
                method: 'GET',
                params: {
                    'userId': '@userId'
                }
            },
            update: {
                url: '/api/user/:userId',
                method: 'PUT',
                params: {
                    'userId': '@id'
                },
                headers: {
                    'X-CSRFToken': function() {
                        return $cookies.csrftoken;
                    }
                }
            },
            update_password: {
    
    Cam Saul's avatar
    Cam Saul committed
                method: 'PUT',
                params: {
                    'userId': '@id'
                },
                headers: {
                    'X-CSRFToken': function() {
                        return $cookies.csrftoken;
                    }
                }
            }
        });
    }]);
    
    CoreServices.factory('Organization', ['$resource', '$cookies', function($resource, $cookies) {
        return $resource('/api/org/:orgId', {}, {
    
                url: '/api/org/form_input',
                method: 'GET'
    
    Cam Saul's avatar
    Cam Saul committed
            list: {
                url: '/api/org/',
                method: 'GET',
                isArray: true
            },
    
    Kyle Doherty's avatar
    Kyle Doherty committed
            create: {
                url: '/api/org',
                method: 'POST',
                headers: {
                    'X-CSRFToken': function() {
                        return $cookies.csrftoken;
                    }
    
    Kyle Doherty's avatar
    Kyle Doherty committed
                }
    
    Kyle Doherty's avatar
    Kyle Doherty committed
            },
    
    Cam Saul's avatar
    Cam Saul committed
            get: {
                url: '/api/org/:orgId',
                method: 'GET',
                params: {
                    orgId: '@orgId'
                }
            },
            get_by_slug: {
                url: '/api/org/slug/:slug',
                method: 'GET',
                params: {
                    slug: '@slug'
                }
            },
            update: {
                url: '/api/org/:orgId',
                method: 'PUT',
                params: {
                    orgId: '@id'
                },
                headers: {
                    'X-CSRFToken': function() {