diff --git a/Makefile b/Makefile index f205b7d0d..22d4ab15d 100644 --- a/Makefile +++ b/Makefile @@ -1,20 +1,48 @@ -# Config +# OpenCraft -- tools to aid developing and hosting free software projects +# Copyright (C) 2015 OpenCraft +# +# This program is free software: you can redistribute it and/or modify +# it under the terms of the GNU Affero General Public License as +# published by the Free Software Foundation, either version 3 of the +# License, or (at your option) any later version. +# +# This program is distributed in the hope that it will be useful, +# but WITHOUT ANY WARRANTY; without even the implied warranty of +# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +# GNU Affero General Public License for more details. +# +# You should have received a copy of the GNU Affero General Public License +# along with this program. If not, see . +# + +# Config ###################################################################### + WORKERS = 4 SHELL = /bin/bash + +# Parameters ################################################################## + # For `test_one` use the rest as arguments and turn them into do-nothing targets ifeq (test_one,$(firstword $(MAKECMDGOALS))) RUN_ARGS := $(wordlist 2,$(words $(MAKECMDGOALS)),$(MAKECMDGOALS)) $(eval $(RUN_ARGS):;@:) endif + +# Commands #################################################################### + +all: + rundev + clean: find -name '*.pyc' -delete find -name '*~' -delete find -name '__pycache__' -type d -delete rm -rf .coverage build + find static/js/external -type f -not -name 'Makefile' -not -name '.gitignore' -delete -collectstatic: clean +collectstatic: clean js_external honcho run ./manage.py collectstatic --noinput migrate: clean @@ -26,12 +54,18 @@ migration_check: clean run: clean migration_check collectstatic honcho start --concurrency "worker=$(WORKERS)" -rundev: clean migration_check +rundev: clean migration_check js_external honcho start -f Procfile.dev shell: honcho run ./manage.py shell_plus +upgrade_dependencies: + pip freeze --local | grep -v '^\-e' | cut -d = -f 1 | xargs -n1 pip install -U + + +# Tests ####################################################################### + test_prospector: clean prospector --profile opencraft @@ -49,11 +83,20 @@ test_integration: clean echo -e "\nIntegration tests skipped (create a '.env.integration' file to run them)" ; \ fi -test: clean test_prospector test_unit test_integration +test_js: clean js_external + cd instance/tests/js && jasmine-ci --logs --browser firefox + +test_js_web: clean js_external + cd instance/tests/js && jasmine --host 0.0.0.0 + +test: clean test_prospector test_unit test_js test_integration @echo -e "\nAll tests OK!\n" test_one: clean honcho -e .env.test run ./manage.py test $(RUN_ARGS) -upgrade_dependencies: - pip freeze --local | grep -v '^\-e' | cut -d = -f 1 | xargs -n1 pip install -U + +# Files ####################################################################### + +js_external: + $(MAKE) -C static/js/external diff --git a/README.md b/README.md index c7bc79d9e..44bd1a234 100644 --- a/README.md +++ b/README.md @@ -168,14 +168,18 @@ To run a single test, use `make test_one`: $ make test_one instance.tests.models.test_server ``` -You can also run prospector and the unit tests independently: +You can also run prospector, the unit tests, JS tests and integration independently: ``` $ make test_prospector $ make test_unit +$ make test_js $ make test_integration ``` +JS tests can be run in your browser for debugging (see `make test_js_web` and then go to +http://localhost:8888/ ), or in a CI manner via selenium and `jasmine-ci` (see `make test_js`). + Note that the integration tests aren't run by default, as they require a working OpenStack cluster configured. To run them, create a `.env.integration` file - your development environment is likely a good starting point: diff --git a/Vagrantfile b/Vagrantfile index 67d0ce6fa..6bf27b99e 100644 --- a/Vagrantfile +++ b/Vagrantfile @@ -54,8 +54,16 @@ Vagrant.configure(2) do |config| config.vm.box = 'ubuntu/trusty64' config.vm.network 'forwarded_port', guest: 2001, host: 2001 config.vm.network 'forwarded_port', guest: 5000, host: 5000 + config.vm.network 'forwarded_port', guest: 8888, host: 8888 + config.ssh.forward_x11 = true config.vm.provision 'shell', inline: PROVISION, privileged: false, keep_color: true + + config.vm.provider :virtualbox do |vb| + # Allow DNS to work for Ubuntu host + # http://askubuntu.com/questions/238040/how-do-i-fix-name-service-for-vagrant-client + vb.customize ["modifyvm", :id, "--natdnshostresolver1", "on"] + end end diff --git a/circle.yml b/circle.yml index a402f76ad..fdc40b303 100644 --- a/circle.yml +++ b/circle.yml @@ -4,6 +4,8 @@ machine: dependencies: pre: - pip install --upgrade pip + override: + - pip install -r requirements.txt test: override: - make test: diff --git a/debian_packages.lst b/debian_packages.lst index fd752643c..d3296a5d5 100644 --- a/debian_packages.lst +++ b/debian_packages.lst @@ -3,3 +3,4 @@ python3-pip libpq-dev python-dev redis-server +firefox diff --git a/instance/static/js/instance.js b/instance/static/js/src/instance.js similarity index 66% rename from instance/static/js/instance.js rename to instance/static/js/src/instance.js index 54e090781..dac8c2fc5 100644 --- a/instance/static/js/instance.js +++ b/instance/static/js/src/instance.js @@ -14,9 +14,9 @@ // You should have received a copy of the GNU Affero General Public License // along with this program. If not, see . +(function(){ "use strict"; - // App configuration ////////////////////////////////////////////////////////// var app = angular.module('InstanceApp', [ @@ -44,12 +44,11 @@ app.config(function($stateProvider, $urlRouterProvider, RestangularProvider) { url: "/", templateUrl: "/static/html/instance/index.html", controller: "Index" - }) + }); }); -// Services /////////////////////////////////////////////////////////////////// - +// Services app.factory('OpenCraftAPI', function(Restangular) { return Restangular.withConfig(function(RestangularConfigurer) { RestangularConfigurer.setBaseUrl('/api/v1'); @@ -57,48 +56,30 @@ app.factory('OpenCraftAPI', function(Restangular) { }); -// Function /////////////////////////////////////////////////////////////////// - -function updateInstanceList($scope, OpenCraftAPI) { - // Display loading message - $scope.loading = true; - - OpenCraftAPI.all("openedxinstance").getList().then(function(instanceList) { - console.log('Updating instance list', instanceList); - $scope.instanceList = instanceList; - - if($scope.selected.instance){ - var updated_instance = null; - _.each(instanceList, function(instance) { - if(instance.id === $scope.selected.instance.id) { - updated_instance = instance; - } - }); - $scope.selected.instance = updated_instance; - } - }, function(response) { - console.log('Error from server: ', response); - }).finally(function () { - $scope.loading = false; - }); -} - - // Controllers //////////////////////////////////////////////////////////////// app.controller("Index", ['$scope', 'Restangular', 'OpenCraftAPI', '$q', function ($scope, Restangular, OpenCraftAPI, $q) { - // Display loading message - $scope.loading = true; - // Selection - $scope.selected = Array(); + $scope.init = function() { + $scope.loading = true; + $scope.selected = {}; + + $scope.updateInstanceList(); + + // Init websockets + swampdragon.onChannelMessage($scope.handleChannelMessage); + swampdragon.ready(function() { + swampdragon.subscribe('notifier', 'notification', null); + swampdragon.subscribe('notifier', 'log', null); + }); + }; + $scope.select = function(selection_type, value) { $scope.selected[selection_type] = value; console.log('Selected ' + selection_type + ':', value); }; - // Reprovisioning $scope.provision = function(instance) { console.log('Provisioning instance', instance); instance.status = 'terminating'; @@ -107,18 +88,37 @@ app.controller("Index", ['$scope', 'Restangular', 'OpenCraftAPI', '$q', server.status = 'terminating'; } }); - instance.post("provision"); + return instance.post("provision"); }; - // Retrieve instance list - updateInstanceList($scope, OpenCraftAPI); + $scope.updateInstanceList = function() { + $scope.loading = true; // Display loading message - // Intialize websockets - swampdragon.onChannelMessage(function(channels, message) { + return OpenCraftAPI.all("openedxinstance").getList().then(function(instanceList) { + console.log('Updating instance list', instanceList); + $scope.instanceList = instanceList; + + if($scope.selected.instance){ + var updated_instance = null; + _.each(instanceList, function(instance) { + if(instance.id === $scope.selected.instance.id) { + updated_instance = instance; + } + }); + $scope.selected.instance = updated_instance; + } + }, function(response) { + console.log('Error from server: ', response); + }).finally(function () { + $scope.loading = false; + }); + }; + + $scope.handleChannelMessage = function(channels, message) { console.log('Received websocket message', channels, message.data); if(message.data.type === 'server_update') { - updateInstanceList($scope, OpenCraftAPI); + $scope.updateInstanceList(); } else if(message.data.type === 'instance_log') { if($scope.selected.instance && $scope.selected.instance.id === message.data.instance_id) { $scope.$apply(function(){ @@ -126,10 +126,10 @@ app.controller("Index", ['$scope', 'Restangular', 'OpenCraftAPI', '$q', }); } } - }); - swampdragon.ready(function() { - swampdragon.subscribe('notifier', 'notification', null); - swampdragon.subscribe('notifier', 'log', null); - }); + }; + + $scope.init(); } ]); + +})(); diff --git a/instance/templates/instance/index.html b/instance/templates/instance/index.html index 9dbe19378..48c384286 100644 --- a/instance/templates/instance/index.html +++ b/instance/templates/instance/index.html @@ -20,5 +20,5 @@ {% block body_js %} {% swampdragon_settings %} - + {% endblock body_js %} diff --git a/instance/tests/fixtures/api/instances_list.json b/instance/tests/fixtures/api/instances_list.json new file mode 100644 index 000000000..8188e82bb --- /dev/null +++ b/instance/tests/fixtures/api/instances_list.json @@ -0,0 +1,61 @@ +[ + { + "id": 2, + "api_url": "http://localhost:5000/api/v1/openedxinstance/2/", + "active_server_set": [ + { + "id": 23, + "api_url": "http://localhost:5000/api/v1/openstackserver/23/", + "created": "2015-08-20T06:40:18.835733Z", + "instance": "http://localhost:5000/api/v1/openedxinstance/2/", + "modified": "2015-08-20T12:12:34.781802Z", + "openstack_id": "f889c46a-975a-4ecb-acb8-b6c3c5f97c8e", + "status": "ready" + } + ], + "base_domain": "opencraft.com", + "branch_name": "master", + "commit_id": "24bca27b46a0e65c00628716a77d19a6c5aecd55", + "created": "2015-08-08T07:22:56.062796Z", + "domain": "tmp.sandbox.opencraft.com", + "email": "contact@example.com", + "github_base_url": "https://github.com/antoviaque/edx-platform", + "github_branch_url": "https://github.com/antoviaque/edx-platform/tree/master", + "log_text": "2015-08-08 07:23:16 [info] Terminate servers for instance tmp.sandbox - antoviaque/edx-platform/master (24bca27) (http://tmp.sandbox.opencraft.com/)...\n2015-08-08 07:23:16 [info] Start new server for instance tmp.sandbox - antoviaque/edx-platform/master (24bca27) (http://tmp.sandbox.opencraft.com/)...\n2015-08-08 07:23:18 [info] Waiting for IP assignment on server 3841c324-c004-4188-9d8c-f1071cd635c5...\n" + }, + { + "id": 1, + "api_url": "http://localhost:5000/api/v1/openedxinstance/1/", + "active_server_set": [ + { + "id": 19, + "api_url": "http://localhost:5000/api/v1/openstackserver/19/", + "created": "2015-08-18T05:48:55.163364Z", + "instance": "http://localhost:5000/api/v1/openedxinstance/1/", + "modified": "2015-08-18T05:48:56.360140Z", + "openstack_id": "c007b320-1643-4f95-838a-1467ab0927e7", + "status": "started" + } + ], + "base_domain": "opencraft.com", + "branch_name": "OC-791-course-timeline", + "commit_id": "40952060bc7573c5035620b985b211a24360f311", + "created": "2015-08-07T18:10:56.823929Z", + "domain": "pr2.sandbox.opencraft.com", + "email": "contact@example.com", + "github_base_url": "https://github.com/antoviaque/edx-platform", + "github_branch_url": "https://github.com/antoviaque/edx-platform/tree/OC-791-course-timeline", + "log_text": "2015-08-07 18:10:56 [info] Terminate servers for instance PR#2: Display start date, end date, and number of sections (weeks) on \"Course Info\" tab of instructor dashboard. (antoviaque) - antoviaque/edx-platform/OC-791-course-timeline (4095206) (http://pr2.sandbox.opencraft.com/)...\n", + "github_organization_name": "antoviaque", + "modified": "2015-08-18T05:48:54.356884Z", + "name": "PR#2: Display start date, end date, and number of sections (weeks) on \"Course Info\" tab of instructor dashboard. (antoviaque) - antoviaque/edx-platform/OC-791-course-timeline (4095206)", + "protocol": "http", + "repository_url": "https://github.com/antoviaque/edx-platform.git", + "status": "started", + "studio_url": "http://studio.pr2.sandbox.opencraft.com/", + "sub_domain": "pr2.sandbox", + "url": "http://pr2.sandbox.opencraft.com/", + "updates_feed": "https://github.com/antoviaque/edx-platform/commits/OC-791-course-timeline.atom", + "vars_str": "# System\nCOMMON_HOSTNAME: 'pr2.sandbox.opencraft.com'\n\n# edxapp\nEDXAPP_PLATFORM_NAME: \"PR#2: Display start date, end date, and number of sections (weeks) on "Course Info" tab of instructor dashboard. (antoviaque) - antoviaque/edx-platform/OC-791-course-timeline (4095206)\"\nEDXAPP_LMS_NGINX_PORT: 80\nEDXAPP_LMS_PREVIEW_NGINX_PORT: 80\nEDXAPP_CMS_NGINX_PORT: 80\nEDXAPP_SITE_NAME: 'pr2.sandbox.opencraft.com'\nEDXAPP_LMS_SITE_NAME: 'pr2.sandbox.opencraft.com'\nEDXAPP_LMS_BASE: 'pr2.sandbox.opencraft.com'\nEDXAPP_PREVIEW_LMS_BASE: 'pr2.sandbox.opencraft.com'\n\nEDXAPP_CMS_SITE_NAME: 'studio.pr2.sandbox.opencraft.com'\nEDXAPP_CMS_BASE: 'studio.pr2.sandbox.opencraft.com'\n\n# Forum environment settings\nFORUM_RACK_ENV: 'production'\nFORUM_SINATRA_ENV: 'production'\n\n# Emails\nEDXAPP_CONTACT_EMAIL: 'contact@example.com'\nEDXAPP_TECH_SUPPORT_EMAIL: 'contact@example.com'\nEDXAPP_BUGS_EMAIL: 'contact@example.com'\nEDXAPP_FEEDBACK_SUBMISSION_EMAIL: 'contact@example.com'\nEDXAPP_DEFAULT_FROM_EMAIL: 'contact@example.com'\nEDXAPP_DEFAULT_FEEDBACK_EMAIL: 'contact@example.com'\nEDXAPP_SERVER_EMAIL: 'contact@example.com'\nEDXAPP_BULK_EMAIL_DEFAULT_FROM_EMAIL: 'contact@example.com'\n\n# Misc\nEDXAPP_TIME_ZONE: 'UTC'\n\n# Security updates\nCOMMON_SECURITY_UPDATES: true\nSECURITY_UNATTENDED_UPGRADES: true\nSECURITY_UPDATE_ALL_PACKAGES: false\nSECURITY_UPGRADE_ON_ANSIBLE: true\n\n# Repositories URLs\nedx_ansible_source_repo: 'https://github.com/edx/configuration.git'\nedx_platform_repo: 'https://github.com/antoviaque/edx-platform.git'\n\n# Pin down dependencies to specific (known to be compatible) commits.\nedx_platform_version: '40952060bc7573c5035620b985b211a24360f311'\nconfiguration_version: 'master'\nforum_version: 'master'\nnotifier_version: 'master'\nxqueue_version: 'master'\ncerts_version: 'master'\n\n# Features\nEDXAPP_FEATURES:\n USE_MICROSITES: false\n AUTH_USE_OPENID: false\n ENABLE_DISCUSSION_SERVICE: true\n ENABLE_INSTRUCTOR_ANALYTICS: true\n ENABLE_INSTRUCTOR_EMAIL: true\n REQUIRE_COURSE_EMAIL_AUTH: false\n ENABLE_PEARSON_HACK_TEST: false\n SUBDOMAIN_BRANDING: false\n SUBDOMAIN_COURSE_LISTINGS: false\n PREVIEW_LMS_BASE: 'pr2.sandbox.opencraft.com'\n ENABLE_DJANGO_ADMIN_SITE: true\n ALLOW_ALL_ADVANCED_COMPONENTS: true\n\n\n" + } +] diff --git a/instance/tests/js/helpers/fixtures.js b/instance/tests/js/helpers/fixtures.js new file mode 100644 index 000000000..a212f99b8 --- /dev/null +++ b/instance/tests/js/helpers/fixtures.js @@ -0,0 +1,28 @@ +// OpenCraft -- tools to aid developing and hosting free software projects +// Copyright (C) 2015 OpenCraft +// +// This program is free software: you can redistribute it and/or modify +// it under the terms of the GNU Affero General Public License as +// published by the Free Software Foundation, either version 3 of the +// License, or (at your option) any later version. +// +// This program is distributed in the hope that it will be useful, +// but WITHOUT ANY WARRANTY; without even the implied warranty of +// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +// GNU Affero General Public License for more details. +// +// You should have received a copy of the GNU Affero General Public License +// along with this program. If not, see . + +(function(){ +"use strict"; + +// Functions ////////////////////////////////////////////////////////////////// + +// Load the JS object from a JSON fixture +jasmine.loadFixture = function(fixturePath) { + var fixtureJSON = jasmine.httpGET('/__src__/instance/tests/fixtures/' + fixturePath); + return JSON.parse(fixtureJSON); +}; + +})(); diff --git a/instance/tests/js/helpers/http.js b/instance/tests/js/helpers/http.js new file mode 100644 index 000000000..b97fc21dd --- /dev/null +++ b/instance/tests/js/helpers/http.js @@ -0,0 +1,41 @@ +// OpenCraft -- tools to aid developing and hosting free software projects +// Copyright (C) 2015 OpenCraft +// +// This program is free software: you can redistribute it and/or modify +// it under the terms of the GNU Affero General Public License as +// published by the Free Software Foundation, either version 3 of the +// License, or (at your option) any later version. +// +// This program is distributed in the hope that it will be useful, +// but WITHOUT ANY WARRANTY; without even the implied warranty of +// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +// GNU Affero General Public License for more details. +// +// You should have received a copy of the GNU Affero General Public License +// along with this program. If not, see . + +(function(){ +"use strict"; + +// Functions ////////////////////////////////////////////////////////////////// + +// Synchronously get a file via HTTP +jasmine.httpGET = function(path) { + path = path + "?" + new Date().getTime(); + + var xhr; + try { + xhr = new XMLHttpRequest(); + xhr.open("GET", path, false); + xhr.send(null); + } catch (e) { + throw new Error("couldn't fetch " + path + ": " + e); + } + if (xhr.status < 200 || xhr.status > 299) { + throw new Error("Could not load '" + path + "'."); + } + + return xhr.responseText; +}; + +})(); diff --git a/instance/tests/js/helpers/restangular.js b/instance/tests/js/helpers/restangular.js new file mode 100644 index 000000000..8e2ed993d --- /dev/null +++ b/instance/tests/js/helpers/restangular.js @@ -0,0 +1,45 @@ +// OpenCraft -- tools to aid developing and hosting free software projects +// Copyright (C) 2015 OpenCraft +// +// This program is free software: you can redistribute it and/or modify +// it under the terms of the GNU Affero General Public License as +// published by the Free Software Foundation, either version 3 of the +// License, or (at your option) any later version. +// +// This program is distributed in the hope that it will be useful, +// but WITHOUT ANY WARRANTY; without even the implied warranty of +// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +// GNU Affero General Public License for more details. +// +// You should have received a copy of the GNU Affero General Public License +// along with this program. If not, see . + +(function(){ +"use strict"; + +// Functions ////////////////////////////////////////////////////////////////// + +// Helpers for testing restangular calls - https://github.com/mgonto/restangular/issues/98 + +// Remove all Restangular/AngularJS added methods in order to use Jasmine toEqual between the +// retrieved resource and the model +jasmine.sanitizeRestangularOne = function(item) { + return _.omit(item, "route", "parentResource", "getList", "get", "post", "put", "remove", + "head", "trace", "options", "patch", "$get", "$save", "$query", "$remove", + "$delete", "$put", "$post", "$head", "$trace", "$options", "$patch", "$then", + "$resolved", "restangularCollection", "customOperation", "customGET", + "customPOST", "customPUT", "customDELETE", "customGETLIST", "$getList", + "$resolved", "restangularCollection", "one", "all", "doGET", "doPOST", + "doPUT", "doDELETE", "doGETLIST", "addRestangularMethod", "getRestangularUrl", + "getRequestedUrl", "clone", "reqParams", "withHttpConfig", "plain", + "restangularized", "several", "oneUrl", "allUrl", "fromServer", + "getParentList", "save"); +}; + + +// Apply "sanitizeRestangularOne" function to an array of items +jasmine.sanitizeRestangularAll = function(items) { + return _.map(items, jasmine.sanitizeRestangularOne); +}; + +})(); diff --git a/instance/tests/js/instance_app_spec.js b/instance/tests/js/instance_app_spec.js new file mode 100644 index 000000000..b2b493497 --- /dev/null +++ b/instance/tests/js/instance_app_spec.js @@ -0,0 +1,120 @@ +// OpenCraft -- tools to aid developing and hosting free software projects +// Copyright (C) 2015 OpenCraft +// +// This program is free software: you can redistribute it and/or modify +// it under the terms of the GNU Affero General Public License as +// published by the Free Software Foundation, either version 3 of the +// License, or (at your option) any later version. +// +// This program is distributed in the hope that it will be useful, +// but WITHOUT ANY WARRANTY; without even the implied warranty of +// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +// GNU Affero General Public License for more details. +// +// You should have received a copy of the GNU Affero General Public License +// along with this program. If not, see . + +(function(){ +"use strict"; + +// Tests ////////////////////////////////////////////////////////////////////// + +describe('Instance app', function () { + var httpBackend, + indexController, + instanceList, + OpenCraftAPI, + $scope; + + beforeEach(function() { + window.swampdragon = { + onChannelMessage: jasmine.createSpy(), + ready: jasmine.createSpy() + }; + angular.mock.module('restangular'); + angular.mock.module('InstanceApp'); + }); + + describe('Index controller', function() { + beforeEach(inject(function($controller, _$httpBackend_, $rootScope, _OpenCraftAPI_) { + $scope = $rootScope.$new(); + httpBackend = _$httpBackend_; + OpenCraftAPI = _OpenCraftAPI_; + + // Models + instanceList = jasmine.loadFixture('api/instances_list.json'); + httpBackend.whenGET('/api/v1/openedxinstance/').respond(instanceList); + + indexController = $controller('Index', {$scope: $scope, OpenCraftAPI: OpenCraftAPI}); + httpBackend.flush(); // Clear calls from the controller init + })); + + afterEach(function () { + httpBackend.verifyNoOutstandingExpectation(); + httpBackend.verifyNoOutstandingRequest(); + }); + + describe('$scope.select', function() { + it('select the instance', function() { + $scope.select('a', 'b'); + expect($scope.selected.a).toEqual('b'); + }); + }); + + describe('$scope.updateInstanceList', function() { + it('loads the instance list from the API on init', function() { + expect(jasmine.sanitizeRestangularAll($scope.instanceList)).toEqual(instanceList); + }); + + it('sets $scope.loading while making the ajax call', function() { + expect($scope.loading).not.toBeTruthy(); + $scope.updateInstanceList(); + expect($scope.loading).toBeTruthy(); + httpBackend.flush(); + expect($scope.loading).not.toBeTruthy(); + }); + + it('updates the selected instance when updating the list', function() { + $scope.instanceList[0].domain = 'old.example.com'; + $scope.select('instance', $scope.instanceList[0]); + expect($scope.selected.instance.domain).toEqual('old.example.com'); + + $scope.updateInstanceList(); + httpBackend.flush(); + expect($scope.selected.instance.domain).toEqual('tmp.sandbox.opencraft.com'); + }); + }); + + describe('$scope.provision', function() { + it('notifies backend and changes status to terminating', function() { + var instance = $scope.instanceList[0]; + expect(instance.active_server_set[0].status).toEqual('ready'); + httpBackend.expectPOST('/api/v1/openedxinstance/2/provision/').respond(''); + $scope.provision(instance); + httpBackend.flush(); + expect(instance.active_server_set[0].status).toEqual('terminating'); + }); + }); + + describe('$scope.handleChannelMessage', function() { + it('server_update', function() { + $scope.updateInstanceList = jasmine.createSpy(); + $scope.handleChannelMessage('notifier', {data: {type: 'server_update'}}); + expect($scope.updateInstanceList).toHaveBeenCalled(); + }); + + it('instance_log', function() { + $scope.select('instance', $scope.instanceList[0]); + expect($scope.selected.instance.log_text).not.toContain('### Added via websocket ###'); + $scope.handleChannelMessage('notifier', {data: { + type: 'instance_log', + instance_id: 2, + log_entry: '### Added via websocket ###' + }}); + expect($scope.selected.instance.log_text).toContain('### Added via websocket ###'); + }); + }); + }); +}); + +})(); diff --git a/instance/tests/js/spec/javascripts/support/jasmine.yml b/instance/tests/js/spec/javascripts/support/jasmine.yml new file mode 100644 index 000000000..06fa849fd --- /dev/null +++ b/instance/tests/js/spec/javascripts/support/jasmine.yml @@ -0,0 +1,85 @@ + +# src_files +# +# Return an array of filepaths relative to src_dir to include before jasmine specs. +# Default: [] +# +# EXAMPLE: +# +# src_files: +# - lib/source1.js +# - lib/source2.js +# - dist/**/*.js +# +src_files: + - static/js/external/underscore-min.js + - static/js/external/angular.min.js + - static/js/external/angular-route.min.js + - static/js/external/angular-ui-router.min.js + - static/js/dist/angular-foundation-tpls.js + - static/js/external/restangular.min.js + - static/js/external/angular-mocks.js + - static/js/external/jshint.js + - instance/static/js/src/*.js + +# stylesheets +# +# Return an array of stylesheet filepaths relative to src_dir to include before jasmine specs. +# Default: [] +# +# EXAMPLE: +# +# stylesheets: +# - css/style.css +# - stylesheets/*.css +# +stylesheets: + +# helpers +# +# Return an array of filepaths relative to spec_dir to include before jasmine specs. +# Default: ["helpers/**/*.js"] +# +# EXAMPLE: +# +# helpers: +# - helpers/**/*.js +# +helpers: + - "helpers/**/*.js" + +# spec_files +# +# Return an array of filepaths relative to spec_dir to include. +# Default: ["**/*[sS]pec.js"] +# +# EXAMPLE: +# +# spec_files: +# - **/*[sS]pec.js +# +spec_files: + - "**/*[Ss]pec.js" + + +# src_dir +# +# Source directory path. Your src_files must be returned relative to this path. Will use root if left blank. +# Default: project root +# +# EXAMPLE: +# +# src_dir: public +# +src_dir: ../../.. + +# spec_dir +# +# Spec directory path. Your spec_files must be returned relative to this path. +# Default: spec/javascripts +# +# EXAMPLE: +# +# spec_dir: spec/javascripts +# +spec_dir: . diff --git a/instance/tests/js/z_jshint_spec.js b/instance/tests/js/z_jshint_spec.js new file mode 100644 index 000000000..68b279d9c --- /dev/null +++ b/instance/tests/js/z_jshint_spec.js @@ -0,0 +1,34 @@ +// Copyright (C) 2011 GitHub - Brandon Keepers (https://github.com/bkeepers) + +// https://github.com/bkeepers/lucid/blob/master/spec/javascripts/lucid.aspects.spec.js + +(function(){ +"use strict"; + +// Tests ////////////////////////////////////////////////////////////////////// + +describe('JSHint', function () { + var options = {curly: true, white: true, indent: 2}, + files = /^.*\/js\/src\/.*\.js$/; + + _.each(document.getElementsByTagName('script'), function (element) { + var script = element.getAttribute('src'); + if (!files.test(script)) { + return; + } + + it(script, function () { + console.log('Running JSHint on script: ' + script); + var self = this; + var source = jasmine.httpGET(script); + var result = JSHINT(source, options); + _.each(JSHINT.errors, function (error) { + fail("line " + error.line + ' - ' + error.reason + ' - ' + error.evidence); + }); + expect(true).toBe(true); // force spec to show up if there are no errors + }); + + }); +}); + +})(); diff --git a/requirements.txt b/requirements.txt index 5e6de2438..55af7d338 100644 --- a/requirements.txt +++ b/requirements.txt @@ -1,5 +1,6 @@ astroid==1.3.8 Babel==2.0 +CherryPy==3.8.0 cliff==1.14.0 cmd2==0.6.8 cookies==2.2.1 @@ -22,9 +23,11 @@ django-zurb-foundation==5.5.0 djangorestframework==3.2.3 dodgy==0.1.7 factory-boy==2.5.2 +Flask==0.10.1 freezegun==0.3.5 gitdb==0.6.4 GitPython==1.0.1 +glob2==0.4.1 gunicorn==19.3.0 honcho==0.6.6 -e git+https://github.com/coleifer/huey.git@6a2e47485e2780ec190eef7d2ebe179b63a7c779#egg=huey-dev @@ -32,10 +35,15 @@ ipdb==0.8.1 ipython==4.0.0 ipython-genutils==0.1.0 iso8601==0.1.10 +itsdangerous==0.24 +jasmine==2.3.0 +jasmine-core==2.3.4 +Jinja2==2.8 jira==0.50 libsass==0.8.3 logilab-common==1.0.2 Markdown==2.6.2 +MarkupSafe==0.23 mccabe==0.3.1 mock==1.3.0 monotonic==0.3 @@ -70,13 +78,14 @@ python-keystoneclient==1.6.0 python-neutronclient==2.6.0 python-novaclient==2.26.0 pytz==2015.4 -PyYAML==3.11 +PyYAML==3.10 redis==2.10.3 requests==2.7.0 requests-oauthlib==0.5.0 requests-toolbelt==0.4.0 requirements-detector==0.4 responses==0.4.0 +selenium==2.47.3 setoptconf==0.2.0 simplegeneric==0.8.1 simplejson==3.8.0 diff --git a/static/js/dist/angular-foundation-tpls-0.6.0.min.js b/static/js/dist/angular-foundation-tpls-0.6.0.min.js deleted file mode 100644 index 3e03e71ef..000000000 --- a/static/js/dist/angular-foundation-tpls-0.6.0.min.js +++ /dev/null @@ -1,10 +0,0 @@ -/* - * angular-mm-foundation - * http://pineconellc.github.io/angular-foundation/ - - * Version: 0.6.0 - 2015-04-13 - * License: MIT - * (c) Pinecone, LLC - */ -angular.module("mm.foundation",["mm.foundation.tpls","mm.foundation.accordion","mm.foundation.alert","mm.foundation.bindHtml","mm.foundation.buttons","mm.foundation.position","mm.foundation.mediaQueries","mm.foundation.dropdownToggle","mm.foundation.interchange","mm.foundation.transition","mm.foundation.modal","mm.foundation.offcanvas","mm.foundation.pagination","mm.foundation.tooltip","mm.foundation.popover","mm.foundation.progressbar","mm.foundation.rating","mm.foundation.tabs","mm.foundation.topbar","mm.foundation.tour","mm.foundation.typeahead"]),angular.module("mm.foundation.tpls",["template/accordion/accordion-group.html","template/accordion/accordion.html","template/alert/alert.html","template/modal/backdrop.html","template/modal/window.html","template/pagination/pager.html","template/pagination/pagination.html","template/tooltip/tooltip-html-unsafe-popup.html","template/tooltip/tooltip-popup.html","template/popover/popover.html","template/progressbar/bar.html","template/progressbar/progress.html","template/progressbar/progressbar.html","template/rating/rating.html","template/tabs/tab.html","template/tabs/tabset.html","template/topbar/has-dropdown.html","template/topbar/toggle-top-bar.html","template/topbar/top-bar-dropdown.html","template/topbar/top-bar-section.html","template/topbar/top-bar.html","template/tour/tour.html","template/typeahead/typeahead-match.html","template/typeahead/typeahead-popup.html"]),angular.module("mm.foundation.accordion",[]).constant("accordionConfig",{closeOthers:!0}).controller("AccordionController",["$scope","$attrs","accordionConfig",function(a,b,c){this.groups=[],this.closeOthers=function(d){var e=angular.isDefined(b.closeOthers)?a.$eval(b.closeOthers):c.closeOthers;e&&angular.forEach(this.groups,function(a){a!==d&&(a.isOpen=!1)})},this.addGroup=function(a){var b=this;this.groups.push(a),a.$on("$destroy",function(){b.removeGroup(a)})},this.removeGroup=function(a){var b=this.groups.indexOf(a);-1!==b&&this.groups.splice(b,1)}}]).directive("accordion",function(){return{restrict:"EA",controller:"AccordionController",transclude:!0,replace:!1,templateUrl:"template/accordion/accordion.html"}}).directive("accordionGroup",["$parse",function(a){return{require:"^accordion",restrict:"EA",transclude:!0,replace:!0,templateUrl:"template/accordion/accordion-group.html",scope:{heading:"@"},controller:function(){this.setHeading=function(a){this.heading=a}},link:function(b,c,d,e){var f,g;e.addGroup(b),b.isOpen=!1,d.isOpen&&(f=a(d.isOpen),g=f.assign,b.$parent.$watch(f,function(a){b.isOpen=!!a})),b.$watch("isOpen",function(a){a&&e.closeOthers(b),g&&g(b.$parent,a)})}}}]).directive("accordionHeading",function(){return{restrict:"EA",transclude:!0,template:"",replace:!0,require:"^accordionGroup",compile:function(a,b,c){return function(a,b,d,e){e.setHeading(c(a,function(){}))}}}}).directive("accordionTransclude",function(){return{require:"^accordionGroup",link:function(a,b,c,d){a.$watch(function(){return d[c.accordionTransclude]},function(a){a&&(b.html(""),b.append(a))})}}}),angular.module("mm.foundation.alert",[]).controller("AlertController",["$scope","$attrs",function(a,b){a.closeable="close"in b}]).directive("alert",function(){return{restrict:"EA",controller:"AlertController",templateUrl:"template/alert/alert.html",transclude:!0,replace:!0,scope:{type:"=",close:"&"}}}),angular.module("mm.foundation.bindHtml",[]).directive("bindHtmlUnsafe",function(){return function(a,b,c){b.addClass("ng-binding").data("$binding",c.bindHtmlUnsafe),a.$watch(c.bindHtmlUnsafe,function(a){b.html(a||"")})}}),angular.module("mm.foundation.buttons",[]).constant("buttonConfig",{activeClass:"active",toggleEvent:"click"}).controller("ButtonsController",["buttonConfig",function(a){this.activeClass=a.activeClass,this.toggleEvent=a.toggleEvent}]).directive("btnRadio",function(){return{require:["btnRadio","ngModel"],controller:"ButtonsController",link:function(a,b,c,d){var e=d[0],f=d[1];f.$render=function(){b.toggleClass(e.activeClass,angular.equals(f.$modelValue,a.$eval(c.btnRadio)))},b.bind(e.toggleEvent,function(){b.hasClass(e.activeClass)||a.$apply(function(){f.$setViewValue(a.$eval(c.btnRadio)),f.$render()})})}}}).directive("btnCheckbox",function(){return{require:["btnCheckbox","ngModel"],controller:"ButtonsController",link:function(a,b,c,d){function e(){return g(c.btnCheckboxTrue,!0)}function f(){return g(c.btnCheckboxFalse,!1)}function g(b,c){var d=a.$eval(b);return angular.isDefined(d)?d:c}var h=d[0],i=d[1];i.$render=function(){b.toggleClass(h.activeClass,angular.equals(i.$modelValue,e()))},b.bind(h.toggleEvent,function(){a.$apply(function(){i.$setViewValue(b.hasClass(h.activeClass)?f():e()),i.$render()})})}}}),angular.module("mm.foundation.position",[]).factory("$position",["$document","$window",function(a,b){function c(a,c){return a.currentStyle?a.currentStyle[c]:b.getComputedStyle?b.getComputedStyle(a)[c]:a.style[c]}function d(a){return"static"===(c(a,"position")||"static")}var e=function(b){for(var c=a[0],e=b.offsetParent||c;e&&e!==c&&d(e);)e=e.offsetParent;return e||c};return{position:function(b){var c=this.offset(b),d={top:0,left:0},f=e(b[0]);f!=a[0]&&(d=this.offset(angular.element(f)),d.top+=f.clientTop-f.scrollTop,d.left+=f.clientLeft-f.scrollLeft);var g=b[0].getBoundingClientRect();return{width:g.width||b.prop("offsetWidth"),height:g.height||b.prop("offsetHeight"),top:c.top-d.top,left:c.left-d.left}},offset:function(c){var d=c[0].getBoundingClientRect();return{width:d.width||c.prop("offsetWidth"),height:d.height||c.prop("offsetHeight"),top:d.top+(b.pageYOffset||a[0].body.scrollTop||a[0].documentElement.scrollTop),left:d.left+(b.pageXOffset||a[0].body.scrollLeft||a[0].documentElement.scrollLeft)}}}}]),angular.module("mm.foundation.mediaQueries",[]).factory("matchMedia",["$document","$window",function(a,b){return b.matchMedia||function(a){var b,c=a.documentElement,d=c.firstElementChild||c.firstChild,e=a.createElement("body"),f=a.createElement("div");return f.id="mq-test-1",f.style.cssText="position:absolute;top:-100em",e.style.background="none",e.appendChild(f),function(a){return f.innerHTML='­',c.insertBefore(e,d),b=42===f.offsetWidth,c.removeChild(e),{matches:b,media:a}}}(a[0])}]).factory("mediaQueries",["$document","matchMedia",function(a,b){var c=angular.element(a[0].querySelector("head"));c.append(''),c.append(''),c.append(''),c.append('');var d=/^[\/\\'"]+|(;\s?})+|[\/\\'"]+$/g,e={topbar:getComputedStyle(c[0].querySelector("meta.foundation-mq-topbar")).fontFamily.replace(d,""),small:getComputedStyle(c[0].querySelector("meta.foundation-mq-small")).fontFamily.replace(d,""),medium:getComputedStyle(c[0].querySelector("meta.foundation-mq-medium")).fontFamily.replace(d,""),large:getComputedStyle(c[0].querySelector("meta.foundation-mq-large")).fontFamily.replace(d,"")};return{topbarBreakpoint:function(){return!b(e.topbar).matches},small:function(){return b(e.small).matches},medium:function(){return b(e.medium).matches},large:function(){return b(e.large).matches}}}]),angular.module("mm.foundation.dropdownToggle",["mm.foundation.position","mm.foundation.mediaQueries"]).controller("DropdownToggleController",["$scope","$attrs","mediaQueries",function(a,b,c){this.small=function(){return c.small()&&!c.medium()}}]).directive("dropdownToggle",["$document","$window","$location","$position",function(a,b,c,d){var e=null,f=angular.noop;return{restrict:"CA",scope:{dropdownToggle:"@"},controller:"DropdownToggleController",link:function(c,g,h,i){var j=g.parent(),k=angular.element(a[0].querySelector(c.dropdownToggle)),l=function(){return j.hasClass("has-dropdown")},m=function(h){k=angular.element(a[0].querySelector(c.dropdownToggle));var m=g===e;if(h.preventDefault(),h.stopPropagation(),e&&f(),!m&&!g.hasClass("disabled")&&!g.prop("disabled")){k.css("display","block");var n=d.offset(g),o=d.offset(angular.element(k[0].offsetParent)),p=k.prop("offsetWidth"),q={top:n.top-o.top+n.height+"px"};if(i.small())q.left=Math.max((o.width-p)/2,8)+"px",q.position="absolute",q.width="95%",q["max-width"]="none";else{var r=Math.round(n.left-o.left),s=b.innerWidth-p-8;r>s&&(r=s,k.removeClass("left").addClass("right")),q.left=r+"px",q.position=null,q["max-width"]=null}k.css(q),g.addClass("expanded"),l()&&j.addClass("hover"),e=g,f=function(){a.off("click",f),k.css("display","none"),g.removeClass("expanded"),f=angular.noop,e=null,j.hasClass("hover")&&j.removeClass("hover")},a.on("click",f)}};k&&k.css("display","none"),c.$watch("$location.path",function(){f()}),g.on("click",m),g.on("$destroy",function(){g.off("click",m)})}}}]),angular.module("mm.foundation.interchange",["mm.foundation.mediaQueries"]).factory("interchangeQueries",["$document",function(a){for(var b,c,d={"default":"only screen",landscape:"only screen and (orientation: landscape)",portrait:"only screen and (orientation: portrait)",retina:"only screen and (-webkit-min-device-pixel-ratio: 2),only screen and (min--moz-device-pixel-ratio: 2),only screen and (-o-min-device-pixel-ratio: 2/1),only screen and (min-device-pixel-ratio: 2),only screen and (min-resolution: 192dpi),only screen and (min-resolution: 2dppx)"},e="foundation-mq-",f=["small","medium","large","xlarge","xxlarge"],g=angular.element(a[0].querySelector("head")),h=0;h'),b=getComputedStyle(g[0].querySelector("meta."+e+f[h])),c=b.fontFamily.replace(/^[\/\\'"]+|(;\s?})+|[\/\\'"]+$/g,""),d[f[h]]=c;return d}]).factory("interchangeQueriesManager",["interchangeQueries",function(a){return{add:function(b,c){return b&&c&&angular.isString(b)&&angular.isString(c)&&!a[b]?(a[b]=c,!0):!1}}}]).factory("interchangeTools",["$window","matchMedia","interchangeQueries",function(a,b,c){var d=function(a){for(var b,c=a.split(/\[(.*?)\]/),d=c.length,e=/^(.+)\,\ \((.+)\)$/,f={};d--;)c[d].replace(/[\W\d]+/,"").length>4&&(b=e.exec(c[d]),b&&3===b.length&&(f[b[2]]=b[1]));return f},e=function(a){var d,e,f;for(d in a)if(e=c[d]||d,f=b(e),f.matches)return a[d]};return{parseAttribute:d,findCurrentMediaFile:e}}]).directive("interchange",["$window","$rootScope","interchangeTools",function(a,b,c){var d=/[A-Za-z0-9-_]+\.(jpg|jpeg|png|gif|bmp|tiff)\ *,/i;return{restrict:"A",scope:!0,priority:450,compile:function(e,f){return"DIV"!==e[0].nodeName||d.test(f.interchange)||e.html(''),{pre:function(){},post:function(d,e,f){var g,h;switch(h=e&&e[0]&&e[0].nodeName,d.fileMap=c.parseAttribute(f.interchange),h){case"DIV":g=c.findCurrentMediaFile(d.fileMap),d.type=/[A-Za-z0-9-_]+\.(jpg|jpeg|png|gif|bmp|tiff)$/i.test(g)?"background":"include";break;case"IMG":d.type="image";break;default:return}var i=function(a){var f=c.findCurrentMediaFile(d.fileMap);if(!d.currentFile||d.currentFile!==f){switch(d.currentFile=f,d.type){case"image":e.attr("src",d.currentFile);break;case"background":e.css("background-image","url("+d.currentFile+")")}b.$emit("replace",e,d),a&&d.$apply()}};i(),a.addEventListener("resize",i),d.$on("$destroy",function(){a.removeEventListener("resize",i)})}}}}}]),angular.module("mm.foundation.transition",[]).factory("$transition",["$q","$timeout","$rootScope",function(a,b,c){function d(a){for(var b in a)if(void 0!==f.style[b])return a[b]}var e=function(d,f,g){g=g||{};var h=a.defer(),i=e[g.animation?"animationEndEventName":"transitionEndEventName"],j=function(){c.$apply(function(){d.unbind(i,j),h.resolve(d)})};return i&&d.bind(i,j),b(function(){angular.isString(f)?d.addClass(f):angular.isFunction(f)?f(d):angular.isObject(f)&&d.css(f),i||h.resolve(d)}),h.promise.cancel=function(){i&&d.unbind(i,j),h.reject("Transition cancelled")},h.promise},f=document.createElement("trans"),g={WebkitTransition:"webkitTransitionEnd",MozTransition:"transitionend",OTransition:"oTransitionEnd",transition:"transitionend"},h={WebkitTransition:"webkitAnimationEnd",MozTransition:"animationend",OTransition:"oAnimationEnd",transition:"animationend"};return e.transitionEndEventName=d(g),e.animationEndEventName=d(h),e}]),angular.module("mm.foundation.modal",["mm.foundation.transition"]).factory("$$stackedMap",function(){return{createNew:function(){var a=[];return{add:function(b,c){a.push({key:b,value:c})},get:function(b){for(var c=0;c0?c[0].querySelectorAll("[autofocus]")[0].focus():c[0].focus()})}}}]).factory("$modalStack",["$window","$transition","$timeout","$document","$compile","$rootScope","$$stackedMap",function(a,b,c,d,e,f,g){function h(){for(var a=-1,b=o.keys(),c=0;c0),j()})}function j(){if(l&&-1==h()){var a=m;k(l,m,150,function(){a.$destroy(),a=null}),l=void 0,m=void 0}}function k(a,d,e,f){function g(){g.done||(g.done=!0,a.remove(),f&&f())}d.animate=!1;var h=b.transitionEndEventName;if(h){var i=c(g,e);a.bind(h,function(){c.cancel(i),g(),d.$apply()})}else c(g,0)}var l,m,n="modal-open",o=g.createNew(),p={};return f.$watch(h,function(a){m&&(m.index=a)}),d.bind("keydown",function(a){var b;27===a.which&&(b=o.top(),b&&b.value.keyboard&&f.$apply(function(){p.dismiss(b.key)}))}),p.open=function(b,c){o.add(b,{deferred:c.deferred,modalScope:c.scope,backdrop:c.backdrop,keyboard:c.keyboard});var g=d.find("body").eq(0),i=h();i>=0&&!l&&(m=f.$new(!0),m.index=i,l=e("
")(m),g.append(l));var j=angular.element('
');g.append(j[0]);var k=parseInt(getComputedStyle(j[0]).top)||0;j.remove();var p=a.pageYOffset||0,q=p+k,r=angular.element('
').attr({"window-class":c.windowClass,index:o.length()-1,animate:"animate"});r.html(c.content);var s=e(r)(c.scope);o.top().value.modalDomEl=s,g.append(s),g.addClass(n)},p.close=function(a,b){var c=o.get(a).value;c&&(c.deferred.resolve(b),i(a))},p.dismiss=function(a,b){var c=o.get(a).value;c&&(c.deferred.reject(b),i(a))},p.dismissAll=function(a){for(var b=this.getTop();b;)this.dismiss(b.key,a),b=this.getTop()},p.getTop=function(){return o.top()},p}]).provider("$modal",function(){var a={options:{backdrop:!0,keyboard:!0},$get:["$injector","$rootScope","$q","$http","$templateCache","$controller","$modalStack",function(b,c,d,e,f,g,h){function i(a){return a.template?d.when(a.template):e.get(a.templateUrl,{cache:f}).then(function(a){return a.data})}function j(a){var c=[];return angular.forEach(a,function(a){(angular.isFunction(a)||angular.isArray(a))&&c.push(d.when(b.invoke(a)))}),c}var k={};return k.open=function(b){var e=d.defer(),f=d.defer(),k={result:e.promise,opened:f.promise,close:function(a){h.close(k,a)},dismiss:function(a){h.dismiss(k,a)}};if(b=angular.extend({},a.options,b),b.resolve=b.resolve||{},!b.template&&!b.templateUrl)throw new Error("One of template or templateUrl options is required.");var l=d.all([i(b)].concat(j(b.resolve)));return l.then(function(a){var d=(b.scope||c).$new();d.$close=k.close,d.$dismiss=k.dismiss;var f,i={},j=1;b.controller&&(i.$scope=d,i.$modalInstance=k,angular.forEach(b.resolve,function(b,c){i[c]=a[j++]}),f=g(b.controller,i),b.controllerAs&&(d[b.controllerAs]=f)),h.open(k,{scope:d,deferred:e,content:a[0],backdrop:b.backdrop,keyboard:b.keyboard,windowClass:b.windowClass})},function(a){e.reject(a)}),l.then(function(){f.resolve(!0)},function(){f.reject(!1)}),k},k}]};return a}),angular.module("mm.foundation.offcanvas",[]).directive("offCanvasWrap",["$window",function(a){return{scope:{},restrict:"C",link:function(b,c){var d=angular.element(a),e=b.sidebar=c;b.hide=function(){e.removeClass("move-left"),e.removeClass("move-right")},d.bind("resize.body",b.hide),b.$on("$destroy",function(){d.unbind("resize.body",b.hide)})},controller:["$scope",function(a){this.leftToggle=function(){a.sidebar.toggleClass("move-right")},this.rightToggle=function(){a.sidebar.toggleClass("move-left")},this.hide=function(){a.hide()}}]}}]).directive("leftOffCanvasToggle",[function(){return{require:"^offCanvasWrap",restrict:"C",link:function(a,b,c,d){b.on("click",function(){d.leftToggle()})}}}]).directive("rightOffCanvasToggle",[function(){return{require:"^offCanvasWrap",restrict:"C",link:function(a,b,c,d){b.on("click",function(){d.rightToggle()})}}}]).directive("exitOffCanvas",[function(){return{require:"^offCanvasWrap",restrict:"C",link:function(a,b,c,d){b.on("click",function(){d.hide()})}}}]).directive("offCanvasList",[function(){return{require:"^offCanvasWrap",restrict:"C",link:function(a,b,c,d){b.on("click",function(){d.hide()})}}}]),angular.module("mm.foundation.pagination",[]).controller("PaginationController",["$scope","$attrs","$parse","$interpolate",function(a,b,c,d){var e=this,f=b.numPages?c(b.numPages).assign:angular.noop;this.init=function(d){b.itemsPerPage?a.$parent.$watch(c(b.itemsPerPage),function(b){e.itemsPerPage=parseInt(b,10),a.totalPages=e.calculateTotalPages()}):this.itemsPerPage=d},this.noPrevious=function(){return 1===this.page},this.noNext=function(){return this.page===a.totalPages},this.isActive=function(a){return this.page===a},this.calculateTotalPages=function(){var b=this.itemsPerPage<1?1:Math.ceil(a.totalItems/this.itemsPerPage);return Math.max(b||0,1)},this.getAttributeValue=function(b,c,e){return angular.isDefined(b)?e?d(b)(a.$parent):a.$parent.$eval(b):c},this.render=function(){this.page=parseInt(a.page,10)||1,this.page>0&&this.page<=a.totalPages&&(a.pages=this.getPages(this.page,a.totalPages))},a.selectPage=function(b){!e.isActive(b)&&b>0&&b<=a.totalPages&&(a.page=b,a.onSelectPage({page:b}))},a.$watch("page",function(){e.render()}),a.$watch("totalItems",function(){a.totalPages=e.calculateTotalPages()}),a.$watch("totalPages",function(b){f(a.$parent,b),e.page>b?a.selectPage(b):e.render()})}]).constant("paginationConfig",{itemsPerPage:10,boundaryLinks:!1,directionLinks:!0,firstText:"First",previousText:"Previous",nextText:"Next",lastText:"Last",rotate:!0}).directive("pagination",["$parse","paginationConfig",function(a,b){return{restrict:"EA",scope:{page:"=",totalItems:"=",onSelectPage:" &"},controller:"PaginationController",templateUrl:"template/pagination/pagination.html",replace:!0,link:function(c,d,e,f){function g(a,b,c,d){return{number:a,text:b,active:c,disabled:d}}var h,i=f.getAttributeValue(e.boundaryLinks,b.boundaryLinks),j=f.getAttributeValue(e.directionLinks,b.directionLinks),k=f.getAttributeValue(e.firstText,b.firstText,!0),l=f.getAttributeValue(e.previousText,b.previousText,!0),m=f.getAttributeValue(e.nextText,b.nextText,!0),n=f.getAttributeValue(e.lastText,b.lastText,!0),o=f.getAttributeValue(e.rotate,b.rotate);f.init(b.itemsPerPage),e.maxSize&&c.$parent.$watch(a(e.maxSize),function(a){h=parseInt(a,10),f.render()}),f.getPages=function(a,b){var c=[],d=1,e=b,p=angular.isDefined(h)&&b>h;p&&(o?(d=Math.max(a-Math.floor(h/2),1),e=d+h-1,e>b&&(e=b,d=e-h+1)):(d=(Math.ceil(a/h)-1)*h+1,e=Math.min(d+h-1,b)));for(var q=d;e>=q;q++){var r=g(q,q,f.isActive(q),!1);c.push(r)}if(p&&!o){if(d>1){var s=g(d-1,"...",!1,!1);c.unshift(s)}if(b>e){var t=g(e+1,"...",!1,!1);c.push(t)}}if(j){var u=g(a-1,l,!1,f.noPrevious());c.unshift(u);var v=g(a+1,m,!1,f.noNext());c.push(v)}if(i){var w=g(1,k,!1,f.noPrevious());c.unshift(w);var x=g(b,n,!1,f.noNext());c.push(x)}return c}}}}]).constant("pagerConfig",{itemsPerPage:10,previousText:"« Previous",nextText:"Next »",align:!0}).directive("pager",["pagerConfig",function(a){return{restrict:"EA",scope:{page:"=",totalItems:"=",onSelectPage:" &"},controller:"PaginationController",templateUrl:"template/pagination/pager.html",replace:!0,link:function(b,c,d,e){function f(a,b,c,d,e){return{number:a,text:b,disabled:c,previous:i&&d,next:i&&e}}var g=e.getAttributeValue(d.previousText,a.previousText,!0),h=e.getAttributeValue(d.nextText,a.nextText,!0),i=e.getAttributeValue(d.align,a.align);e.init(a.itemsPerPage),e.getPages=function(a){return[f(a-1,g,e.noPrevious(),!0,!1),f(a+1,h,e.noNext(),!1,!0)]}}}}]),angular.module("mm.foundation.tooltip",["mm.foundation.position","mm.foundation.bindHtml"]).provider("$tooltip",function(){function a(a){var b=/[A-Z]/g,c="-";return a.replace(b,function(a,b){return(b?c:"")+a.toLowerCase()})}var b={placement:"top",animation:!0,popupDelay:0},c={mouseenter:"mouseleave",click:"click",focus:"blur"},d={};this.options=function(a){angular.extend(d,a)},this.setTriggers=function(a){angular.extend(c,a)},this.$get=["$window","$compile","$timeout","$parse","$document","$position","$interpolate",function(e,f,g,h,i,j,k){return function(e,l,m){function n(a){var b=a||o.trigger||m,d=c[b]||b;return{show:b,hide:d}}var o=angular.extend({},b,d),p=a(e),q=k.startSymbol(),r=k.endSymbol(),s="
';return{restrict:"EA",scope:!0,compile:function(){var a=f(s);return function(b,c,d){function f(){b.tt_isOpen?m():k()}function k(){(!z||b.$eval(d[l+"Enable"]))&&(b.tt_popupDelay?(v=g(p,b.tt_popupDelay,!1),v.then(function(a){a()})):p()())}function m(){b.$apply(function(){q()})}function p(){return b.tt_content?(r(),u&&g.cancel(u),t.css({top:0,left:0,display:"block"}),w?i.find("body").append(t):c.after(t),A(),b.tt_isOpen=!0,b.$digest(),A):angular.noop}function q(){b.tt_isOpen=!1,g.cancel(v),b.tt_animation?u=g(s,500):s()}function r(){t&&s(),t=a(b,function(){}),b.$digest()}function s(){t&&(t.remove(),t=null)}var t,u,v,w=angular.isDefined(o.appendToBody)?o.appendToBody:!1,x=n(void 0),y=!1,z=angular.isDefined(d[l+"Enable"]),A=function(){var a,d,e,f;switch(a=w?j.offset(c):j.position(c),d=t.prop("offsetWidth"),e=t.prop("offsetHeight"),b.tt_placement){case"right":f={top:a.top+a.height/2-e/2,left:a.left+a.width+10};break;case"bottom":f={top:a.top+a.height+10,left:a.left};break;case"left":f={top:a.top+a.height/2-e/2,left:a.left-d-10};break;default:f={top:a.top-e-10,left:a.left}}f.top+="px",f.left+="px",t.css(f)};b.tt_isOpen=!1,d.$observe(e,function(a){b.tt_content=a,!a&&b.tt_isOpen&&q()}),d.$observe(l+"Title",function(a){b.tt_title=a}),d[l+"Placement"]=d[l+"Placement"]||null,d.$observe(l+"Placement",function(a){b.tt_placement=angular.isDefined(a)&&a?a:o.placement}),d[l+"PopupDelay"]=d[l+"PopupDelay"]||null,d.$observe(l+"PopupDelay",function(a){var c=parseInt(a,10);b.tt_popupDelay=isNaN(c)?o.popupDelay:c});var B=function(){y&&(angular.isFunction(x.show)?C():(c.unbind(x.show,k),c.unbind(x.hide,m)))},C=function(){};d[l+"Trigger"]=d[l+"Trigger"]||null,d.$observe(l+"Trigger",function(a){B(),C(),x=n(a),angular.isFunction(x.show)?C=b.$watch(function(){return x.show(b,c,d)},function(a){return g(a?p:q)}):x.show===x.hide?c.bind(x.show,f):(c.bind(x.show,k),c.bind(x.hide,m)),y=!0});var D=b.$eval(d[l+"Animation"]);b.tt_animation=angular.isDefined(D)?!!D:o.animation,d.$observe(l+"AppendToBody",function(a){w=angular.isDefined(a)?h(a)(b):w}),w&&b.$on("$locationChangeSuccess",function(){b.tt_isOpen&&q()}),b.$on("$destroy",function(){g.cancel(u),g.cancel(v),B(),C(),s()})}}}}}]}).directive("tooltipPopup",function(){return{restrict:"EA",replace:!0,scope:{content:"@",placement:"@",animation:"&",isOpen:"&"},templateUrl:"template/tooltip/tooltip-popup.html"}}).directive("tooltip",["$tooltip",function(a){return a("tooltip","tooltip","mouseenter")}]).directive("tooltipHtmlUnsafePopup",function(){return{restrict:"EA",replace:!0,scope:{content:"@",placement:"@",animation:"&",isOpen:"&"},templateUrl:"template/tooltip/tooltip-html-unsafe-popup.html"}}).directive("tooltipHtmlUnsafe",["$tooltip",function(a){return a("tooltipHtmlUnsafe","tooltip","mouseenter")}]),angular.module("mm.foundation.popover",["mm.foundation.tooltip"]).directive("popoverPopup",function(){return{restrict:"EA",replace:!0,scope:{title:"@",content:"@",placement:"@",animation:"&",isOpen:"&"},templateUrl:"template/popover/popover.html"}}).directive("popover",["$tooltip",function(a){return a("popover","popover","click")}]),angular.module("mm.foundation.progressbar",["mm.foundation.transition"]).constant("progressConfig",{animate:!0,max:100}).controller("ProgressController",["$scope","$attrs","progressConfig","$transition",function(a,b,c,d){var e=this,f=[],g=angular.isDefined(b.max)?a.$parent.$eval(b.max):c.max,h=angular.isDefined(b.animate)?a.$parent.$eval(b.animate):c.animate;this.addBar=function(a,b){var c=0,d=a.$parent.$index;angular.isDefined(d)&&f[d]&&(c=f[d].value),f.push(a),this.update(b,a.value,c),a.$watch("value",function(a,c){a!==c&&e.update(b,a,c)}),a.$on("$destroy",function(){e.removeBar(a)})},this.update=function(a,b,c){var e=this.getPercentage(b);h?(a.css("width",this.getPercentage(c)+"%"),d(a,{width:e+"%"})):a.css({transition:"none",width:e+"%"})},this.removeBar=function(a){f.splice(f.indexOf(a),1)},this.getPercentage=function(a){return Math.round(100*a/g)}}]).directive("progress",function(){return{restrict:"EA",replace:!0,transclude:!0,controller:"ProgressController",require:"progress",scope:{},template:'
'}}).directive("bar",function(){return{restrict:"EA",replace:!0,transclude:!0,require:"^progress",scope:{value:"=",type:"@"},templateUrl:"template/progressbar/bar.html",link:function(a,b,c,d){d.addBar(a,b)}}}).directive("progressbar",function(){return{restrict:"EA",replace:!0,transclude:!0,controller:"ProgressController",scope:{value:"=",type:"@"},templateUrl:"template/progressbar/progressbar.html",link:function(a,b,c,d){d.addBar(a,angular.element(b.children()[0]))}}}),angular.module("mm.foundation.rating",[]).constant("ratingConfig",{max:5,stateOn:null,stateOff:null}).controller("RatingController",["$scope","$attrs","$parse","ratingConfig",function(a,b,c,d){this.maxRange=angular.isDefined(b.max)?a.$parent.$eval(b.max):d.max,this.stateOn=angular.isDefined(b.stateOn)?a.$parent.$eval(b.stateOn):d.stateOn,this.stateOff=angular.isDefined(b.stateOff)?a.$parent.$eval(b.stateOff):d.stateOff,this.createRateObjects=function(a){for(var b={stateOn:this.stateOn,stateOff:this.stateOff},c=0,d=a.length;d>c;c++)a[c]=angular.extend({index:c},b,a[c]);return a},a.range=this.createRateObjects(angular.isDefined(b.ratingStates)?angular.copy(a.$parent.$eval(b.ratingStates)):new Array(this.maxRange)),a.rate=function(b){a.value===b||a.readonly||(a.value=b)},a.enter=function(b){a.readonly||(a.val=b),a.onHover({value:b})},a.reset=function(){a.val=angular.copy(a.value),a.onLeave()},a.$watch("value",function(b){a.val=b}),a.readonly=!1,b.readonly&&a.$parent.$watch(c(b.readonly),function(b){a.readonly=!!b})}]).directive("rating",function(){return{restrict:"EA",scope:{value:"=",onHover:"&",onLeave:"&"},controller:"RatingController",templateUrl:"template/rating/rating.html",replace:!0}}),angular.module("mm.foundation.tabs",[]).controller("TabsetController",["$scope",function(a){var b=this,c=b.tabs=a.tabs=[];b.select=function(a){angular.forEach(c,function(a){a.active=!1}),a.active=!0},b.addTab=function(a){c.push(a),(1===c.length||a.active)&&b.select(a)},b.removeTab=function(a){var d=c.indexOf(a);if(a.active&&c.length>1){var e=d==c.length-1?d-1:d+1;b.select(c[e])}c.splice(d,1)}}]).directive("tabset",function(){return{restrict:"EA",transclude:!0,replace:!0,scope:{},controller:"TabsetController",templateUrl:"template/tabs/tabset.html",link:function(a,b,c){a.vertical=angular.isDefined(c.vertical)?a.$parent.$eval(c.vertical):!1,a.justified=angular.isDefined(c.justified)?a.$parent.$eval(c.justified):!1,a.type=angular.isDefined(c.type)?a.$parent.$eval(c.type):"tabs"}}}).directive("tab",["$parse",function(a){return{require:"^tabset",restrict:"EA",replace:!0,templateUrl:"template/tabs/tab.html",transclude:!0,scope:{heading:"@",onSelect:"&select",onDeselect:"&deselect"},controller:function(){},compile:function(b,c,d){return function(b,c,e,f){var g,h;e.active?(g=a(e.active),h=g.assign,b.$parent.$watch(g,function(a,c){a!==c&&(b.active=!!a)}),b.active=g(b.$parent)):h=g=angular.noop,b.$watch("active",function(a){angular.isFunction(h)&&(h(b.$parent,a),a?(f.select(b),b.onSelect()):b.onDeselect())}),b.disabled=!1,e.disabled&&b.$parent.$watch(a(e.disabled),function(a){b.disabled=!!a}),b.select=function(){b.disabled||(b.active=!0)},f.addTab(b),b.$on("$destroy",function(){f.removeTab(b)}),b.$transcludeFn=d}}}}]).directive("tabHeadingTransclude",[function(){return{restrict:"A",require:"^tab",link:function(a,b){a.$watch("headingElement",function(a){a&&(b.html(""),b.append(a))})}}}]).directive("tabContentTransclude",function(){function a(a){return a.tagName&&(a.hasAttribute("tab-heading")||a.hasAttribute("data-tab-heading")||"tab-heading"===a.tagName.toLowerCase()||"data-tab-heading"===a.tagName.toLowerCase())}return{restrict:"A",require:"^tabset",link:function(b,c,d){var e=b.$eval(d.tabContentTransclude);e.$transcludeFn(e.$parent,function(b){angular.forEach(b,function(b){a(b)?e.headingElement=b:c.append(b)})})}}}),angular.module("mm.foundation.topbar",["mm.foundation.mediaQueries"]).factory("closest",[function(){return function(a,b){for(var c=function(a,b){for(var c=(a.parentNode||a.document).querySelectorAll(b),d=-1;c[++d]&&c[d]!=a;);return!!c[d]},d=a[0];d;){if(c(d,b))return angular.element(d);d=d.parentElement}return!1}}]).directive("topBar",["$timeout","$compile","$window","$document","mediaQueries",function(a,b,c,d,e){return{scope:{stickyClass:"@",backText:"@",stickyOn:"=",customBackText:"=",isHover:"=",mobileShowParentLink:"=",scrolltop:"="},restrict:"EA",replace:!0,templateUrl:"template/topbar/top-bar.html",transclude:!0,controller:["$window","$scope","closest",function(b,c,f){c.settings={},c.settings.stickyClass=c.stickyClass||"sticky",c.settings.backText=c.backText||"Back",c.settings.stickyOn=c.stickyOn||"all",c.settings.customBackText=void 0===c.customBackText?!0:c.customBackText,c.settings.isHover=void 0===c.isHover?!0:c.isHover,c.settings.mobileShowParentLink=void 0===c.mobileShowParentLink?!0:c.mobileShowParentLink,c.settings.scrolltop=void 0===c.scrolltop?!0:c.scrolltop,this.settings=c.settings,c.index=0;var g=function(a){var b=a.offsetHeight,c=a.currentStyle||getComputedStyle(a);return b+=parseInt(c.marginTop,10)+parseInt(c.marginBottom,10)},h=[];this.addSection=function(a){h.push(a)},this.removeSection=function(a){var b=h.indexOf(a);b>-1&&h.splice(b,1)};var i=/rtl/i.test(d.find("html").attr("dir"))?"right":"left";c.$watch("index",function(a){for(var b=0;bb&&!g.hasClass("fixed")?(g.addClass("fixed"),h.css("padding-top",a.originalHeight+"px")):c.pageYOffset<=b&&g.hasClass("fixed")&&(g.removeClass("fixed"),h.css("padding-top",""))}},l=function(){var b=e.topbarBreakpoint();if(i!==b){i=e.topbarBreakpoint(),f.removeClass("expanded"),f.parent().removeClass("expanded"),a.height="";var c=angular.element(f[0].querySelectorAll("section"));angular.forEach(c,function(a){angular.element(a.querySelectorAll("li.moved")).removeClass("moved")}),a.$apply()}},m=function(){k(),a.$apply()};if(a.toggle=function(b){if(!e.topbarBreakpoint())return!1;var d=void 0===b?!f.hasClass("expanded"):b;d?f.addClass("expanded"):f.removeClass("expanded"),a.settings.scrolltop?!d&&f.hasClass("fixed")?(f.parent().addClass("fixed"),f.removeClass("fixed"),h.css("padding-top",a.originalHeight+"px")):d&&f.parent().hasClass("fixed")&&(f.parent().removeClass("fixed"),f.addClass("fixed"),h.css("padding-top",""),c.scrollTo(0,0)):(j()&&f.parent().addClass("fixed"),f.parent().hasClass("fixed")&&(d?(f.addClass("fixed"),f.parent().addClass("expanded"),h.css("padding-top",a.originalHeight+"px")):(f.removeClass("fixed"),f.parent().removeClass("expanded"),k())))},g.hasClass("fixed")||j()){a.stickyTopbar=!0,a.height=g[0].offsetHeight;var n=g[0].getBoundingClientRect().top+c.pageYOffset}else a.height=f[0].offsetHeight;a.originalHeight=a.height,a.$watch("height",function(a){a?f.css("height",a+"px"):f.css("height","")}),angular.element(c).bind("resize",l),angular.element(c).bind("scroll",m),a.$on("$destroy",function(){angular.element(c).unbind("scroll",l),angular.element(c).unbind("resize",m)}),g.hasClass("fixed")&&h.css("padding-top",a.originalHeight+"px")}}}]).directive("toggleTopBar",["closest",function(a){return{scope:{},require:"^topBar",restrict:"A",replace:!0,templateUrl:"template/topbar/toggle-top-bar.html",transclude:!0,link:function(b,c,d,e){c.bind("click",function(b){var c=a(angular.element(b.currentTarget),"li");c.hasClass("back")||c.hasClass("has-dropdown")||e.toggle()}),b.$on("$destroy",function(){c.unbind("click")})}}}]).directive("topBarSection",["$compile","closest",function(a,b){return{scope:{},require:"^topBar",restrict:"EA",replace:!0,templateUrl:"template/topbar/top-bar-section.html",transclude:!0,link:function(a,c,d,e){var f=c;a.reset=function(){angular.element(f[0].querySelectorAll("li.moved")).removeClass("moved")},a.move=function(a,b){f.css("left"===a?{left:-100*b+"%"}:{right:-100*b+"%"})},e.addSection(a),a.$on("$destroy",function(){e.removeSection(a)});var g=f[0].querySelectorAll("li>a");angular.forEach(g,function(c){var d=angular.element(c),f=b(d,"li");f.hasClass("has-dropdown")||f.hasClass("back")||f.hasClass("title")||(d.bind("click",function(){e.toggle(!1)}),a.$on("$destroy",function(){d.bind("click")}))})}}}]).directive("hasDropdown",["mediaQueries",function(a){return{scope:{},require:"^topBar",restrict:"A",templateUrl:"template/topbar/has-dropdown.html",replace:!0,transclude:!0,link:function(b,c,d,e){b.triggerLink=c.children("a")[0];var f=angular.element(b.triggerLink);f.bind("click",function(a){e.forward(a)}),b.$on("$destroy",function(){f.unbind("click")}),c.bind("mouseenter",function(){e.settings.isHover&&!a.topbarBreakpoint()&&c.addClass("not-click")}),c.bind("click",function(){e.settings.isHover||a.topbarBreakpoint()||c.toggleClass("not-click")}),c.bind("mouseleave",function(){c.removeClass("not-click")}),b.$on("$destroy",function(){c.unbind("click"),c.unbind("mouseenter"),c.unbind("mouseleave")})},controller:["$window","$scope",function(a,b){this.triggerLink=b.triggerLink}]}}]).directive("topBarDropdown",["$compile",function(a){return{scope:{},require:["^topBar","^hasDropdown"],restrict:"A",replace:!0,templateUrl:"template/topbar/top-bar-dropdown.html",transclude:!0,link:function(b,c,d,e){var f,g=e[0],h=e[1],i=angular.element(h.triggerLink),j=i.attr("href");b.linkText=i.text(),b.back=function(a){g.back(a)},b.backText=g.settings.customBackText?g.settings.backText:"« "+i.html(),f=angular.element(g.settings.mobileShowParentLink&&j&&j.length>1?'
  • {{backText}}
  • {{linkText}}
  • ':'
  • {{backText}}
  • '),a(f)(b),c.prepend(f)}}}]),angular.module("mm.foundation.tour",["mm.foundation.position","mm.foundation.tooltip"]).service("$tour",["$window",function(a){function b(){try{return parseInt(a.localStorage.getItem("mm.tour.step"),10)}catch(b){if("SecurityError"!==b.name)throw b}}function c(){try{a.localStorage.setItem("mm.tour.step",e)}catch(b){if("SecurityError"!==b.name)throw b}}function d(a){e=a,c()}var e=b(),f={};this.add=function(a,b){f[a]=b},this.has=function(a){return!!f[a]},this.isActive=function(){return e>0},this.current=function(a){return a?void d(e):e},this.start=function(){d(1)},this.next=function(){d(e+1)},this.end=function(){d(0)}}]).directive("stepTextPopup",["$tour",function(a){return{restrict:"EA",replace:!0,scope:{title:"@",content:"@",placement:"@",animation:"&",isOpen:"&"},templateUrl:"template/tour/tour.html",link:function(b,c){b.isLastStep=function(){return!a.has(a.current()+1)},b.endTour=function(){c.remove(),a.end()},b.nextStep=function(){c.remove(),a.next()},b.$on("$locationChangeSuccess",b.endTour)}}}]).directive("stepText",["$position","$tooltip","$tour","$window",function(a,b,c,d){function e(a){var b=a[0].getBoundingClientRect();return b.top>=0&&b.left>=0&&b.bottom<=d.innerHeight-80&&b.right<=d.innerWidth}function f(b,f,g){var h=parseInt(g.stepIndex,10);if(c.isActive()&&h&&(c.add(h,g),h===c.current())){if(!e(f)){var i=a.offset(f);d.scrollTo(0,i.top-d.innerHeight/2)}return!0}return!1}return b("stepText","step",f)}]),angular.module("mm.foundation.typeahead",["mm.foundation.position","mm.foundation.bindHtml"]).factory("typeaheadParser",["$parse",function(a){var b=/^\s*(.*?)(?:\s+as\s+(.*?))?\s+for\s+(?:([\$\w][\$\w\d]*))\s+in\s+(.*)$/;return{parse:function(c){var d=c.match(b);if(!d)throw new Error("Expected typeahead specification in form of '_modelValue_ (as _label_)? for _item_ in _collection_' but got '"+c+"'.");return{itemName:d[3],source:a(d[4]),viewMapper:a(d[2]||d[1]),modelMapper:a(d[1])}}}}]).directive("typeahead",["$compile","$parse","$q","$timeout","$document","$position","typeaheadParser",function(a,b,c,d,e,f,g){var h=[9,13,27,38,40];return{require:"ngModel",link:function(i,j,k,l){var m,n=i.$eval(k.typeaheadMinLength)||1,o=i.$eval(k.typeaheadWaitMs)||0,p=i.$eval(k.typeaheadEditable)!==!1,q=b(k.typeaheadLoading).assign||angular.noop,r=b(k.typeaheadOnSelect),s=k.typeaheadInputFormatter?b(k.typeaheadInputFormatter):void 0,t=k.typeaheadAppendToBody?b(k.typeaheadAppendToBody):!1,u=b(k.ngModel).assign,v=g.parse(k.typeahead),w=angular.element("
    ");w.attr({matches:"matches",active:"activeIdx",select:"select(activeIdx)",query:"query",position:"position"}),angular.isDefined(k.typeaheadTemplateUrl)&&w.attr("template-url",k.typeaheadTemplateUrl);var x=i.$new();i.$on("$destroy",function(){x.$destroy()});var y=function(){x.matches=[],x.activeIdx=-1},z=function(a){var b={$viewValue:a};q(i,!0),c.when(v.source(i,b)).then(function(c){if(a===l.$viewValue&&m){if(c.length>0){x.activeIdx=0,x.matches.length=0;for(var d=0;d=n?o>0?(A&&d.cancel(A),A=d(function(){z(a)},o)):z(a):(q(i,!1),y()),p?a:a?void l.$setValidity("editable",!1):(l.$setValidity("editable",!0),a)}),l.$formatters.push(function(a){var b,c,d={};return s?(d.$model=a,s(i,d)):(d[v.itemName]=a,b=v.viewMapper(i,d),d[v.itemName]=void 0,c=v.viewMapper(i,d),b!==c?b:a)}),x.select=function(a){var b,c,d={};d[v.itemName]=c=x.matches[a].model,b=v.modelMapper(i,d),u(i,b),l.$setValidity("editable",!0),r(i,{$item:c,$model:b,$label:v.viewMapper(i,d)}),y(),j[0].focus()},j.bind("keydown",function(a){0!==x.matches.length&&-1!==h.indexOf(a.which)&&(a.preventDefault(),40===a.which?(x.activeIdx=(x.activeIdx+1)%x.matches.length,x.$digest()):38===a.which?(x.activeIdx=(x.activeIdx?x.activeIdx:x.matches.length)-1,x.$digest()):13===a.which||9===a.which?x.$apply(function(){x.select(x.activeIdx)}):27===a.which&&(a.stopPropagation(),y(),x.$digest()))}),j.bind("blur",function(){m=!1}),j.bind("focus",function(){m=!0});var B=function(a){j[0]!==a.target&&(y(),x.$digest())};e.bind("click",B),i.$on("$destroy",function(){e.unbind("click",B)});var C=a(w)(x);t?e.find("body").append(C):j.after(C)}}}]).directive("typeaheadPopup",function(){return{restrict:"EA",scope:{matches:"=",query:"=",active:"=",position:"=",select:"&"},replace:!0,templateUrl:"template/typeahead/typeahead-popup.html",link:function(a,b,c){a.templateUrl=c.templateUrl,a.isOpen=function(){return a.matches.length>0},a.isActive=function(b){return a.active==b},a.selectActive=function(b){a.active=b},a.selectMatch=function(b){a.select({activeIdx:b})}}}}).directive("typeaheadMatch",["$http","$templateCache","$compile","$parse",function(a,b,c,d){return{restrict:"EA",scope:{index:"=",match:"=",query:"="},link:function(e,f,g){var h=d(g.templateUrl)(e.$parent)||"template/typeahead/typeahead-match.html";a.get(h,{cache:b}).success(function(a){f.replaceWith(c(a.trim())(e))})}}}]).filter("typeaheadHighlight",function(){function a(a){return a.replace(/([.?*+^$[\]\\(){}|-])/g,"\\$1")}return function(b,c){return c?b.replace(new RegExp(a(c),"gi"),"$&"):b}}),angular.module("template/accordion/accordion-group.html",[]).run(["$templateCache",function(a){a.put("template/accordion/accordion-group.html",'
    \n {{heading}}\n
    \n
    \n')}]),angular.module("template/accordion/accordion.html",[]).run(["$templateCache",function(a){a.put("template/accordion/accordion.html",'
    \n')}]),angular.module("template/alert/alert.html",[]).run(["$templateCache",function(a){a.put("template/alert/alert.html","
    \n \n ×\n
    \n")}]),angular.module("template/modal/backdrop.html",[]).run(["$templateCache",function(a){a.put("template/modal/backdrop.html",'
    \n')}]),angular.module("template/modal/window.html",[]).run(["$templateCache",function(a){a.put("template/modal/window.html",'
    \n
    \n
    \n')}]),angular.module("template/pagination/pager.html",[]).run(["$templateCache",function(a){a.put("template/pagination/pager.html",'\n')}]),angular.module("template/pagination/pagination.html",[]).run(["$templateCache",function(a){a.put("template/pagination/pagination.html",'\n')}]),angular.module("template/tooltip/tooltip-html-unsafe-popup.html",[]).run(["$templateCache",function(a){a.put("template/tooltip/tooltip-html-unsafe-popup.html",'\n \n \n\n')}]),angular.module("template/tooltip/tooltip-popup.html",[]).run(["$templateCache",function(a){a.put("template/tooltip/tooltip-popup.html",'\n \n \n\n')}]),angular.module("template/popover/popover.html",[]).run(["$templateCache",function(a){a.put("template/popover/popover.html",'
    \n \n
    \n

    \n

    \n
    \n
    \n')}]),angular.module("template/progressbar/bar.html",[]).run(["$templateCache",function(a){a.put("template/progressbar/bar.html",'\n')}]),angular.module("template/progressbar/progress.html",[]).run(["$templateCache",function(a){a.put("template/progressbar/progress.html",'
    \n')}]),angular.module("template/progressbar/progressbar.html",[]).run(["$templateCache",function(a){a.put("template/progressbar/progressbar.html",'
    \n \n
    \n')}]),angular.module("template/rating/rating.html",[]).run(["$templateCache",function(a){a.put("template/rating/rating.html",'\n \n\n')}]),angular.module("template/tabs/tab.html",[]).run(["$templateCache",function(a){a.put("template/tabs/tab.html",'
    \n {{heading}}\n
    \n')}]),angular.module("template/tabs/tabset.html",[]).run(["$templateCache",function(a){a.put("template/tabs/tabset.html",'
    \n
    \n
    \n
    \n
    \n
    \n
    \n
    \n')}]),angular.module("template/topbar/has-dropdown.html",[]).run(["$templateCache",function(a){a.put("template/topbar/has-dropdown.html",'
  • ')}]),angular.module("template/topbar/toggle-top-bar.html",[]).run(["$templateCache",function(a){a.put("template/topbar/toggle-top-bar.html",'')}]),angular.module("template/topbar/top-bar-dropdown.html",[]).run(["$templateCache",function(a){a.put("template/topbar/top-bar-dropdown.html",'')}]),angular.module("template/topbar/top-bar-section.html",[]).run(["$templateCache",function(a){a.put("template/topbar/top-bar-section.html",'
    ')}]),angular.module("template/topbar/top-bar.html",[]).run(["$templateCache",function(a){a.put("template/topbar/top-bar.html",'')}]),angular.module("template/tour/tour.html",[]).run(["$templateCache",function(a){a.put("template/tour/tour.html",'
    \n \n
    \n

    \n

    \n Next\n End\n ×\n
    \n
    \n')}]),angular.module("template/typeahead/typeahead-match.html",[]).run(["$templateCache",function(a){a.put("template/typeahead/typeahead-match.html",'')}]),angular.module("template/typeahead/typeahead-popup.html",[]).run(["$templateCache",function(a){a.put("template/typeahead/typeahead-popup.html","
      \n"+'
    • \n
      \n
    • \n
    \n')}]); \ No newline at end of file diff --git a/static/js/dist/angular-foundation-tpls-0.6.0.js b/static/js/dist/angular-foundation-tpls.js similarity index 99% rename from static/js/dist/angular-foundation-tpls-0.6.0.js rename to static/js/dist/angular-foundation-tpls.js index 68daed18d..4bda0f62c 100644 --- a/static/js/dist/angular-foundation-tpls-0.6.0.js +++ b/static/js/dist/angular-foundation-tpls.js @@ -30,7 +30,7 @@ angular.module('mm.foundation.accordion', []) }); } }; - + // This is called from the accordion-group directive to add itself to the accordion this.addGroup = function(groupScope) { var that = this; @@ -83,7 +83,7 @@ angular.module('mm.foundation.accordion', []) accordionCtrl.addGroup(scope); scope.isOpen = false; - + if ( attrs.isOpen ) { getIsOpen = $parse(attrs.isOpen); setIsOpen = getIsOpen.assign; @@ -229,7 +229,7 @@ angular.module('mm.foundation.buttons', []) function getFalseValue() { return getCheckboxValue(attrs.btnCheckboxFalse, false); } - + function getCheckboxValue(attributeValue, defaultValue) { var val = scope.$eval(attributeValue); return angular.isDefined(val) ? val : defaultValue; @@ -1620,7 +1620,7 @@ angular.module( 'mm.foundation.tooltip', [ 'mm.foundation.position', 'mm.foundat // The options specified to the provider globally. var globalOptions = {}; - + /** * `options({})` allows global configuration of all tooltips in the * application. @@ -1689,7 +1689,7 @@ angular.module( 'mm.foundation.tooltip', [ 'mm.foundation.position', 'mm.foundat var startSym = $interpolate.startSymbol(); var endSym = $interpolate.endSymbol(); - var template = + var template = '
    +# +# This program is free software: you can redistribute it and/or modify +# it under the terms of the GNU Affero General Public License as +# published by the Free Software Foundation, either version 3 of the +# License, or (at your option) any later version. +# +# This program is distributed in the hope that it will be useful, +# but WITHOUT ANY WARRANTY; without even the implied warranty of +# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +# GNU Affero General Public License for more details. +# +# You should have received a copy of the GNU Affero General Public License +# along with this program. If not, see . +# + +# Variables ################################################################### + +GET = wget -qO +CDNJS = https://cdnjs.cloudflare.com/ajax/libs + + +# Files ####################################################################### + +all: angular.min.js angular.min.js.map angular-mocks.js angular-route.min.js angular-route.min.js.map \ + angular-ui-router.min.js jquery.min.js jshint.js restangular.min.js underscore-min.js \ + underscore-min.map + +angular.min.js: + $(GET) $@ $(CDNJS)/angular.js/1.4.6/angular.min.js + +angular.min.js.map: + $(GET) $@ $(CDNJS)/angular.js/1.4.6/angular.min.js.map + +angular-mocks.js: + $(GET) $@ $(CDNJS)/angular.js/1.4.6/angular-mocks.js + +angular-route.min.js: + $(GET) $@ $(CDNJS)/angular.js/1.4.6/angular-route.min.js + +angular-route.min.js.map: + $(GET) $@ $(CDNJS)/angular.js/1.4.6/angular-route.min.js.map + +angular-ui-router.min.js: + $(GET) $@ $(CDNJS)/angular-ui-router/0.2.15/angular-ui-router.min.js + +jquery.min.js: + $(GET) $@ $(CDNJS)/jquery/2.1.4/jquery.min.js + +jshint.js: + $(GET) $@ $(CDNJS)/jshint/2.8.0/jshint.js + +restangular.min.js: + $(GET) $@ $(CDNJS)/restangular/1.5.1/restangular.min.js + +underscore-min.js: + $(GET) $@ $(CDNJS)/underscore.js/1.8.3/underscore-min.js + +underscore-min.map: + $(GET) $@ $(CDNJS)/underscore.js/1.8.3/underscore-min.map diff --git a/templates/base.html b/templates/base.html index 473ac5970..c68d95ef1 100644 --- a/templates/base.html +++ b/templates/base.html @@ -23,7 +23,7 @@ {# Underscore #} {% block underscore_js %} - + {% endblock underscore_js %} {# Modernizr #} @@ -33,10 +33,10 @@ {# Angular #} {% block angular_js %} - - - - + + + + {% endblock angular_js %} {# Custom head JS #} @@ -58,12 +58,12 @@ {# jQuery #} {% block jquery_js %} - + {% endblock jquery_js %} {# Foundation JS #} {% block angular_foundation_js %} - + {% endblock angular_foundation_js %} {# Custom JS #}