├── utils ├── createUDC.bat ├── checkHostName.bat ├── checkUDCExist.bat ├── checkVPExist.bat ├── restartService.bat ├── stopService.bat ├── convertExcel.bat ├── startService.bat ├── createVirtualProxy.bat ├── createServiceDispatcher.bat ├── convertXlsx.js ├── createVP.json ├── checkHostName.js ├── checkVPExist.js ├── checkUDCExist.js ├── udcDef.json ├── udcDefTest.json ├── ServicesConfBuilder.ps1 ├── createUDC.js └── createVirtualProxy.js ├── docs ├── images │ ├── udc_csv.png │ ├── vp_form.png │ ├── udc_excel.png │ ├── gss │ │ ├── tags_menu.png │ │ ├── streams_menu.png │ │ ├── licensing_menu.png │ │ ├── security_rules_menu.png │ │ ├── streams_customprop.png │ │ ├── custom_properties_menu.png │ │ ├── licensing_configuration_login.png │ │ └── licensing_configuration_user.png │ ├── app_customprop.png │ ├── user_access_rule.png │ ├── vp_associated_items.png │ └── wiki │ │ └── serviceconfpowershell │ │ ├── 1.png │ │ ├── 2.png │ │ ├── 3.png │ │ ├── 4.png │ │ └── 5.png ├── Governed Self-Service Settings.xlsx └── gss_setup_guide.md ├── public ├── images │ ├── photos │ │ ├── e_reese.jpg │ │ ├── j_green.jpg │ │ ├── a_foster.jpg │ │ ├── a_wilson.jpg │ │ ├── e_hanson.jpg │ │ ├── j_thomas.jpg │ │ ├── l_denton.jpg │ │ ├── l_johnson.jpg │ │ ├── p_harris.jpg │ │ └── s_bowers.jpg │ ├── powertoolslogo.png │ └── clipart │ │ ├── a_foster.jpg │ │ ├── a_wilson.jpg │ │ ├── e_hanson.jpg │ │ ├── e_reese.jpg │ │ ├── j_green.jpg │ │ ├── j_thomas.jpg │ │ ├── l_denton.jpg │ │ ├── p_harris.jpg │ │ ├── s_bowers.jpg │ │ └── l_johnson.jpg ├── fonts │ ├── glyphicons-halflings-regular.eot │ ├── glyphicons-halflings-regular.ttf │ ├── glyphicons-halflings-regular.woff │ └── glyphicons-halflings-regular.woff2 ├── stylesheets │ └── style.css └── javascripts │ └── bootstrap.js ├── config ├── services.conf.cfg └── config.js ├── udc ├── app_paths.csv ├── iportal_users.csv └── iportal_attributes.csv ├── views ├── error.hbs ├── autherror.hbs ├── index.hbs └── layout.hbs ├── .gitattributes ├── package.json ├── lib ├── appPaths.js ├── parseUdcFiles.js ├── login.js ├── users.js └── qrsinteractions.js ├── .vscode └── launch.json ├── .gitignore ├── readme.md ├── app.js ├── server.js └── routes └── index.js /utils/createUDC.bat: -------------------------------------------------------------------------------- 1 | %1 %2 %3 -------------------------------------------------------------------------------- /utils/checkHostName.bat: -------------------------------------------------------------------------------- 1 | %1 %2 %3 %4 -------------------------------------------------------------------------------- /utils/checkUDCExist.bat: -------------------------------------------------------------------------------- 1 | %1 %2 %3 %4 %5 -------------------------------------------------------------------------------- /utils/checkVPExist.bat: -------------------------------------------------------------------------------- 1 | %1 %2 %3 %4 %5 -------------------------------------------------------------------------------- /utils/restartService.bat: -------------------------------------------------------------------------------- 1 | net stop QlikSenseServiceDispatcher 2 | net start QlikSenseServiceDispatcher -------------------------------------------------------------------------------- /utils/stopService.bat: -------------------------------------------------------------------------------- 1 | net stop QlikSenseServiceDispatcher 2 | net stop QlikEAPowerToolsServiceDispatcher -------------------------------------------------------------------------------- /utils/convertExcel.bat: -------------------------------------------------------------------------------- 1 | echo About to convert excel file to csv 2 | rem pause 3 | 4 | %1 %2 5 | rem pause -------------------------------------------------------------------------------- /utils/startService.bat: -------------------------------------------------------------------------------- 1 | net start QlikSenseServiceDispatcher 2 | net start QlikEAPowerToolsServiceDispatcher -------------------------------------------------------------------------------- /docs/images/udc_csv.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/eapowertools-archive/iPortal/HEAD/docs/images/udc_csv.png -------------------------------------------------------------------------------- /docs/images/vp_form.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/eapowertools-archive/iPortal/HEAD/docs/images/vp_form.png -------------------------------------------------------------------------------- /docs/images/udc_excel.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/eapowertools-archive/iPortal/HEAD/docs/images/udc_excel.png -------------------------------------------------------------------------------- /docs/images/gss/tags_menu.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/eapowertools-archive/iPortal/HEAD/docs/images/gss/tags_menu.png -------------------------------------------------------------------------------- /docs/images/app_customprop.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/eapowertools-archive/iPortal/HEAD/docs/images/app_customprop.png -------------------------------------------------------------------------------- /docs/images/gss/streams_menu.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/eapowertools-archive/iPortal/HEAD/docs/images/gss/streams_menu.png -------------------------------------------------------------------------------- /docs/images/user_access_rule.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/eapowertools-archive/iPortal/HEAD/docs/images/user_access_rule.png -------------------------------------------------------------------------------- /public/images/photos/e_reese.jpg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/eapowertools-archive/iPortal/HEAD/public/images/photos/e_reese.jpg -------------------------------------------------------------------------------- /public/images/photos/j_green.jpg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/eapowertools-archive/iPortal/HEAD/public/images/photos/j_green.jpg -------------------------------------------------------------------------------- /public/images/powertoolslogo.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/eapowertools-archive/iPortal/HEAD/public/images/powertoolslogo.png -------------------------------------------------------------------------------- /docs/images/gss/licensing_menu.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/eapowertools-archive/iPortal/HEAD/docs/images/gss/licensing_menu.png -------------------------------------------------------------------------------- /public/images/clipart/a_foster.jpg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/eapowertools-archive/iPortal/HEAD/public/images/clipart/a_foster.jpg -------------------------------------------------------------------------------- /public/images/clipart/a_wilson.jpg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/eapowertools-archive/iPortal/HEAD/public/images/clipart/a_wilson.jpg -------------------------------------------------------------------------------- /public/images/clipart/e_hanson.jpg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/eapowertools-archive/iPortal/HEAD/public/images/clipart/e_hanson.jpg -------------------------------------------------------------------------------- /public/images/clipart/e_reese.jpg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/eapowertools-archive/iPortal/HEAD/public/images/clipart/e_reese.jpg -------------------------------------------------------------------------------- /public/images/clipart/j_green.jpg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/eapowertools-archive/iPortal/HEAD/public/images/clipart/j_green.jpg -------------------------------------------------------------------------------- /public/images/clipart/j_thomas.jpg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/eapowertools-archive/iPortal/HEAD/public/images/clipart/j_thomas.jpg -------------------------------------------------------------------------------- /public/images/clipart/l_denton.jpg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/eapowertools-archive/iPortal/HEAD/public/images/clipart/l_denton.jpg -------------------------------------------------------------------------------- /public/images/clipart/p_harris.jpg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/eapowertools-archive/iPortal/HEAD/public/images/clipart/p_harris.jpg -------------------------------------------------------------------------------- /public/images/clipart/s_bowers.jpg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/eapowertools-archive/iPortal/HEAD/public/images/clipart/s_bowers.jpg -------------------------------------------------------------------------------- /public/images/photos/a_foster.jpg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/eapowertools-archive/iPortal/HEAD/public/images/photos/a_foster.jpg -------------------------------------------------------------------------------- /public/images/photos/a_wilson.jpg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/eapowertools-archive/iPortal/HEAD/public/images/photos/a_wilson.jpg -------------------------------------------------------------------------------- /public/images/photos/e_hanson.jpg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/eapowertools-archive/iPortal/HEAD/public/images/photos/e_hanson.jpg -------------------------------------------------------------------------------- /public/images/photos/j_thomas.jpg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/eapowertools-archive/iPortal/HEAD/public/images/photos/j_thomas.jpg -------------------------------------------------------------------------------- /public/images/photos/l_denton.jpg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/eapowertools-archive/iPortal/HEAD/public/images/photos/l_denton.jpg -------------------------------------------------------------------------------- /public/images/photos/l_johnson.jpg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/eapowertools-archive/iPortal/HEAD/public/images/photos/l_johnson.jpg -------------------------------------------------------------------------------- /public/images/photos/p_harris.jpg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/eapowertools-archive/iPortal/HEAD/public/images/photos/p_harris.jpg -------------------------------------------------------------------------------- /public/images/photos/s_bowers.jpg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/eapowertools-archive/iPortal/HEAD/public/images/photos/s_bowers.jpg -------------------------------------------------------------------------------- /docs/images/vp_associated_items.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/eapowertools-archive/iPortal/HEAD/docs/images/vp_associated_items.png -------------------------------------------------------------------------------- /public/images/clipart/l_johnson.jpg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/eapowertools-archive/iPortal/HEAD/public/images/clipart/l_johnson.jpg -------------------------------------------------------------------------------- /utils/createVirtualProxy.bat: -------------------------------------------------------------------------------- 1 | echo About to add a virtual proxy to your Qlik Sense installation 2 | rem pause 3 | 4 | %1 %2 5 | rem pause -------------------------------------------------------------------------------- /docs/images/gss/security_rules_menu.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/eapowertools-archive/iPortal/HEAD/docs/images/gss/security_rules_menu.png -------------------------------------------------------------------------------- /docs/images/gss/streams_customprop.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/eapowertools-archive/iPortal/HEAD/docs/images/gss/streams_customprop.png -------------------------------------------------------------------------------- /docs/Governed Self-Service Settings.xlsx: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/eapowertools-archive/iPortal/HEAD/docs/Governed Self-Service Settings.xlsx -------------------------------------------------------------------------------- /docs/images/gss/custom_properties_menu.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/eapowertools-archive/iPortal/HEAD/docs/images/gss/custom_properties_menu.png -------------------------------------------------------------------------------- /docs/images/wiki/serviceconfpowershell/1.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/eapowertools-archive/iPortal/HEAD/docs/images/wiki/serviceconfpowershell/1.png -------------------------------------------------------------------------------- /docs/images/wiki/serviceconfpowershell/2.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/eapowertools-archive/iPortal/HEAD/docs/images/wiki/serviceconfpowershell/2.png -------------------------------------------------------------------------------- /docs/images/wiki/serviceconfpowershell/3.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/eapowertools-archive/iPortal/HEAD/docs/images/wiki/serviceconfpowershell/3.png -------------------------------------------------------------------------------- /docs/images/wiki/serviceconfpowershell/4.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/eapowertools-archive/iPortal/HEAD/docs/images/wiki/serviceconfpowershell/4.png -------------------------------------------------------------------------------- /docs/images/wiki/serviceconfpowershell/5.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/eapowertools-archive/iPortal/HEAD/docs/images/wiki/serviceconfpowershell/5.png -------------------------------------------------------------------------------- /public/fonts/glyphicons-halflings-regular.eot: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/eapowertools-archive/iPortal/HEAD/public/fonts/glyphicons-halflings-regular.eot -------------------------------------------------------------------------------- /public/fonts/glyphicons-halflings-regular.ttf: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/eapowertools-archive/iPortal/HEAD/public/fonts/glyphicons-halflings-regular.ttf -------------------------------------------------------------------------------- /public/fonts/glyphicons-halflings-regular.woff: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/eapowertools-archive/iPortal/HEAD/public/fonts/glyphicons-halflings-regular.woff -------------------------------------------------------------------------------- /public/fonts/glyphicons-halflings-regular.woff2: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/eapowertools-archive/iPortal/HEAD/public/fonts/glyphicons-halflings-regular.woff2 -------------------------------------------------------------------------------- /docs/images/gss/licensing_configuration_login.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/eapowertools-archive/iPortal/HEAD/docs/images/gss/licensing_configuration_login.png -------------------------------------------------------------------------------- /docs/images/gss/licensing_configuration_user.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/eapowertools-archive/iPortal/HEAD/docs/images/gss/licensing_configuration_user.png -------------------------------------------------------------------------------- /config/services.conf.cfg: -------------------------------------------------------------------------------- 1 | [iportal] 2 | Identity=iportal 3 | Enabled=true 4 | DisplayName=iPortal 5 | ExecType=nodejs 6 | ExePath=Node\node.exe 7 | Script=..\EAPowerTools\iportal\server.js -------------------------------------------------------------------------------- /udc/app_paths.csv: -------------------------------------------------------------------------------- 1 | appName,port,path,boolAuth 2 | hub,,/hub,true 3 | qmc,,/qmc,true 4 | qmcu,,/content/default/qmculogin.html,true 5 | pinit,,/pinit,true 6 | gms,8590,/masterlib/testpage,false -------------------------------------------------------------------------------- /udc/iportal_users.csv: -------------------------------------------------------------------------------- 1 | userid,name 2 | a_wilson,Amy Wilson 3 | j_thomas,Jeremy Thomas 4 | p_harris,Paul Harris 5 | j_green,James Green 6 | s_bowers,Sara Bowers 7 | l_denton,Lisa Denton 8 | a_foster,Anne Foster 9 | e_hanson,Eric Hanson 10 | l_johnson,Laura Johnson 11 | e_reese,Eddie Reese -------------------------------------------------------------------------------- /views/error.hbs: -------------------------------------------------------------------------------- 1 |
2 |
ERROR: {{message}}
3 |
4 | {{error.status}} 5 | 6 | {{error.stack}} 7 | 8 |
9 |
10 | 11 |
12 | Return to List of Users 13 |
-------------------------------------------------------------------------------- /views/autherror.hbs: -------------------------------------------------------------------------------- 1 |
2 |
{{message}}
3 |
4 | 9 |
10 |
11 | 12 |
13 | Return to List of Users 14 |
15 | 16 | -------------------------------------------------------------------------------- /.gitattributes: -------------------------------------------------------------------------------- 1 | # Auto detect text files and perform LF normalization 2 | * text=auto 3 | 4 | # Custom for Visual Studio 5 | *.cs diff=csharp 6 | 7 | # Standard to msysgit 8 | *.doc diff=astextplain 9 | *.DOC diff=astextplain 10 | *.docx diff=astextplain 11 | *.DOCX diff=astextplain 12 | *.dot diff=astextplain 13 | *.DOT diff=astextplain 14 | *.pdf diff=astextplain 15 | *.PDF diff=astextplain 16 | *.rtf diff=astextplain 17 | *.RTF diff=astextplain 18 | -------------------------------------------------------------------------------- /utils/createServiceDispatcher.bat: -------------------------------------------------------------------------------- 1 | sc query "QlikEAPowerToolsServiceDispatcher" 2 | 3 | IF %ERRORLEVEL% EQU 0 (GOTO END) ELSE (GOTO ADDSERVICE) 4 | 5 | :ADDSERVICE 6 | sc create QlikEAPowerToolsServiceDispatcher binPath= "%~1" DisplayName= "Qlik EAPowerTools Service Dispatcher" start= auto 7 | sc description "QlikEAPowerToolsServiceDispatcher" "Service Dispatcher for running EA Powertools" 8 | 9 | sc start QlikEAPowerToolsServiceDispatcher 10 | 11 | TIMEOUT /T 10 12 | 13 | :END 14 | 15 | 16 | -------------------------------------------------------------------------------- /utils/convertXlsx.js: -------------------------------------------------------------------------------- 1 | var xl = require('xlsx'); 2 | var fs = require('fs'); 3 | var path = require('path'); 4 | var wb = xl.readFile(path.join(__dirname, "../udc/excel/iportal_users.xlsx")); 5 | 6 | 7 | fs.writeFileSync(path.join(__dirname, "../udc/iportal_users.csv"), xl.utils.sheet_to_csv(wb.Sheets["Users"])); 8 | 9 | fs.writeFileSync(path.join(__dirname, "../udc/iportal_attributes.csv"), xl.utils.sheet_to_csv(wb.Sheets["Attributes"])); 10 | 11 | console.log("Excel file converted to two csv files"); -------------------------------------------------------------------------------- /package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "iPortal", 3 | "version": "0.1.0", 4 | "private": true, 5 | "scripts": { 6 | "start": "node Node/iPortal/server.js" 7 | }, 8 | "dependencies": { 9 | "bluebird": "^3.4.0", 10 | "body-parser": "~1.13.2", 11 | "cookie-parser": "~1.3.5", 12 | "csvjson": "^4.1.3", 13 | "csvtojson": "^1.1.4", 14 | "debug": "~2.2.0", 15 | "express": "~4.13.1", 16 | "express-session": "~1.13.0", 17 | "extend": "~3.0.0", 18 | "hbs": "~3.1.0", 19 | "jquery": "~2.2.3", 20 | "log4js": "~0.6.36", 21 | "morgan": "~1.6.1", 22 | "request": "^2.72.0", 23 | "serve-favicon": "~2.3.0", 24 | "winston": "^2.2.0", 25 | "xlsx": "~0.8.0" 26 | } 27 | } 28 | -------------------------------------------------------------------------------- /utils/createVP.json: -------------------------------------------------------------------------------- 1 | { 2 | "prefix": "iportal", 3 | "description": "iportal", 4 | "authenticationModuleRedirectUri": "https://ENTERSERVER:ENTERPORT/", 5 | "sessionModuleBaseUri": "", 6 | "loadBalancingModuleBaseUri": "", 7 | "loadBalancingServerNodes": [{ 8 | "id": ENTERLBID 9 | }], 10 | "authenticationMethod": 0, 11 | "anonymousAccessMode": 0, 12 | "windowsAuthenticationEnabledDevicePattern": "Windows", 13 | "sessionCookieHeaderName": "X-Qlik-Session-iportal", 14 | "sessionCookieDomain": "", 15 | "additionalResponseHeaders": "", 16 | "sessionInactivityTimeout": 30, 17 | "extendedSecurityEnvironment": false, 18 | "websocketCrossOriginWhiteList": ["ENTERSERVER"], 19 | "defaultVirtualProxy": false, 20 | "tags": [] 21 | } -------------------------------------------------------------------------------- /lib/appPaths.js: -------------------------------------------------------------------------------- 1 | var csv = require('csvjson'); 2 | var cfg = require("../config/config") 3 | var path = require("path"); 4 | var winston = require("winston"); 5 | var fs = require('fs'); 6 | 7 | //set up logging 8 | var logger = new(winston.Logger)({ 9 | level: cfg.logLevel, 10 | transports: [ 11 | new(winston.transports.Console)(), 12 | new(winston.transports.File)({ filename: cfg.logFile }) 13 | ] 14 | }); 15 | 16 | module.exports = function() { 17 | var appsFile = fs.readFileSync(path.join(cfg.csvFilePath, "app_paths.csv"), { encoding: 'utf8' }); 18 | 19 | var appsArray = csv.toObject(appsFile); 20 | if (appsArray.length > 0) { 21 | logger.info("appPaths: ", appsArray.length, " apps loaded"); 22 | } else { 23 | logger.info("appPaths: no apps parsed from csv file"); 24 | throw new Error("error: no apps parsed from csv file"); 25 | } 26 | return appsArray; 27 | 28 | }; -------------------------------------------------------------------------------- /utils/checkHostName.js: -------------------------------------------------------------------------------- 1 | process.env.NODE_PATH= __dirname; 2 | var qrsInteract = require('../lib/qrsinteractions'); 3 | var config = require('../config/config'); 4 | var winston = require('winston'); 5 | var Promise = require('bluebird'); 6 | var fs = require('fs'); 7 | 8 | //set up logging 9 | var logger = new (winston.Logger)({ 10 | level: config.logLevel, 11 | transports: [ 12 | new (winston.transports.Console)(), 13 | new (winston.transports.File)({ filename: config.logFile}) 14 | ] 15 | }); 16 | 17 | process.argv.forEach(function (val, index, array) { 18 | console.log(index + ': ' + val); 19 | }); 20 | 21 | logger.info('Checking for existence of ' + process.argv[2] + ' on port ' + process.argv[3], {module: 'checkHostName.js'}); 22 | 23 | var path = "https://" + process.argv[2] + ":" + process.argv[3] + "/qrs/ServiceStatus/full"; 24 | path += "?xrfkey=ABCDEFG123456789"; 25 | 26 | qrsInteract.get(path) 27 | .then(function(result) 28 | { 29 | //console.log(result); 30 | fs.writeFileSync(config.utilsPath + '\\checkHostName.txt', 'true'); 31 | }) 32 | .catch(function(error) 33 | { 34 | //do nothing 35 | }); -------------------------------------------------------------------------------- /.vscode/launch.json: -------------------------------------------------------------------------------- 1 | { 2 | "version": "0.2.0", 3 | "configurations": [ 4 | { 5 | "name": "Launch", 6 | "type": "node", 7 | "request": "launch", 8 | "program": "${workspaceRoot}/server.js", 9 | "stopOnEntry": false, 10 | "args": [], 11 | "cwd": "${workspaceRoot}", 12 | "preLaunchTask": null, 13 | "runtimeExecutable": null, 14 | "runtimeArgs": [ 15 | "--nolazy" 16 | ], 17 | "env": { 18 | "NODE_ENV": "development" 19 | }, 20 | "externalConsole": false, 21 | "sourceMaps": false, 22 | "outDir": null 23 | }, 24 | { 25 | "name": "Attach", 26 | "type": "node", 27 | "request": "attach", 28 | "port": 5858, 29 | "address": "localhost", 30 | "restart": false, 31 | "sourceMaps": false, 32 | "outDir": null, 33 | "localRoot": "${workspaceRoot}", 34 | "remoteRoot": null 35 | } 36 | ] 37 | } -------------------------------------------------------------------------------- /utils/checkVPExist.js: -------------------------------------------------------------------------------- 1 | process.env.NODE_PATH= __dirname; 2 | var qrsInteract = require('../lib/qrsinteractions'); 3 | var config = require('../config/config'); 4 | var winston = require('winston'); 5 | var Promise = require('bluebird'); 6 | var fs = require('fs'); 7 | 8 | //set up logging 9 | var logger = new (winston.Logger)({ 10 | level: config.logLevel, 11 | transports: [ 12 | new (winston.transports.Console)(), 13 | new (winston.transports.File)({ filename: config.logFile}) 14 | ] 15 | }); 16 | 17 | process.argv.forEach(function (val, index, array) { 18 | console.log(index + ': ' + val); 19 | }); 20 | 21 | logger.info('Checking for existence of virtual proxy prefix ' + process.argv[4] + ' on ' + process.argv[2] + ' on port ' + process.argv[3], {module: 'checkVPExist.js'}); 22 | 23 | var path = "https://" + process.argv[2] + ":" + process.argv[3] + "/qrs/virtualProxyConfig/full"; 24 | path += "?xrfkey=ABCDEFG123456789&filter=prefix eq '" + process.argv[4] + "'"; 25 | 26 | qrsInteract.get(path) 27 | .then(function(result) 28 | { 29 | if(JSON.stringify(result)=='[]') 30 | { 31 | //the virtual proxy doesn't exist. 32 | logger.info('The virtual proxy: ' + process.argv[4] + ' does not exist.', {module: 'checkVPExist'}); 33 | } 34 | else 35 | { 36 | fs.writeFileSync(config.utilsPath + '/checkVPExist.txt', 'true'); 37 | } 38 | }) 39 | .catch(function(error) 40 | { 41 | //do nothing 42 | }); -------------------------------------------------------------------------------- /utils/checkUDCExist.js: -------------------------------------------------------------------------------- 1 | process.env.NODE_PATH= __dirname; 2 | var qrsInteract = require('../lib/qrsinteractions'); 3 | var config = require('../config/config'); 4 | var winston = require('winston'); 5 | var Promise = require('bluebird'); 6 | var fs = require('fs'); 7 | 8 | //set up logging 9 | var logger = new (winston.Logger)({ 10 | level: config.logLevel, 11 | transports: [ 12 | new (winston.transports.Console)(), 13 | new (winston.transports.File)({ filename: config.logFile}) 14 | ] 15 | }); 16 | 17 | process.argv.forEach(function (val, index, array) { 18 | console.log(index + ': ' + val); 19 | }); 20 | 21 | logger.info('Checking for existence of user directory connection ' + process.argv[4] + ' on ' + process.argv[2] + ' on port ' + process.argv[3], {module: 'checkUDCExist.js'}); 22 | 23 | var path = "https://" + process.argv[2] + ":" + process.argv[3] + "/qrs/UserDirectory/full"; 24 | path += "?xrfkey=ABCDEFG123456789&filter=settings.value eq '" + process.argv[4] + "'"; 25 | 26 | qrsInteract.get(path) 27 | .then(function(result) 28 | { 29 | if(JSON.stringify(result)=='[]') 30 | { 31 | //the virtual proxy doesn't exist. 32 | logger.info('The user directory connection: ' + process.argv[4] + ' does not exist.', {module: 'checkUDCExist'}); 33 | } 34 | else 35 | { 36 | fs.writeFileSync(config.utilsPath + '/checkUDCExist.txt', 'true'); 37 | } 38 | }) 39 | .catch(function(error) 40 | { 41 | //do nothing 42 | }); -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | #ignoring folders 2 | tmp/ 3 | tests/ 4 | 5 | # Logs 6 | logs 7 | *.log 8 | npm-debug.log* 9 | 10 | # Runtime data 11 | pids 12 | *.pid 13 | *.seed 14 | 15 | # Directory for instrumented libs generated by jscoverage/JSCover 16 | lib-cov 17 | 18 | # Coverage directory used by tools like istanbul 19 | coverage 20 | 21 | # Grunt intermediate storage (http://gruntjs.com/creating-plugins#storing-task-files) 22 | .grunt 23 | 24 | # node-waf configuration 25 | .lock-wscript 26 | 27 | # Compiled binary addons (http://nodejs.org/api/addons.html) 28 | build/Release 29 | 30 | # Dependency directory 31 | # https://docs.npmjs.com/misc/faq#should-i-check-my-node-modules-folder-into-git 32 | node_modules 33 | 34 | # ========================= 35 | # Operating System Files 36 | # ========================= 37 | 38 | # OSX 39 | # ========================= 40 | 41 | .DS_Store 42 | .AppleDouble 43 | .LSOverride 44 | 45 | # Thumbnails 46 | ._* 47 | 48 | # Files that might appear in the root of a volume 49 | .DocumentRevisions-V100 50 | .fseventsd 51 | .Spotlight-V100 52 | .TemporaryItems 53 | .Trashes 54 | .VolumeIcon.icns 55 | 56 | # Directories potentially created on remote AFP share 57 | .AppleDB 58 | .AppleDesktop 59 | Network Trash Folder 60 | Temporary Items 61 | .apdisk 62 | install 63 | 64 | # Windows 65 | # ========================= 66 | 67 | # Windows image file caches 68 | Thumbs.db 69 | ehthumbs.db 70 | 71 | # Folder config file 72 | Desktop.ini 73 | 74 | # Recycle Bin used on file shares 75 | $RECYCLE.BIN/ 76 | 77 | # Windows Installer files 78 | *.cab 79 | *.msi 80 | *.msm 81 | *.msp 82 | 83 | # Windows shortcuts 84 | *.lnk 85 | -------------------------------------------------------------------------------- /readme.md: -------------------------------------------------------------------------------- 1 | # Status 2 | [![Project Status: Unsupported – The project has reached a stable, usable state but the author(s) have ceased all work on it. A new maintainer may be desired.](https://www.repostatus.org/badges/latest/unsupported.svg)](https://www.repostatus.org/#unsupported) 3 | 4 | # iPortal Installation 5 | 6 | **[Latest Release Notes](https://github.com/eapowertools/iPortal/releases/latest)** 7 | 8 | iPortal is a web application that allows you to easily impersonate multiple users in a Qlik Sense Enterprise deployment. **It is not intended for production environments.** 9 | 10 | Click **[here](https://github.com/eapowertools/iPortal/releases/latest)** to download the most recent installation package. The fully automated installer will: 11 | 12 | * Install the iPortal web application 13 | * Configure the Qlik Sense service dispatcher to automatically run the iPortal web application 14 | * Add & configure a Qlik Sense virtual proxy 15 | * Add & configure a Qlik Sense user directory connector 16 | 17 | > The installer has been tested with Qlik Sense 2.2.4 and 3.0 18 | 19 | Additional security configuration is required to fully enable the Governed Self Service reference deployment. Please refer to the [GSS Setup Guide](docs/gss_setup_guide.md) for more information. 20 | 21 | # License 22 | 23 | EA Power Tools are a collection of software programs and methodologies for Qlik products. EA Power Tools and iPortal are provided free of charge and are not supported by Qlik. EA Power Tools and iPortal use Qlik APIs, but are open source solutions provided without warranty. Use of EA Power Tools and iPortal is at your own risk. 24 | 25 | For more information, please visit the [EAPowertools](https://community.qlik.com/community/qlik-sense/ea-powertools) space on Qlik Community. 26 | -------------------------------------------------------------------------------- /utils/udcDef.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "iportal", 3 | "userDirectoryName": "Queries user directory via the corresponding User Directory Connector", 4 | "configured": false, 5 | "operational": false, 6 | "type": "Repository.UserDirectoryConnectors.ODBC.ODBC", 7 | "syncOnlyLoggedInUsers": false, 8 | "syncStatus": 0, 9 | "configuredError": "", 10 | "operationalError": "", 11 | "settings": [{ 12 | "name": "User directory name", 13 | "value": "IPORTAL", 14 | "secret": false, 15 | "userDirectorySettingType": "String" 16 | }, 17 | { 18 | "name": "Users table", 19 | "value": "iportal_users.csv", 20 | "secret": false, 21 | "userDirectorySettingType": "String" 22 | }, 23 | { 24 | "name": "Attributes table", 25 | "value": "iportal_attributes.csv", 26 | "secret": false, 27 | "userDirectorySettingType": "String" 28 | }, 29 | { 30 | "name": "Connection string part 1", 31 | "value": "Driver={Microsoft Access Text Driver (*.txt, *.csv)};Extensions=asc,csv,tab,txt;Dbq=ENTERPATH\\udc", 32 | "secret": false, 33 | "userDirectorySettingType": "String" 34 | }, 35 | { 36 | "name": "Connection string part 2 (secret)", 37 | "value": "", 38 | "secret": true, 39 | "secretValue": "", 40 | "userDirectorySettingType": "String" 41 | }, 42 | { 43 | "name": "Synchronization timeout in seconds", 44 | "value": "240", 45 | "secret": false, 46 | "userDirectorySettingType": "Int" 47 | } 48 | ], 49 | "tags": [], 50 | "creationType": 1 51 | } -------------------------------------------------------------------------------- /config/config.js: -------------------------------------------------------------------------------- 1 | var path = require('path'); 2 | var extend = require('extend'); 3 | 4 | var certPath = path.join(process.env.programdata, '/Qlik/Sense/Repository/Exported Certificates/.Local Certificates'); 5 | var logPath = path.join(__dirname, '/../log/'); 6 | var routePath = path.join(__dirname, 'server/routes/'); 7 | var publicPath = path.join(__dirname, 'public/'); 8 | var utilsPath = path.join(__dirname, '/../utils/'); 9 | var excelFilePath = path.join(__dirname, '/../udc/excel/iportal_users.xlsx'); 10 | var csvFilePath = path.join(__dirname, '/../udc/'); 11 | 12 | var logFile = logPath + 'iPortal_' + dateTimeString() + '.log'; 13 | 14 | var config = extend(true, { 15 | serverPort: 3090, 16 | qpsPort: 4243, 17 | qrsPort: 4242, 18 | repoAccount: 'UserDirectory=Internal;UserId=sa_repository', 19 | hostname: 'senseServerName', 20 | virtualProxy: 'iportal', 21 | allowedConnections: 'domainOfSystems', 22 | userDirectory: 'iportal', 23 | sessionSecret: 'iportal-secret', 24 | certificates: { 25 | client: path.resolve(certPath, 'client.pem'), 26 | client_key: path.resolve(certPath, 'client_key.pem'), 27 | server: path.resolve(certPath, 'server.pem'), 28 | server_key: path.resolve(certPath, 'server_key.pem'), 29 | root: path.resolve(certPath, 'root.pem') 30 | }, 31 | routePath: routePath, 32 | publicPath: publicPath, 33 | logPath: logPath, 34 | logFile: logFile, 35 | utilsPath: utilsPath, 36 | excelFilePath: excelFilePath, 37 | csvFilePath: csvFilePath, 38 | logLevel: 'debug', 39 | theme: 'clipart', 40 | version: '1.6.0' 41 | }); 42 | 43 | module.exports = config; 44 | 45 | function dateTimeString() { 46 | var now = new Date(); 47 | var strDate = now.toISOString(); 48 | 49 | return strDate.split(':').join('.'); 50 | 51 | } -------------------------------------------------------------------------------- /utils/udcDefTest.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "iportal", 3 | "userDirectoryName": "Queries user directory via the corresponding User Directory Connector", 4 | "configured": false, 5 | "operational": false, 6 | "type": "Repository.UserDirectoryConnectors.ODBC.ODBC", 7 | "syncOnlyLoggedInUsers": false, 8 | "syncStatus": 0, 9 | "configuredError": "", 10 | "operationalError": "", 11 | "settings": [{ 12 | "name": "User directory name", 13 | "value": "IPORTAL", 14 | "secret": false, 15 | "userDirectorySettingType": "String" 16 | }, 17 | { 18 | "name": "Users table", 19 | "value": "iportal_users.csv", 20 | "secret": false, 21 | "userDirectorySettingType": "String" 22 | }, 23 | { 24 | "name": "Attributes table", 25 | "value": "iportal_attributes.csv", 26 | "secret": false, 27 | "userDirectorySettingType": "String" 28 | }, 29 | { 30 | "name": "Connection string part 1", 31 | "value": "Driver={Microsoft Access Text Driver (*.txt, *.csv)};Extensions=asc,csv,tab,txt;Dbq=c:\\program files\\qlik\\sense\\eapowertools\\iportal\\udc", 32 | "secret": false, 33 | "userDirectorySettingType": "String" 34 | }, 35 | { 36 | "name": "Connection string part 2 (secret)", 37 | "value": "", 38 | "secret": true, 39 | "secretValue": "", 40 | "userDirectorySettingType": "String" 41 | }, 42 | { 43 | "name": "Synchronization timeout in seconds", 44 | "value": "240", 45 | "secret": false, 46 | "userDirectorySettingType": "Int" 47 | } 48 | ], 49 | "tags": [], 50 | "creationType": 1 51 | } -------------------------------------------------------------------------------- /views/index.hbs: -------------------------------------------------------------------------------- 1 |
2 | 3 | {{#each users}} 4 |
5 |
{{this.name}}
6 | 7 |
8 |
9 | {{#each this.groups}} 10 | {{this.groups}} 11 | 12 | {{/each}} 13 |
14 |
15 | {{#each this.apps}} 16 | 17 | {{this}} 18 | {{/each}} 19 |
20 |
21 |
22 | {{/each}} 23 | 24 | 43 | 44 | 45 |
46 | 47 | 48 | -------------------------------------------------------------------------------- /app.js: -------------------------------------------------------------------------------- 1 | var express = require('express'); 2 | var path = require('path'); 3 | var favicon = require('serve-favicon'); 4 | var winston = require('winston'); 5 | var cookieParser = require('cookie-parser'); 6 | var session = require('express-session'); 7 | var bodyParser = require('body-parser'); 8 | var hbs = require('hbs'); 9 | var jquery = require('jquery'); 10 | var cfg = require('./config/config'); 11 | 12 | var routes = require('./routes/index'); 13 | 14 | var app = express(); 15 | 16 | // view engine setup 17 | app.set('views', path.join(__dirname, 'views')); 18 | app.set('view engine', 'hbs'); 19 | 20 | // load partials from directory 21 | hbs.registerPartials(__dirname+'/views/partials'); 22 | 23 | // uncomment after placing your favicon in /public 24 | app.use(favicon(path.join(__dirname, 'public', '/images/powertoolslogo.png'))); 25 | app.use(session({ resave: true, 26 | saveUninitialized: true, 27 | secret: cfg.sessionSecret})); 28 | app.use(bodyParser.json()); 29 | app.use(bodyParser.urlencoded({ extended: false })); 30 | app.use(cookieParser()); 31 | app.use(express.static(path.join(__dirname, 'public'))); 32 | 33 | app.use('/', routes); 34 | 35 | // catch 404 and forward to error handler 36 | app.use(function(req, res, next) { 37 | var err = new Error('Not Found'); 38 | err.status = 404; 39 | next(err); 40 | }); 41 | 42 | // error handlers 43 | 44 | // development error handler 45 | // will print stacktrace 46 | if (app.get('env') === 'development') { 47 | app.use(function(err, req, res, next) { 48 | res.status(err.status || 500); 49 | res.render('error', { 50 | message: err.message, 51 | error: err 52 | }); 53 | }); 54 | } 55 | 56 | // production error handler 57 | // no stacktraces leaked to user 58 | app.use(function(err, req, res, next) { 59 | res.status(err.status || 500); 60 | res.render('error', { 61 | message: err.message, 62 | error: {} 63 | }); 64 | }); 65 | 66 | 67 | module.exports = app; 68 | -------------------------------------------------------------------------------- /udc/iportal_attributes.csv: -------------------------------------------------------------------------------- 1 | userid,type,value 2 | a_wilson,group,QlikRootAdmin 3 | a_wilson,group,IT 4 | a_wilson,group,QlikSenseUserAccess 5 | j_thomas,group,QlikTeamAdmin 6 | j_thomas,group,QlikDeveloper 7 | j_thomas,group,Finance 8 | j_thomas,group,QlikSenseUserAccess 9 | p_harris,group,QlikTeamAdmin 10 | p_harris,group,QlikDeveloper 11 | p_harris,group,Sales 12 | p_harris,group,QlikSenseUserAccess 13 | j_green,group,QlikDesigner 14 | j_green,group,Sales 15 | j_green,group,QlikSenseUserAccess 16 | s_bowers,group,QlikDesigner 17 | s_bowers,group,IT 18 | s_bowers,group,QlikSenseUserAccess 19 | l_denton,group,QlikContributor 20 | l_denton,group,Marketing 21 | l_denton,group,QlikSenseUserAccess 22 | a_foster,group,QlikContributor 23 | a_foster,group,Sales 24 | a_foster,group,QlikSenseUserAccess 25 | e_hanson,group,QlikAnalyst 26 | e_hanson,group,Finance 27 | e_hanson,group,QlikSenseLoginAccess 28 | l_johnson,group,QlikConsumer 29 | l_johnson,group,Executive 30 | l_johnson,group,Sales 31 | l_johnson,group,QlikSenseLoginAccess 32 | e_reese,group,QlikConsumer 33 | e_reese,group,Sales 34 | e_reese,group,QlikSenseLoginAccess 35 | a_wilson,title,BI Administrator 36 | j_thomas,title,BI Developer 37 | p_harris,title,SR. BI Developr 38 | j_green,title,BI Analyst 39 | s_bowers,title,Data Analyst 40 | l_denton,title,Marketing Analyst 41 | a_foster,title,Sales Operations Lead 42 | e_hanson,title,Financial Analyst 43 | l_johnson,title,VP - Operations 44 | e_reese,title,Sales Territory Mgr 45 | a_wilson,app,Hub 46 | j_thomas,app,Hub 47 | p_harris,app,Hub 48 | j_green,app,Hub 49 | s_bowers,app,Hub 50 | l_denton,app,Hub 51 | a_foster,app,Hub 52 | e_hanson,app,Hub 53 | l_johnson,app,Hub 54 | e_reese,app,Hub 55 | a_wilson,app,QMC 56 | j_thomas,app,QMC 57 | p_harris,app,QMC 58 | j_thomas,app,qmcu 59 | a_wilson,udc,iPortal 60 | j_thomas,udc,iPortal 61 | p_harris,udc,iPortal 62 | j_green,udc,iPortal 63 | s_bowers,udc,iPortal 64 | l_denton,udc,iPortal 65 | a_foster,udc,iPortal 66 | e_hanson,udc,iPortal 67 | l_johnson,udc,iPortal 68 | e_reese,udc,iPortal 69 | a_wilson,image,a_wilson.jpg 70 | j_thomas,image,j_thomas.jpg 71 | p_harris,image,p_harris.jpg 72 | j_green,image,j_green.jpg 73 | s_bowers,image,s_bowers.jpg 74 | l_denton,image,l_denton.jpg 75 | a_foster,image,a_foster.jpg 76 | e_hanson,image,e_hanson.jpg 77 | l_johnson,image,l_johnson.jpg 78 | e_reese,image,e_reese.jpg 79 | -------------------------------------------------------------------------------- /public/stylesheets/style.css: -------------------------------------------------------------------------------- 1 | body { 2 | padding: 50px; 3 | font: 14px "Lucida Grande", Helvetica, Arial, sans-serif; 4 | } 5 | 6 | a { 7 | color: #00B7FF; 8 | cursor: pointer; 9 | } 10 | 11 | .footer { 12 | margin-top: 15px; 13 | text-align: center; 14 | } 15 | 16 | .user-container { 17 | text-align: center; 18 | } 19 | 20 | .user-panel { 21 | display: inline-block; 22 | position: relative; 23 | vertical-align: top; 24 | width: 170px; 25 | height: 210px; 26 | border: 1px solid; 27 | color: lightgrey; 28 | padding: 2px; 29 | margin: 2px; 30 | } 31 | 32 | .user-panel-body { 33 | display: inline-block; 34 | width: 160px; 35 | height: 160px; 36 | position: relative; 37 | vertical-align: top; 38 | } 39 | 40 | .user-panel:hover { 41 | background-color: lightgrey; 42 | } 43 | 44 | .user-panel-name { 45 | color: darkgreen; 46 | font-weight: bold; 47 | font-size: 16px; 48 | } 49 | 50 | .user-panel-title { 51 | color: darkgreen; 52 | font-style: italic; 53 | white-space: nowrap; 54 | } 55 | 56 | .user-panel-title:hover { 57 | color: darkgreen; 58 | font-style: italic; 59 | text-decoration: none; 60 | cursor: pointer; 61 | } 62 | 63 | .user-panel-groups { 64 | color: grey; 65 | font-size: 10px; 66 | margin-top: 5px; 67 | display: none; 68 | } 69 | 70 | .user-panel-apps { 71 | position: absolute; 72 | bottom: 4px; 73 | width: 100%; 74 | text-align: center; 75 | } 76 | 77 | .user-app-btn { 78 | width: 40%; 79 | margin-top: 5px; 80 | } 81 | 82 | .group-btn { 83 | -webkit-border-radius: 28; 84 | -moz-border-radius: 28; 85 | border-radius: 28px; 86 | font-family: Arial; 87 | background-color: white; 88 | color: grey; 89 | font-size: 11px; 90 | padding: 2px 7px 2px 7px; 91 | border: solid grey 1px; 92 | text-decoration: none; 93 | margin: 2px; 94 | } 95 | 96 | 97 | /* Dropdown Content (Hidden by Default) */ 98 | 99 | .dropdown-content { 100 | display: none; 101 | position: absolute; 102 | background-color: #f9f9f9; 103 | min-width: 160px; 104 | box-shadow: 0px 8px 16px 0px rgba(0, 0, 0, 0.2); 105 | } 106 | 107 | 108 | /* Show the dropdown menu on hover */ 109 | 110 | .dropdown:hover .dropdown-content { 111 | display: block; 112 | z-index: 100; 113 | } -------------------------------------------------------------------------------- /server.js: -------------------------------------------------------------------------------- 1 | process.env.NODE_PATH = __dirname; 2 | var app = require('./app'); 3 | var cfg = require('./config/config'); 4 | var https = require('https'); 5 | var winston = require('winston'); 6 | var fs = require('fs'); 7 | 8 | //set up logging 9 | var logger = new (winston.Logger)({ 10 | level: cfg.logLevel, 11 | transports: [ 12 | new (winston.transports.Console)(), 13 | new (winston.transports.File)({ filename: cfg.logFile}) 14 | ] 15 | }); 16 | 17 | /** 18 | * Get port from environment and store in Express. 19 | */ 20 | 21 | var port = normalizePort(cfg.serverPort || '3090'); 22 | app.set('port', port); 23 | 24 | /** 25 | * Create HTTPs server. 26 | */ 27 | //Server options to run an HTTPS server 28 | try { 29 | var httpsoptions = { 30 | cert: fs.readFileSync(cfg.certificates.server), 31 | key: fs.readFileSync(cfg.certificates.server_key) 32 | }; 33 | } catch (e) { 34 | logger.error(e); 35 | logger.error('iPortal application terminated.'); 36 | process.exit(1); 37 | } 38 | 39 | var server = https.createServer(httpsoptions, app); 40 | 41 | /** 42 | * Listen on provided port, on all network interfaces. 43 | */ 44 | 45 | server.listen(port); 46 | server.on('error', onError); 47 | server.on('listening', onListening); 48 | 49 | /** 50 | * Normalize a port into a number, string, or false. 51 | */ 52 | 53 | function normalizePort(val) { 54 | var port = parseInt(val, 10); 55 | 56 | if (isNaN(port)) { 57 | // named pipe 58 | return val; 59 | } 60 | 61 | if (port >= 0) { 62 | // port number 63 | return port; 64 | } 65 | 66 | return false; 67 | } 68 | 69 | /** 70 | * Event listener for HTTP server "error" event. 71 | */ 72 | 73 | function onError(error) { 74 | if (error.syscall !== 'listen') { 75 | throw error; 76 | } 77 | 78 | var bind = typeof port === 'string' 79 | ? 'Pipe ' + port 80 | : 'Port ' + port; 81 | 82 | // handle specific listen errors with friendly messages 83 | switch (error.code) { 84 | case 'EACCES': 85 | logger.error(bind + ' requires elevated privileges!'); 86 | process.exit(1); 87 | break; 88 | case 'EADDRINUSE': 89 | logger.error(bind + ' is already in use!'); 90 | process.exit(1); 91 | break; 92 | default: 93 | throw error; 94 | } 95 | } 96 | 97 | /** 98 | * Event listener for HTTP server "listening" event. 99 | */ 100 | 101 | function onListening() { 102 | var addr = server.address(); 103 | var bind = typeof addr === 'string' 104 | ? 'pipe ' + addr 105 | : 'port ' + addr.port; 106 | logger.info('Listening on ' + bind); 107 | } 108 | -------------------------------------------------------------------------------- /routes/index.js: -------------------------------------------------------------------------------- 1 | var express = require('express'); 2 | var cfg = require('../config/config'); 3 | var login = require('../lib/login'); 4 | var users = require("../lib/parseUdcFiles"); 5 | var handlebars = require('hbs'); 6 | var router = express.Router(); 7 | var winston = require('winston'); 8 | var appPaths = require("../lib/appPaths")(); 9 | 10 | //set up logging 11 | var logger = new(winston.Logger)({ 12 | level: cfg.logLevel, 13 | transports: [ 14 | new(winston.transports.Console)(), 15 | new(winston.transports.File)({ filename: cfg.logFile }) 16 | ] 17 | }); 18 | /* 19 | GET / 20 | 21 | This is the only web page in this application and it displays the list of users 22 | associated with the EXCELUDS 23 | 24 | */ 25 | router.get('/', function(req, res, next) { 26 | logger.debug('Route: GET /'); 27 | 28 | handlebars.registerHelper('ButtonStyle', function(app) { 29 | if (app.toUpperCase() == "HUB") { 30 | return "btn-success"; 31 | } else { 32 | return "btn-primary"; 33 | } 34 | }); 35 | 36 | var config = users.loadUsers("iportal_users.csv", "iportal_attributes.csv"); 37 | config.cfg = cfg; 38 | 39 | res.render('index', config); 40 | }); 41 | 42 | router.get('/login', function(req, res, next) { 43 | logger.debug('Route: GET /login'); 44 | 45 | var user = req.query.user; 46 | var directory = req.query.directory; 47 | var app = req.query.app; 48 | 49 | var appInfo = getAppInfo(app, res); 50 | 51 | if (appInfo.boolAuth) { 52 | var authUri = 'https://' + cfg.hostname + ':' + cfg.qpsPort + '/qps/' + cfg.virtualProxy; 53 | 54 | logger.debug('Route: GET /login - USER: (', user, ') DIRECTORY: (', directory, ')'); 55 | 56 | logger.debug('Route: GET /login - Requesting ticket...'); 57 | logger.debug(authUri); 58 | login.requestticket(req, res, next, user, directory, authUri, app, appInfo.path); 59 | req.session.destroy(); 60 | } else { 61 | var url = "https://" + cfg.hostname + (appInfo.port == "" ? "" : ":" + appInfo.port) + appInfo.path; 62 | res.redirect(url); 63 | } 64 | 65 | }); 66 | 67 | module.exports = router; 68 | 69 | function getAppInfo(app, res) { 70 | //get app information from appPaths array 71 | var appInfo = appPaths.filter(function(item) { 72 | return item.appName.toUpperCase() === app.toUpperCase(); 73 | }) 74 | 75 | if (appInfo.length == 1) { 76 | return appInfo[0]; 77 | } else { 78 | var message; 79 | if (appInfo.length > 1) { 80 | message = "More than one reference with the same appName. This is not allowed. Fix the app_paths.csv file."; 81 | } else { 82 | message = "There is an issue with app_paths.csv. Like you don't have any app references listed in the file."; 83 | } 84 | res.status(400).send("

" + message + "

"); 85 | } 86 | } -------------------------------------------------------------------------------- /lib/parseUdcFiles.js: -------------------------------------------------------------------------------- 1 | var csv = require('csvjson'); 2 | var cfg = require("../config/config") 3 | var path = require("path"); 4 | var winston = require("winston"); 5 | var fs = require('fs'); 6 | 7 | //set up logging 8 | var logger = new(winston.Logger)({ 9 | level: cfg.logLevel, 10 | transports: [ 11 | new(winston.transports.Console)(), 12 | new(winston.transports.File)({ filename: cfg.logFile }) 13 | ] 14 | }); 15 | 16 | module.exports = { 17 | loadUsers: function(userFile, attributeFile) { 18 | logger.info('parseUdcFiles: Reading csv files (', cfg.csvFilePath, ")..."); 19 | var usersFile = fs.readFileSync(path.join(cfg.csvFilePath, userFile), { encoding: 'utf8' }); 20 | 21 | var attributesFile = fs.readFileSync(path.join(cfg.csvFilePath, attributeFile), { encoding: 'utf8' }); 22 | 23 | var userArray = csv.toObject(usersFile); 24 | if (userArray.length > 0) { 25 | logger.info("parseUdcFiles: ", userArray.length, " users loaded"); 26 | } else { 27 | logger.info("parseUdcFiles: no users parsed from csv file"); 28 | throw new Error("error: no users parsed from csv file"); 29 | } 30 | 31 | var attributesArray = csv.toObject(attributesFile); 32 | if (attributesArray.length > 0) { 33 | logger.info("parseUdcFiles: ", attributesArray.length, " attribute records loaded"); 34 | } else { 35 | logger.info("parseUdcFiles: no attributes parsed from csv file"); 36 | throw new Error("error: No attributes parsed from attributes file"); 37 | } 38 | 39 | 40 | var Users = {}; 41 | 42 | userArray.forEach(function(user, index) { 43 | var User = { 44 | userid: user.userid, 45 | name: user.name 46 | }; 47 | 48 | 49 | 50 | var userAttributes = attributesArray.filter(function(item) { 51 | return item.userid == User.userid; 52 | }); 53 | 54 | var userGroups = []; 55 | var userApps = []; 56 | 57 | userAttributes.forEach(function(item) { 58 | switch (item.type) { 59 | case "group": 60 | userGroups.push(item.value); 61 | break; 62 | case "app": 63 | userApps.push(item.value); 64 | break; 65 | case "image": 66 | User.image = item.value; 67 | break; 68 | case "udc": 69 | User.udc = item.value; 70 | break; 71 | case "title": 72 | User.title = item.value; 73 | break; 74 | default: 75 | User[item.type] = item.value; 76 | 77 | } 78 | }); 79 | 80 | User.groups = userGroups; 81 | User.apps = userApps; 82 | Users[index] = User; 83 | }) 84 | 85 | var config = {}; 86 | config.users = Users; 87 | logger.info("Completed parsing UDC csv files."); 88 | return config; 89 | 90 | } 91 | } -------------------------------------------------------------------------------- /utils/ServicesConfBuilder.ps1: -------------------------------------------------------------------------------- 1 | $installParent = Get-Location 2 | $installedProjects = Get-ChildItem -path $installParent | Where-Object{($_.PSIsContainer)} | foreach-object{$_.Name} 3 | $count = 0 4 | 5 | $intro = "This is the EAPowerTools services.conf update script.`n" 6 | $intro += "This script will review installed PowerTools and add entries to the`n" 7 | $intro += "services.conf file if applicable and if missing due to an upgrade of`n" 8 | $intro += "Qlik Sense.`n`n" 9 | $intro += "Press any key to begin the update." 10 | 11 | Write-Host $intro 12 | Write-Host "========================================================================" 13 | 14 | $x = $Host.ui.RawUI.ReadKey("NoEcho,IncludeKeyDown") 15 | 16 | Write-Host "========================================================================" 17 | Write-Host 18 | 19 | 20 | function getServiceDispatcherPath 21 | { 22 | $Entry = Get-ChildItem "HKLM:\Software\Microsoft\Windows\CurrentVersion\Uninstall" -Recurse | 23 | ForEach-Object {Get-ItemProperty $_.pspath} | Where-Object {$_.DisplayName -eq "Qlik Sense Service Dispatcher"} 24 | 25 | return $Entry.InstallLocation + "servicedispatcher\services.conf" 26 | } 27 | 28 | $serviceDispatcherPath = getServiceDispatcherPath 29 | 30 | Write-Host "The services.conf file resides at path: "$serviceDispatcherPath 31 | Write-Host 32 | 33 | function getConf 34 | { 35 | param([string]$dir, [string]$app) 36 | #$dir = $args[0] 37 | #$app = $args[1] 38 | 39 | if(Test-Path "$dir\$app\config\services.conf.cfg") 40 | { 41 | $config = [IO.File]::ReadAllText("$dir\$app\config\services.conf.cfg") 42 | return $config 43 | } 44 | } 45 | 46 | Write-Host "Reviewing list of installed EAPowerTools" 47 | Write-Host 48 | Write-Host "========================================================================" 49 | 50 | 51 | forEach($appName in $installedProjects) 52 | { 53 | Write-Host 54 | Write-Host $appName" is installed on this system." 55 | $appWithBracks = "\[$appName\]" 56 | #Write-Host $appWithBracks 57 | $configuredApps = Select-String -Path $serviceDispatcherPath -pattern $appWithBracks 58 | 59 | if(!$configuredApps) 60 | { 61 | Write-Host "Adding "$appName" configuration to services.conf." 62 | $conf = getConf $installParent $appName 63 | Add-Content $serviceDispatcherPath "`n$conf" 64 | $count++ 65 | } 66 | else 67 | { 68 | Write-Host $appName" configuration exists in services.conf" 69 | } 70 | 71 | Write-Host 72 | Write-Host "========================================================================" 73 | 74 | } 75 | 76 | if($count -gt 0) 77 | { 78 | 79 | Write-Host "Made $count updates to the services.conf." 80 | Write-Host "Stopping the Qlik Sense Service Dispatcher Service." 81 | Stop-Service -DisplayName "Qlik Sense Service Dispatcher" 82 | Write-Host 83 | Write-Host "========================================================================" 84 | 85 | Write-Host 86 | Write-Host "Starting the Qlik Sense Service Dispatcher Service." 87 | Start-Service -DisplayName "Qlik Sense Service Dispatcher" 88 | } 89 | else 90 | { 91 | Write-Host 92 | Write-Host "No updates need to be made to the services.conf." 93 | } -------------------------------------------------------------------------------- /views/layout.hbs: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | {{title}} 5 | 6 | 7 | 8 | 9 | 10 | 11 | 12 |
13 |
14 | 15 | 40 | 41 | {{{body}}} 42 | 43 | 44 | 72 | 73 | 74 |
75 |
76 | 77 | 78 | -------------------------------------------------------------------------------- /utils/createUDC.js: -------------------------------------------------------------------------------- 1 | var qrsInteract = require('../lib/qrsinteractions'); 2 | var config = require('../config/config'); 3 | var winston = require('winston'); 4 | var Promise = require('bluebird'); 5 | 6 | //set up logging 7 | var logger = new(winston.Logger)({ 8 | level: config.logLevel, 9 | transports: [ 10 | new(winston.transports.Console)(), 11 | new(winston.transports.File)({ filename: config.logFile }) 12 | ] 13 | }); 14 | 15 | var body = { 16 | "configured": false, 17 | "configuredError": "", 18 | "creationType": 1, 19 | "name": config.userDirectory, 20 | "operational": false, 21 | "operationalError": "", 22 | "settings": [{ 23 | "name": "User directory name", 24 | "value": config.userDirectory, 25 | "secret": false, 26 | "userDirectorySettingType": "String" 27 | }, 28 | { 29 | "name": "Users table", 30 | "value": "iportal_users.csv", 31 | "secret": false, 32 | "userDirectorySettingType": "String" 33 | }, 34 | { 35 | "name": "Attributes table", 36 | "value": "iportal_attributes.csv", 37 | "secret": false, 38 | "userDirectorySettingType": "String" 39 | }, 40 | { 41 | "name": "Connection string part 1", 42 | "value": "Driver={Microsoft Access Text Driver (*.txt, *.csv)};Extensions=asc,csv,tab,txt;Dbq=" + process.argv[2] + "\\udc", 43 | "secret": false, 44 | "userDirectorySettingType": "String" 45 | }, 46 | { 47 | "name": "Connection string part 2 (secret)", 48 | "secretValue": "", 49 | "secret": true, 50 | "userDirectorySettingType": "String", 51 | "value": "" 52 | }, 53 | { 54 | "name": "Synchronization timeout in seconds", 55 | "secret": false, 56 | "userDirectorySettingType": "Int", 57 | "value": "240" 58 | } 59 | ], 60 | "tags": [], 61 | "syncOnlyLoggedInUsers": false, 62 | "syncStatus": 0, 63 | "type": "Repository.UserDirectoryConnectors.ODBC.ODBC" 64 | }; 65 | 66 | 67 | 68 | function createUDC(body) { 69 | var x = {}; 70 | var path = "https://" + config.hostname + ":" + config.qrsPort + "/qrs/UserDirectory"; 71 | path += "?xrfkey=ABCDEFG123456789"; 72 | qrsInteract.post(path, body) 73 | .then(function(result) { 74 | x.UDC = JSON.parse(result); 75 | logger.info('UDC Creation: ' + JSON.stringify(JSON.parse(result)), { module: 'createUDC' }); 76 | return x.UDC.id; 77 | }) 78 | .then(function(result) { 79 | 80 | logger.info('passed result id: ' + result, { module: 'createUDC' }); 81 | var postPath = "https://" + config.hostname + ":" + config.qrsPort + "/qrs/userdirectoryconnector/syncuserdirectories"; 82 | postPath += "?xrfkey=ABCDEFG123456789"; 83 | 84 | var syncBody = []; 85 | syncBody.push(result); 86 | logger.info('syncing UDC: ' + syncBody, { module: 'createUDC' }); 87 | qrsInteract.post(postPath, syncBody) 88 | .then(function(sCode) { 89 | if (sCode == 204) { 90 | logger.info('User Directory Sync started', { module: 'createUDC' }); 91 | } 92 | }) 93 | .catch(function(error) { 94 | logger.error(error, { module: 'createUDC' }); 95 | return error; 96 | }); 97 | }) 98 | .catch(function(error) { 99 | logger.error(error, { module: 'createUDC' }); 100 | return error; 101 | }); 102 | 103 | 104 | } 105 | 106 | function buildModDate() { 107 | var d = new Date(); 108 | return d.toISOString(); 109 | } 110 | 111 | createUDC(body); -------------------------------------------------------------------------------- /lib/login.js: -------------------------------------------------------------------------------- 1 | var express = require('express'); 2 | var https = require('https'); 3 | var cfg = require('../config/config'); 4 | var url = require('url'); 5 | var fs = require('fs'); 6 | var winston = require('winston'); 7 | var querystring = require("querystring"); 8 | var appPaths = require("./appPaths")(); 9 | 10 | //set up logging 11 | var logger = new(winston.Logger)({ 12 | level: cfg.logLevel, 13 | transports: [ 14 | new(winston.transports.Console)(), 15 | new(winston.transports.File)({ filename: cfg.logFile }) 16 | ] 17 | }); 18 | 19 | module.exports = { 20 | requestticket: function(req, res, next, selectedUser, userDirectory, restURI, userApp, path) { 21 | 22 | 23 | 24 | var XRFKEY = rand(16); 25 | //Configure parameters for the ticket request 26 | try { 27 | 28 | logger.debug(cfg.client); 29 | 30 | var options = { 31 | host: url.parse(restURI).hostname, 32 | port: url.parse(restURI).port, 33 | path: url.parse(restURI).path + '/ticket?xrfkey=' + XRFKEY, 34 | method: 'POST', 35 | headers: { 36 | 'X-qlik-xrfkey': XRFKEY, 37 | 'Content-Type': 'application/json' 38 | }, 39 | cert: fs.readFileSync(cfg.certificates.client), 40 | key: fs.readFileSync(cfg.certificates.client_key), 41 | rejectUnauthorized: false, 42 | agent: false 43 | }; 44 | } catch (e) { 45 | logger.error(e); 46 | res.render('error', e); 47 | } 48 | 49 | logger.info("requestTicket: Ticket Options (", options.path.toString(), ")"); 50 | //Send ticket request 51 | var ticketreq = https.request(options, function(ticketres) { 52 | logger.info("requestTicket: statusCode: (", ticketres.statusCode, ")"); 53 | 54 | ticketres.on('data', function(d) { 55 | //Parse ticket response 56 | logger.info("requestTicket: POST Response \n", d.toString()); 57 | if (ticketres.statusCode != 201) { 58 | var authError = {}; 59 | authError.message = "Invalid response code (" + ticketres.statusCode + ") from Qlik Sense."; 60 | authError.response = d.toString(); 61 | authError.ticket = options.path.toString(); 62 | authError.request = jsonrequest; 63 | res.render('autherror', authError); 64 | } else { 65 | // Get the ticket returned by Qlik Sense 66 | var ticket = JSON.parse(d.toString()); 67 | logger.info("requestTicket: Qlik Sense Ticket \n", ticket); 68 | 69 | //Add the QlikTicket to the REDIRECTURI regardless whether the existing userApp has existing params. 70 | var redirectUri = 'https://' + cfg.hostname + '/' + cfg.virtualProxy; 71 | logger.debug("requestTicket: (", redirectUri, ")"); 72 | logger.debug("requestTicket: userApp: (", userApp, ")"); 73 | var myRedirect = url.parse(redirectUri); 74 | 75 | var myQueryString = querystring.parse(myRedirect.query); 76 | myQueryString['QlikTicket'] = ticket.Ticket; 77 | 78 | // The redirectURI currently works for any application that is simply a path on the Qlik Sense URL (ex. /hub, /qmc, or /devhub) 79 | // TODO: Exhance this code to support launching applications that are not an extension of the Qlik Sense URL. 80 | var finalRedirectURI = redirectUri + path + '?Qlikticket=' + ticket.Ticket; 81 | logger.debug("requestTicket: Redirecting to (", finalRedirectURI, ")"); 82 | 83 | res.redirect(finalRedirectURI); 84 | } 85 | }); 86 | }); 87 | 88 | //Send JSON request for ticket 89 | var jsonrequest = JSON.stringify({ 'userDirectory': userDirectory.toString(), 'UserId': selectedUser.toString(), 'Attributes': [] }); 90 | logger.debug("requestTicket: JSON request: ", jsonrequest); 91 | 92 | ticketreq.write(jsonrequest); 93 | ticketreq.end(); 94 | 95 | ticketreq.on('error', function(e) { 96 | logger.error("requestTicket: Error submitting authentication request (", e, ")"); 97 | logger.error('Error' + e); 98 | }); 99 | } 100 | }; 101 | 102 | //Supporting functions 103 | function rand(length, current) { 104 | current = current ? current : ''; 105 | return length ? rand(--length, "0123456789ABCDEFGHIJKLMNOPQRSTUVWXTZabcdefghiklmnopqrstuvwxyz".charAt(Math.floor(Math.random() * 60)) + current) : current; 106 | } -------------------------------------------------------------------------------- /lib/users.js: -------------------------------------------------------------------------------- 1 | var xlsx = require('xlsx'); // JavaScript package for ready Excel files 2 | var cfg = require('../config/config'); 3 | var winston = require('winston'); 4 | 5 | //set up logging 6 | var logger = new (winston.Logger)({ 7 | level: cfg.logLevel, 8 | transports: [ 9 | new (winston.transports.Console)(), 10 | new (winston.transports.File)({ filename: cfg.logFile}) 11 | ] 12 | }); 13 | 14 | module.exports = { 15 | 16 | /* 17 | This function reads data from the Users and Attributes worksheets in an Excel document 18 | and merges it together to create a single JavaScript object used to render the list of 19 | users available for impersonation (views/partials/index.hbs). 20 | 21 | The Excel document is expected to be formatted such that it can be used as a Qlik Sense 22 | User Directory Connector (UDC). 23 | */ 24 | loadExcelUsers: function(fileName) { 25 | logger.info('loadExcelUsers: Reading Excel file (',cfg.excelFilePath,")..."); 26 | 27 | /* Open workbook containing users & attributes */ 28 | var workbook = xlsx.readFile(cfg.excelFilePath); 29 | 30 | /* Convert contents of Excel Users & Attributes file to JavaScript object */ 31 | var xlsxUsers = {}; 32 | var xlsxAttrs = {}; 33 | 34 | /* Sheet containing users must be named 'Users' or rename key below */ 35 | var roa = xlsx.utils.sheet_to_row_object_array(workbook.Sheets['Users']); 36 | if (roa.length > 0){ 37 | xlsxUsers = roa; 38 | logger.info('loadExcelUsers: ', roa.length, 'Users loaded.') 39 | } else { 40 | logger.error('loadExcelUsers: No data found in Users worksheet!'); 41 | } 42 | 43 | /* Sheet containing attributes must be named 'Attributes' or rename key below */ 44 | var roa = xlsx.utils.sheet_to_row_object_array(workbook.Sheets['Attributes']); 45 | if (roa.length > 0){ 46 | xlsxAttrs = roa; 47 | logger.info('loadExcelUsers: ', roa.length, 'Attributes loaded.') 48 | } else { 49 | logger.error('loadExcelUsers: No data found in Attributes worksheet!'); 50 | } 51 | 52 | /* Create empty objects to populated from xlsx content */ 53 | var Users = {}; 54 | logger.debug('loadExcelUsers: Converting Excel data into JavaScript object...'); 55 | 56 | /* Transform xlsx data into JavaScript object used by templating engine to render user list */ 57 | for (var key in xlsxUsers) { 58 | 59 | var User = {}; 60 | 61 | if (xlsxUsers.hasOwnProperty(key)) { 62 | var uid = xlsxUsers[key].userid; 63 | var name = xlsxUsers[key].name; 64 | 65 | logger.info('loadExcelUsers: Transforming user (',name,')...'); 66 | 67 | User["userid"] = uid; 68 | User["name"] = name; 69 | 70 | Users[key]=User; 71 | 72 | var userSpecificAttrs = xlsxAttrs.filter( function(item){return (item.userid==uid);} ); 73 | logger.info('loadExcelUsers: Found ',userSpecificAttrs.length,' attributes associated with user (',name,')'); 74 | 75 | var userGroups = []; 76 | var userApps = []; 77 | 78 | for(var i=0; i 6 | # Table of Contents 7 | 8 | * [Create Tags](#createTags) 9 | * [How to: Create Tags](#howToCreateTags) 10 | * [Create Custom properties](#createCustomProps) 11 | * [Custom Propertes for GSS](#gssCustomProps) 12 | * [How to: Create Custom Properties](#howToCreateCustomProps) 13 | * [Create Streams](#streams) 14 | * [GSS Streams](#gssStreams) 15 | * [How to: Create Streams](#howToCreateStreams) 16 | * [Import Apps](#importApps) 17 | * [Post Import App Configuration](#postImportAppConfig) 18 | * [Security Rules](#secRules) 19 | * [Disable Default Security Rules](#disableSecurityRules) 20 | * [How to: Disable Default Security Rules](#howToDisableSecurityRules) 21 | * [Create Custom Security Rules](#createSecurityRules) 22 | * [How to: Create Custom Security Rules](#howToSecurityRules) 23 | * [The Governed Self-Service Reference Deployment Rules](#gssRules) 24 | * [Create User Access Rule](#createUserAccessRule) 25 | * [Set the TeamAdmin custom property on Jeremy and Paul ](#teamAdminUsers) 26 | * [Jeremy Thomas](#teamAdminJeremy) 27 | * [Paul Harris](#teamAdminPaul) 28 | * [Set AppLevelMgmt custom property on Laura Johnson](#appLevelLaura) 29 | 30 | A definition of each object/resource is provide along with a step-by-step configuration guide. Where appropriate, a detail explanation of the configuration and it’s purpose within the governed self service deployment is provided. 31 | 32 | 33 | ## Create Tags 34 | 35 | Tags are simply labels attached to objects within the QMC for the purpose of identification or to provide other information about an object. The QMC user interface allows you to sort and/or filter objects and resources using these tags. We will be using them to easily located resources that we add, modify or disable as part of this configuration. 36 | 37 | Create the following tags using the instructions provided below: 38 | 39 | 1. Name: **Disabled Default Rule** 40 | 2. Name: **Custom Rule** 41 | 3. Name: **iPortal User** 42 | 4. Name: **PowerTool** 43 | 44 | 45 | ### How to: Create Tags 46 | 47 | 1. In the left navigation pane on the QMC Home/Start page, click on **Tags** in the **Manage Resources** section to open the tags management page. 48 | 49 | ![Tags Menu](./images/gss/tags_menu.png) 50 | 2. Click on the **Create new** button located near the bottom of the page. 51 | 3. Enter the *Name* of your new tag. NOTE: We will associate these tags to resources later; for now, there will be no associated items for your new tags. 52 | 4. Click the **Apply** button located near the bottom of the page to save your changes. 53 | 5. Click the **Add another** button and return to **step #3** until you have created all of the tags listed above. 54 | 55 | [Back to Top](#toc) 56 | 57 | 58 | ## Create Custom Properties 59 | 60 | Custom Properties are metadata that can be associated to resource types within the QMC. Each custom property contains a list of possible values. For example, you could create a custom property named *ReleaseStage* that contains the values *Development*, *Testing* and *Production* and associate it with the Apps resource. Custom properties are primarily used to configure security rules using metadata rather than explicit values. For example, you could configure a security rule that restricts access to Apps with a *ReleaseStage* of *Development* to Users with a *QlikFunction* of *Developer*. 61 | 62 | 63 | ### GSS Custom Properties 64 | Create the following custom properties: 65 | 66 | 1. Name: **AppLevelMgmt** 67 | * *Description*: This custom property allows for app-level exceptions to stream access. With this custom property an app resides in a stream that many users have access, but only a few have access to the specific application. 68 | * *Resource Types*: Apps, Users 69 | * *Values*: Executive, HR, PCI 70 | 2. Name: **DataConnectionType** 71 | * *Description*: This custom property identifies the type of Data Connection. This allows you to evaluate the data connection types in security rules. This comes in handy when developers and designers are allowed to access specific data connections. 72 | * *Resource Types*: Data connections 73 | * *Values*: Admin, Folder, MS Access, ODBC, Oracle, PowerToolQVD, QVD, SQL Server 74 | 3. Name: **ManagedMasterItems** 75 | * *Description*: This custom property is used with the Goverened Metrics Service. It allows an app to "subscribe" to a subject area of metrics that exist in a central Metrics data source. A Metrics Library app pushes the appropriate metrics to the master library of apps with the assigned property values. It is possible to assign more than one subject area to an app. 76 | * *Resource Types*: Apps 77 | * *Values*: Customer Service, Finance, Marketing, Sales 78 | 4. Name: **QlikGroup** 79 | * *Description*: This custom property is for Streams, Apps and Data Connections to help manage access rights to users based on their security group membership. This custom property prevents granular management at the individual object/user level. 80 | * *Resource Types*: Apps, Data connections, Reload tasks, Streams 81 | * *Values*: Finance, IT, Marketing, QlikAdmin, Sales 82 | 5. Name: **TeamAdmin** 83 | * *Description*: Certain users in the iPortal deployment are team admins or department admins. This custom property is assigned to users who will act as content and resource administrators for their departments. 84 | * *Resource Types*: Users 85 | * *Values*: Finance, IT, Marketing, Sales 86 | 87 | 88 | ### How to: Create Custom Properties 89 | 90 | 1. In the left navigation pane on the QMC Home/Start page, click on **Custom properties** in the **Manage Resources** section to open the custom properties management page. 91 | 92 | ![Tags Menu](./images/gss/custom_properties_menu.png) 93 | 2. Click on the **Create new** button located near the bottom of the page. 94 | 3. Enter the *Name* of your new custom property. 95 | 4. Enable each *Resource Type* that is to be associated with the new custom property by clicking on the checkbox. 96 | 5. Click the **Create new** button in the *Values* section and type in a value. Repeat this step until all of the values have been created. 97 | 6. Click the **Apply** button located near the bottom of the page. 98 | 7. Click the **Add another** button and return to **step #3** until you have created all of the custom properties listed above. 99 | 100 | [Back to Top](#toc) 101 | 102 | 103 | ## Create Streams 104 | 105 | Streams allow you to group applications together for administrative purposes. This eliminates the need to apply certain settings and authorization rules to each App individually. 106 | 107 | A stream enables users to read and/or publish Apps, Sheets, and Stories. Users who have publish access to a stream, create the content for that specific stream. The stream access pattern in a Qlik Sense site is determined by the security rules for each stream. By default, Qlik Sense includes two streams: Everyone and Monitoring apps. An app can be published to only one stream. To publish an app to another stream, the app must first be duplicated and then published to the other stream. 108 | 109 | 110 | ### GSS Streams 111 | Create the following streams: 112 | 113 | 1. **Sales** 114 | 2. **Marketing** 115 | 3. **Finance** 116 | 117 | 118 | ### How to: Create Streams 119 | 120 | 1. In the left navigation pane on the QMC Home/Start page, click on **Streams** in the **Manage Content** section to open the streams management page. 121 | 122 | ![Tags Menu](./images/gss/streams_menu.png) 123 | 2. Click on the **Create new** button located near the bottom of the page. 124 | 3. Enter the *Name* of your new stream. 125 | 4. Click the **Apply** button located near the bottom of the page to save your changes. When the Create security rule window is displayed, click the **Cancel** button to continue without creating any rules. You will create security rules in the next section. 126 | 5. Click the **Add another** button and return to **step #3** until you have created all of the streams listed above. 127 | 128 | After creating the streams, edit each stream and add the @QlikGroup custom property value that matches the stream name. 129 | ![Set Custom Property on Stream](./images/gss/streams_customprop.png) 130 | 131 | _Example custom property setting on a stream_ 132 | 133 | [Back to Top](#toc) 134 | 135 | 136 | ## Import Apps 137 | 138 | To test Governed Self-Service, the EA Team provides a set of Qlik demo apps to import and set the custom properties detailed above and publish to streams. With these apps, it is possible to see the impact of setting custom properties along with security rules to control access. You can download the apps from the **[Governed Self Service space on Community](https://community.qlik.com/docs/DOC-16872)**. 139 | 140 | 141 | ## Post Import App Configuration in QMC 142 | 143 | Follow the table to publish apps to the appropriate stream. Set the AppLevelMgmt custom property on the Executive Dashboard app with the value **Executive**. If installing the Governed Metrics Service along with iportal, add the **Sales** custom property value to the ManagedMasterItems custom property of the Executive Dashboard. 144 | 145 | | App Name | Stream | @AppLevelMgmt | @ManagedMasterItems | 146 | | -------- | :------: | :-------------: | :-------------------: | 147 | | Executive Dashboard | Sales | Executive | Sales | 148 | | Customer Experience [Telco] | Marketing | | | 149 | | Sales Management and Customers Analysis | | | | 150 | | Travel Expense Management | Finance | | | 151 | 152 | ![Set Custom Properties on Apps](./images/app_customprop.png) 153 | 154 | _Example custom property setting on Executive Dashboard app_ 155 | 156 | [Back to Top](#toc) 157 | 158 | 159 | ## Security Rules 160 | 161 | 162 | ### Disable Default Security Rules 163 | 164 | The Qlik Sense system includes an attribute-based security rules engine that uses rules as expressions to evaluate what type of access a user or users should be granted for a resource. 165 | 166 | In this section you will disable some default security rules that are provided with the standard Qlik Sense installation - they will be replaced with new custom security rules. You could just edit the default security rules, but we recommend you follow the best practice guideline of disabling default rules and creating new rules. This allows you to retain the default rules just in case you would like to reference or revert to them in the future. 167 | 168 | Disable the following security rules using the step-by-step instructions provided below: 169 | * **ContentAdmin** 170 | * **ContentAdminQmcSections** 171 | * **CreateApp** 172 | * **CreateAppObjectsPublishedApp** 173 | * **DataConnection** 174 | * **Stream** 175 | 176 | 177 | #### How to: Disable Default Security Rules 178 | 179 | 1. In the left navigation pane on the QMC Home/Start page, click on **Security rules** in the **Manage Resources** section to open the security rules management page. 180 | 181 | ![Tags Menu](./images/gss/security_rules_menu.png) 182 | 2. For each of the security rules list above, locate the rule in the list of rules and double-click on the row. Alternatively, you can single click on the row and click on the **Edit** button located near the bottom of the page. 183 | 3. In the *Identification* section of the *security rule form*, click on the **Disabled** checkbox. 184 | 4. Click on the **Tags** field and select *Disabled Default Rule* from the dropdown. 185 | 5. Click the **Apply** button located near the bottom of the page to save your changes. 186 | 6. Click on the **Security rules** breadcrumb near the top of the page. 187 | 7. Repeat **steps #2 through #6** for each of the security rules list above. 188 | 189 | [Back to Top](#toc) 190 | 191 | 192 | ### Create Custom Security Rules 193 | 194 | 195 | #### How to: Create Custom Security Rules 196 | 197 | 1. In the left navigation pane on the QMC Home/Start page, click on **Security rules** in the **Manage Resources** section to open the security rules management page. 198 | 199 | ![Tags Menu](./images/gss/security_rules_menu.png) 200 | 2. Click on the **Create new** button located near the bottom of the page. 201 | 3. Enter the *Name* of your new security rule in the Identification section of the form. 202 | 4. Enter the *Description* in the **Identification** section of the form. 203 | 5. Check or uncheck the appropriate *Actions* in the **Basic** section of the form. 204 | 6. Enter the *Resource filter* in the **Advanced** section of the form. 205 | 7. Enter the *Conditions* in the **Advanced** section of the form. 206 | 8. Select the appropriate *Context* in the **Advanced** section of the form. 207 | 9. Add the appropriate *Tag(s)* in the **Tags** section of the form. 208 | 10. Click the **Apply** button located near the bottom of the page to save your changes. 209 | 11. Click the **Add another** button and return to step #3 until you have created all of the security rules listed below. 210 | 211 | 212 | #### The Governed Self-Service Reference Deployment Rules 213 | 214 | The Qlik Sense system includes an attribute-based security rules engine that uses rules to evaluate what type of access a user or users should be granted for a resource. 215 | 216 | In this section you will create new security rules to replace and augment those rules disabled in the previous section. Each new security rule includes a brief description of the rule and its effect on the implementation. 217 | 218 | > The security rules defined below use a prefix **_gss** in the security rule’s names. This prefix is recommended to be your company name or abbreviation. You can leave this as is or replace it with your own prefix, it will not impact the functioning of the rules. 219 | 220 | Create the following security rules using the step-by-step instructions provided below: 221 | 222 | > NOTE: You need to enter of the *Resource filter* exactly as it appears below. Do not use the *Resource filter* dropdown menu within the QMC application form editor. 223 | 224 | 1. Name: **_gss a– TeamAdmin QMC Sections** 225 | * Description: Allow users the QlikTeamAdmins group to have the same rights as users in the Qlik Role "QlikTeamAdmin". 226 | * Actions: Read 227 | * Resource filter: QmcSection_App, QmcSection_DataConnection, QmcSection_ContentLibrary,QmcSection_App.Object, QmcSection_Task, QmcSection_ReloadTask, QmcSection_Event, QmcSection_SchemaEvent, QmcSection_CompositeEvent, QmcSection_User 228 | * Conditions: 229 | ``` 230 | ((!user.@TeamAdmin.empty())) 231 | ``` 232 | * Context: Only in QMC 233 | * Tags: Custom Rule 234 | 235 | 2. Name: **_gss b– TeamAdmin Rights** 236 | * Description: Grants rights to resources for Team Admins. It has to be separate from the QMCSections rule for Team Admins, as they operate on different resources. 237 | * Actions: Create, Read, Update, Delete, Export, Publish, Change role 238 | * Resource filter: Stream\*, App\*, ReloadTask\*, SchemaEvent\*, Tag\*, CompositeEvent\*, ExecutionResult\*, CustomProperty\*,User_\*, Task\* 239 | * Conditions: 240 | ``` 241 | ((user.group=user.@TeamAdmin 242 | and (user.group=resource.@QlikGroup 243 | or user.group = resource.group) 244 | )) 245 | ``` 246 | * Context: Only in QMC 247 | * Tags: Custom Rule 248 | 249 | 3. Name: **_gss c– Group Access Rule** 250 | * Description: Allow user access to read for all resources matching the user’s security group value. 251 | * Actions: Read 252 | * Resource filter: App\*, Stream_\* 253 | * Conditions: 254 | ``` 255 | user.group=resource.@QlikGroup 256 | ``` 257 | * Context: Both in hub and QMC 258 | * Tags: Custom Rule 259 | 260 | 4. Name: **_gss d– Stream Rule – Apps Default Rule** 261 | * Description: Allow users to see/read resources if they have read access to the stream it is published to. 262 | * Actions: Read 263 | * Resource filter: App\* 264 | * Conditions: 265 | ``` 266 | ( 267 | resource.resourcetype = "App" 268 | and resource.stream.HasPrivilege("read") 269 | and resource.@AppLevelMgmt.empty() 270 | ) 271 | or 272 | ( 273 | ( 274 | resource.resourcetype = "App.Object" 275 | and resource.published = "true" 276 | and resource.objectType != "app_appscript" 277 | ) 278 | and resource.app.stream.HasPrivilege("read") 279 | ) 280 | ``` 281 | * Context: Both in hub and QMC 282 | * Tags: Custom Rule 283 | 284 | 285 | 5. a. Name: **_gss e1– CreateAppObjectsPublishedApp** 286 | * Description: Allows users to create app objects of all types on a published app, except for Consumers, who cannot create sheets. 287 | * Actions: Create 288 | * Resource filter: App.Object_\* 289 | * Conditions: 290 | ``` 291 | !resource.App.stream.Empty() 292 | and resource.App.HasPrivilege("read") 293 | and ( 294 | resource.objectType = "userstate" 295 | or ( 296 | resource.objectType = "sheet" 297 | or resource.objectType = "story" 298 | or resource.objectType = "bookmark" 299 | or resource.objectType = "hiddenbookmark" 300 | or resource.objectType = "snapshot" 301 | or resource.objectType = "embeddedsnapshot" 302 | and user.group != "QlikConsumer" 303 | ) 304 | ) 305 | and !user.IsAnonymous() 306 | ``` 307 | * Context: Only in hub 308 | * Tags: Custom Rule 309 | 310 | b. Name: **_gss e2– CreateAppObjectsPublishedApp** 311 | * Description: Allows users to create app objects of all types on a published app, except for Consumers, who cannot create sheets. 312 | * Actions: Create 313 | * Resource filter: App.Object_\* 314 | * Conditions: 315 | ``` 316 | !resource.App.stream.Empty() 317 | and resource.App.HasPrivilege("read") 318 | and ( 319 | resource.objectType = "userstate" 320 | or ( 321 | resource.objectType = "sheet" 322 | or resource.objectType = "story" 323 | or resource.objectType = "snapshot" 324 | or resource.objectType = "embeddedsnapshot" 325 | and user.group != "QlikConsumer" 326 | ) 327 | ) 328 | or resource.objectType = "bookmark" 329 | or resource.objectType = "hiddenbookmark" 330 | and !user.IsAnonymous() 331 | ``` 332 | * Context: Only in hub 333 | * Tags: Custom Rule 334 | 335 | 6. Name: **_gss f– Publishing Rights by Role** 336 | * Description: Allow Contributors, Designers and Developers to publish to streams. 337 | * Actions: Read, Publish 338 | * Resource filter: Stream_\* 339 | * Conditions: 340 | ``` 341 | ( 342 | user.group = "QlikRootAdmin" 343 | or user.group="QlikContributor" 344 | or user.group like "*Developer" 345 | or user.group="QlikDesigner" 346 | or user.roles = "Developer" 347 | ) 348 | and 349 | ( 350 | user.group=resource.@QlikGroup 351 | ) 352 | ``` 353 | * Context: Both in hub and QMC 354 | * Tags: Custom Rule 355 | 356 | 7. Name: **_gss g– Stream Rule – Apps Exception Rule** 357 | * Description: Allow users to see apps with exception properties if they also have the same exception properties at the user level. 358 | * Actions: Read 359 | * Resource filter: App\* 360 | * Conditions: 361 | ``` 362 | resource.stream.HasPrivilege("read") 363 | and 364 | user.@AppLevelMgmt=resource.@AppLevelMgmt 365 | ``` 366 | * Context: Both in hub and QMC 367 | * Tags: Custom Rule 368 | 369 | 370 | 8. Name: **_gss h– Create App** 371 | * Description: Allows Developers and Designers to create and publish apps/sheets 372 | * Actions: Create, Read, Update, Delete, Export, Publish 373 | * Resource filter: App_\* 374 | * Conditions: 375 | ``` 376 | ( 377 | user.group="QlikRootAdmin" 378 | or user.roles="RootAdmin" 379 | or user.group like "*Developer" 380 | or user.group="QlikDesigner" 381 | or user.roles ="Developer" 382 | ) 383 | and resource.owner = user 384 | ``` 385 | * Context: Only in hub 386 | * Tags: Custom Rule 387 | 388 | 9. Name: **_gss i– DataConnection Read QVDs** 389 | * Description: Allow user to read QVD type data connection if they are a Designer. 390 | * Actions: Read 391 | * Resource filter: DataConnection_\* 392 | * Conditions: 393 | ``` 394 | ( 395 | user.group="QlikDesigner" 396 | or user.group like "*Developer" 397 | or user.roles="Developer" 398 | and resource.@DataConnectionType="QVD" 399 | ) 400 | or 401 | ( 402 | user.group=user.@TeamAdmin 403 | or user.group="QlikRootAdmin" 404 | ) 405 | ``` 406 | * Context: Only in hub 407 | * Tags: Custom Rule 408 | 409 | 10. Name: **_gss j– UpdateAppObjectsPublishedApp** 410 | * Description: Allows Qlik Developers and Team Admins to change update app objects that are published. Used for approving and unapproving content. 411 | * Actions: Update 412 | * Resource filter: App.Object_\* 413 | * Conditions: 414 | ``` 415 | !resource.App.stream.Empty() 416 | and resource.App.HasPrivilege("read") 417 | and resource.objectType = "userstate" 418 | or ( 419 | user.group like "*Developer" 420 | or user.group=user.@TeamAdmin 421 | ) 422 | and !user.IsAnonymous() 423 | ``` 424 | * Context: Both in the hub and the QMC 425 | * Tags: Custom Rule 426 | 427 | 11. Name: **_gss k– DataConnection Create** 428 | * Description: Allow users to create data connections except of type folder. 429 | * Actions: Create 430 | * Resource filter: DataConnection_\* 431 | * Conditions: 432 | ``` 433 | ((user.group="ConnectionCreators" 434 | or user.group=user.@TeamAdmin 435 | or user.group="QlikRootAdmin")) 436 | ``` 437 | * Context: Both in hub and QMC 438 | * Tags: Custom Rule 439 | 440 | 12. Name: **_gss l– Root Admin Group Rule** 441 | * Description: Allow all access to any user that is a member of the group QlikRootAdmin. 442 | * Actions: Create, Read, Update, Delete, Export, Publish, Change owner, Change role 443 | * Resource filter: \* 444 | * Conditions: 445 | ``` 446 | user.group="QlikRootAdmin" 447 | or user.roles="RootAdmin" 448 | ``` 449 | * Context: Only in QMC 450 | * Tags: Custom Rule 451 | 452 | 13. Name: **_gss M- TeamAdmin Duplicate Rights** 453 | * Description: Allows Team Admins to duplicate apps in the QMC. 454 | * Actions: Create, Read, Update, Delete 455 | * Resource filter: App\*, App_\* 456 | * Conditions: 457 | ``` 458 | (user.group="QlikDeveloper") 459 | and(user.group=resource.@QlikGroup or 460 | resource.owner=user) 461 | ``` 462 | * Context: Only in QMC 463 | * Tags: Custom Rule 464 | 465 | 466 | [Back to Top](#toc) 467 | 468 | 469 | ## Create User Access Rule 470 | 471 | User and Login Access Rules define which users will automatically be allocated a license token when logging into Qlik Sense. A *user access rule* allocates a license token to a **named user** whereas a *login access rule* allocates a **login access pass** that allows a user to access Qlik Sense for a predefined amount of time. Please refer to Qlik Sense online help for more details on login access passes. 472 | 473 | The access rules created in this section will leverage a user’s userdirectory value to allocate a token automatically to the user when they log in for the first time. 474 | 475 | 1. In the left navigation pane on the QMC Home/Start page, click on **License and tokens** in the **Manage Resources** section to open the license management page. 476 | 2. Click on the **User access rules** tab on the right side of the page. 477 | 3. Click on the **Create new** button located near the bottom of the page. 478 | 4. Click on the **Basic** and **Tags** properties tab on the right side of the page. A small checkmark will be displayed on the tab and the **Basic** and **Tags** section of the form will now be visible. 479 | 5. Enter **_gss – User Access Token Rule** as the *Name* of the user access rule. 480 | 6. In the **Basic** section of the form, configure the rule such that **user userdDirectory** is equal to the **value** of **IPORTAL**. 481 | 482 | ![Tags Menu](./images/gss/licensing_configuration_user.png) 483 | 7. Add the *Custom Rule* tag in the **Tags** section of the form. 484 | 8. Click the **Apply** button located near the bottom of the page to save your changes. 485 | 9. Click on the **License usage summary** breadcrumb near the top of the page. 486 | 487 | [Back to Top](#toc) 488 | 489 | 490 | ## Set the TeamAdmin custom property on Jeremy Thomas and Paul Harris 491 | 492 | Earlier in the configuration, a custom property named *TeamAdmin* was created. It is time to set values of this custom property on iportal users. 493 | 494 | 1. In the left navigation pane on the QMC Home/Start page, click on **Users** in the **Manage Content** section to open the user management page. 495 | 2. Click on the **filter icon** associated with the *User directory* column and type "iportal" in the popup textbox to filter the list of users to only include those from the iPortal UDC. 496 | 497 | 498 | ### For Jeremy Thomas 499 | 500 | 1. Select Jeremy Thomas and click the **Edit** button at the bottom of the page. 501 | 2. Click on the custom properties item on the right hand side of the page. 502 | 3. In the dialog box for **TeamAdmin**, select the value **Finance** when the list of values appears. 503 | 4. Click the Apply button. 504 | 505 | 506 | ### For Paul Harris 507 | 508 | 1. Select Paul Harris and click the **Edit** button at the bottom of the page. 509 | 2. Click on the custom properties item on the right hand side of the page. 510 | 3. In the dialog box for **TeamAdmin**, select the values **Sales** and **Marketing** when the list of values appears. 511 | 4. Click the Apply button. 512 | 513 | 514 | ## Set AppLevelMgmt custom property on Laura Johnson 515 | 516 | 1. Select Laura Johnson and click the **Edit** button at the bottom of the page. 517 | 2. Click on the custom properties item on the right hand side of the page. 518 | 3. In the dialog box for **AppLevelMgmt**, select the values **Executive** when the list of values appears. 519 | 4. Click the Apply button. 520 | 521 | 522 | Congratulations! You have completed the configuration of Governed Self-Service settings in the Qlik Sense Management Console! 523 | 524 | [Back to Top](#toc) 525 | -------------------------------------------------------------------------------- /public/javascripts/bootstrap.js: -------------------------------------------------------------------------------- 1 | /*! 2 | * Bootstrap v3.3.6 (http://getbootstrap.com) 3 | * Copyright 2011-2015 Twitter, Inc. 4 | * Licensed under the MIT license 5 | */ 6 | 7 | if (typeof jQuery === 'undefined') { 8 | throw new Error('Bootstrap\'s JavaScript requires jQuery') 9 | } 10 | 11 | +function ($) { 12 | 'use strict'; 13 | var version = $.fn.jquery.split(' ')[0].split('.') 14 | if ((version[0] < 2 && version[1] < 9) || (version[0] == 1 && version[1] == 9 && version[2] < 1) || (version[0] > 2)) { 15 | throw new Error('Bootstrap\'s JavaScript requires jQuery version 1.9.1 or higher, but lower than version 3') 16 | } 17 | }(jQuery); 18 | 19 | /* ======================================================================== 20 | * Bootstrap: transition.js v3.3.6 21 | * http://getbootstrap.com/javascript/#transitions 22 | * ======================================================================== 23 | * Copyright 2011-2015 Twitter, Inc. 24 | * Licensed under MIT (https://github.com/twbs/bootstrap/blob/master/LICENSE) 25 | * ======================================================================== */ 26 | 27 | 28 | +function ($) { 29 | 'use strict'; 30 | 31 | // CSS TRANSITION SUPPORT (Shoutout: http://www.modernizr.com/) 32 | // ============================================================ 33 | 34 | function transitionEnd() { 35 | var el = document.createElement('bootstrap') 36 | 37 | var transEndEventNames = { 38 | WebkitTransition : 'webkitTransitionEnd', 39 | MozTransition : 'transitionend', 40 | OTransition : 'oTransitionEnd otransitionend', 41 | transition : 'transitionend' 42 | } 43 | 44 | for (var name in transEndEventNames) { 45 | if (el.style[name] !== undefined) { 46 | return { end: transEndEventNames[name] } 47 | } 48 | } 49 | 50 | return false // explicit for ie8 ( ._.) 51 | } 52 | 53 | // http://blog.alexmaccaw.com/css-transitions 54 | $.fn.emulateTransitionEnd = function (duration) { 55 | var called = false 56 | var $el = this 57 | $(this).one('bsTransitionEnd', function () { called = true }) 58 | var callback = function () { if (!called) $($el).trigger($.support.transition.end) } 59 | setTimeout(callback, duration) 60 | return this 61 | } 62 | 63 | $(function () { 64 | $.support.transition = transitionEnd() 65 | 66 | if (!$.support.transition) return 67 | 68 | $.event.special.bsTransitionEnd = { 69 | bindType: $.support.transition.end, 70 | delegateType: $.support.transition.end, 71 | handle: function (e) { 72 | if ($(e.target).is(this)) return e.handleObj.handler.apply(this, arguments) 73 | } 74 | } 75 | }) 76 | 77 | }(jQuery); 78 | 79 | /* ======================================================================== 80 | * Bootstrap: alert.js v3.3.6 81 | * http://getbootstrap.com/javascript/#alerts 82 | * ======================================================================== 83 | * Copyright 2011-2015 Twitter, Inc. 84 | * Licensed under MIT (https://github.com/twbs/bootstrap/blob/master/LICENSE) 85 | * ======================================================================== */ 86 | 87 | 88 | +function ($) { 89 | 'use strict'; 90 | 91 | // ALERT CLASS DEFINITION 92 | // ====================== 93 | 94 | var dismiss = '[data-dismiss="alert"]' 95 | var Alert = function (el) { 96 | $(el).on('click', dismiss, this.close) 97 | } 98 | 99 | Alert.VERSION = '3.3.6' 100 | 101 | Alert.TRANSITION_DURATION = 150 102 | 103 | Alert.prototype.close = function (e) { 104 | var $this = $(this) 105 | var selector = $this.attr('data-target') 106 | 107 | if (!selector) { 108 | selector = $this.attr('href') 109 | selector = selector && selector.replace(/.*(?=#[^\s]*$)/, '') // strip for ie7 110 | } 111 | 112 | var $parent = $(selector) 113 | 114 | if (e) e.preventDefault() 115 | 116 | if (!$parent.length) { 117 | $parent = $this.closest('.alert') 118 | } 119 | 120 | $parent.trigger(e = $.Event('close.bs.alert')) 121 | 122 | if (e.isDefaultPrevented()) return 123 | 124 | $parent.removeClass('in') 125 | 126 | function removeElement() { 127 | // detach from parent, fire event then clean up data 128 | $parent.detach().trigger('closed.bs.alert').remove() 129 | } 130 | 131 | $.support.transition && $parent.hasClass('fade') ? 132 | $parent 133 | .one('bsTransitionEnd', removeElement) 134 | .emulateTransitionEnd(Alert.TRANSITION_DURATION) : 135 | removeElement() 136 | } 137 | 138 | 139 | // ALERT PLUGIN DEFINITION 140 | // ======================= 141 | 142 | function Plugin(option) { 143 | return this.each(function () { 144 | var $this = $(this) 145 | var data = $this.data('bs.alert') 146 | 147 | if (!data) $this.data('bs.alert', (data = new Alert(this))) 148 | if (typeof option == 'string') data[option].call($this) 149 | }) 150 | } 151 | 152 | var old = $.fn.alert 153 | 154 | $.fn.alert = Plugin 155 | $.fn.alert.Constructor = Alert 156 | 157 | 158 | // ALERT NO CONFLICT 159 | // ================= 160 | 161 | $.fn.alert.noConflict = function () { 162 | $.fn.alert = old 163 | return this 164 | } 165 | 166 | 167 | // ALERT DATA-API 168 | // ============== 169 | 170 | $(document).on('click.bs.alert.data-api', dismiss, Alert.prototype.close) 171 | 172 | }(jQuery); 173 | 174 | /* ======================================================================== 175 | * Bootstrap: button.js v3.3.6 176 | * http://getbootstrap.com/javascript/#buttons 177 | * ======================================================================== 178 | * Copyright 2011-2015 Twitter, Inc. 179 | * Licensed under MIT (https://github.com/twbs/bootstrap/blob/master/LICENSE) 180 | * ======================================================================== */ 181 | 182 | 183 | +function ($) { 184 | 'use strict'; 185 | 186 | // BUTTON PUBLIC CLASS DEFINITION 187 | // ============================== 188 | 189 | var Button = function (element, options) { 190 | this.$element = $(element) 191 | this.options = $.extend({}, Button.DEFAULTS, options) 192 | this.isLoading = false 193 | } 194 | 195 | Button.VERSION = '3.3.6' 196 | 197 | Button.DEFAULTS = { 198 | loadingText: 'loading...' 199 | } 200 | 201 | Button.prototype.setState = function (state) { 202 | var d = 'disabled' 203 | var $el = this.$element 204 | var val = $el.is('input') ? 'val' : 'html' 205 | var data = $el.data() 206 | 207 | state += 'Text' 208 | 209 | if (data.resetText == null) $el.data('resetText', $el[val]()) 210 | 211 | // push to event loop to allow forms to submit 212 | setTimeout($.proxy(function () { 213 | $el[val](data[state] == null ? this.options[state] : data[state]) 214 | 215 | if (state == 'loadingText') { 216 | this.isLoading = true 217 | $el.addClass(d).attr(d, d) 218 | } else if (this.isLoading) { 219 | this.isLoading = false 220 | $el.removeClass(d).removeAttr(d) 221 | } 222 | }, this), 0) 223 | } 224 | 225 | Button.prototype.toggle = function () { 226 | var changed = true 227 | var $parent = this.$element.closest('[data-toggle="buttons"]') 228 | 229 | if ($parent.length) { 230 | var $input = this.$element.find('input') 231 | if ($input.prop('type') == 'radio') { 232 | if ($input.prop('checked')) changed = false 233 | $parent.find('.active').removeClass('active') 234 | this.$element.addClass('active') 235 | } else if ($input.prop('type') == 'checkbox') { 236 | if (($input.prop('checked')) !== this.$element.hasClass('active')) changed = false 237 | this.$element.toggleClass('active') 238 | } 239 | $input.prop('checked', this.$element.hasClass('active')) 240 | if (changed) $input.trigger('change') 241 | } else { 242 | this.$element.attr('aria-pressed', !this.$element.hasClass('active')) 243 | this.$element.toggleClass('active') 244 | } 245 | } 246 | 247 | 248 | // BUTTON PLUGIN DEFINITION 249 | // ======================== 250 | 251 | function Plugin(option) { 252 | return this.each(function () { 253 | var $this = $(this) 254 | var data = $this.data('bs.button') 255 | var options = typeof option == 'object' && option 256 | 257 | if (!data) $this.data('bs.button', (data = new Button(this, options))) 258 | 259 | if (option == 'toggle') data.toggle() 260 | else if (option) data.setState(option) 261 | }) 262 | } 263 | 264 | var old = $.fn.button 265 | 266 | $.fn.button = Plugin 267 | $.fn.button.Constructor = Button 268 | 269 | 270 | // BUTTON NO CONFLICT 271 | // ================== 272 | 273 | $.fn.button.noConflict = function () { 274 | $.fn.button = old 275 | return this 276 | } 277 | 278 | 279 | // BUTTON DATA-API 280 | // =============== 281 | 282 | $(document) 283 | .on('click.bs.button.data-api', '[data-toggle^="button"]', function (e) { 284 | var $btn = $(e.target) 285 | if (!$btn.hasClass('btn')) $btn = $btn.closest('.btn') 286 | Plugin.call($btn, 'toggle') 287 | if (!($(e.target).is('input[type="radio"]') || $(e.target).is('input[type="checkbox"]'))) e.preventDefault() 288 | }) 289 | .on('focus.bs.button.data-api blur.bs.button.data-api', '[data-toggle^="button"]', function (e) { 290 | $(e.target).closest('.btn').toggleClass('focus', /^focus(in)?$/.test(e.type)) 291 | }) 292 | 293 | }(jQuery); 294 | 295 | /* ======================================================================== 296 | * Bootstrap: carousel.js v3.3.6 297 | * http://getbootstrap.com/javascript/#carousel 298 | * ======================================================================== 299 | * Copyright 2011-2015 Twitter, Inc. 300 | * Licensed under MIT (https://github.com/twbs/bootstrap/blob/master/LICENSE) 301 | * ======================================================================== */ 302 | 303 | 304 | +function ($) { 305 | 'use strict'; 306 | 307 | // CAROUSEL CLASS DEFINITION 308 | // ========================= 309 | 310 | var Carousel = function (element, options) { 311 | this.$element = $(element) 312 | this.$indicators = this.$element.find('.carousel-indicators') 313 | this.options = options 314 | this.paused = null 315 | this.sliding = null 316 | this.interval = null 317 | this.$active = null 318 | this.$items = null 319 | 320 | this.options.keyboard && this.$element.on('keydown.bs.carousel', $.proxy(this.keydown, this)) 321 | 322 | this.options.pause == 'hover' && !('ontouchstart' in document.documentElement) && this.$element 323 | .on('mouseenter.bs.carousel', $.proxy(this.pause, this)) 324 | .on('mouseleave.bs.carousel', $.proxy(this.cycle, this)) 325 | } 326 | 327 | Carousel.VERSION = '3.3.6' 328 | 329 | Carousel.TRANSITION_DURATION = 600 330 | 331 | Carousel.DEFAULTS = { 332 | interval: 5000, 333 | pause: 'hover', 334 | wrap: true, 335 | keyboard: true 336 | } 337 | 338 | Carousel.prototype.keydown = function (e) { 339 | if (/input|textarea/i.test(e.target.tagName)) return 340 | switch (e.which) { 341 | case 37: this.prev(); break 342 | case 39: this.next(); break 343 | default: return 344 | } 345 | 346 | e.preventDefault() 347 | } 348 | 349 | Carousel.prototype.cycle = function (e) { 350 | e || (this.paused = false) 351 | 352 | this.interval && clearInterval(this.interval) 353 | 354 | this.options.interval 355 | && !this.paused 356 | && (this.interval = setInterval($.proxy(this.next, this), this.options.interval)) 357 | 358 | return this 359 | } 360 | 361 | Carousel.prototype.getItemIndex = function (item) { 362 | this.$items = item.parent().children('.item') 363 | return this.$items.index(item || this.$active) 364 | } 365 | 366 | Carousel.prototype.getItemForDirection = function (direction, active) { 367 | var activeIndex = this.getItemIndex(active) 368 | var willWrap = (direction == 'prev' && activeIndex === 0) 369 | || (direction == 'next' && activeIndex == (this.$items.length - 1)) 370 | if (willWrap && !this.options.wrap) return active 371 | var delta = direction == 'prev' ? -1 : 1 372 | var itemIndex = (activeIndex + delta) % this.$items.length 373 | return this.$items.eq(itemIndex) 374 | } 375 | 376 | Carousel.prototype.to = function (pos) { 377 | var that = this 378 | var activeIndex = this.getItemIndex(this.$active = this.$element.find('.item.active')) 379 | 380 | if (pos > (this.$items.length - 1) || pos < 0) return 381 | 382 | if (this.sliding) return this.$element.one('slid.bs.carousel', function () { that.to(pos) }) // yes, "slid" 383 | if (activeIndex == pos) return this.pause().cycle() 384 | 385 | return this.slide(pos > activeIndex ? 'next' : 'prev', this.$items.eq(pos)) 386 | } 387 | 388 | Carousel.prototype.pause = function (e) { 389 | e || (this.paused = true) 390 | 391 | if (this.$element.find('.next, .prev').length && $.support.transition) { 392 | this.$element.trigger($.support.transition.end) 393 | this.cycle(true) 394 | } 395 | 396 | this.interval = clearInterval(this.interval) 397 | 398 | return this 399 | } 400 | 401 | Carousel.prototype.next = function () { 402 | if (this.sliding) return 403 | return this.slide('next') 404 | } 405 | 406 | Carousel.prototype.prev = function () { 407 | if (this.sliding) return 408 | return this.slide('prev') 409 | } 410 | 411 | Carousel.prototype.slide = function (type, next) { 412 | var $active = this.$element.find('.item.active') 413 | var $next = next || this.getItemForDirection(type, $active) 414 | var isCycling = this.interval 415 | var direction = type == 'next' ? 'left' : 'right' 416 | var that = this 417 | 418 | if ($next.hasClass('active')) return (this.sliding = false) 419 | 420 | var relatedTarget = $next[0] 421 | var slideEvent = $.Event('slide.bs.carousel', { 422 | relatedTarget: relatedTarget, 423 | direction: direction 424 | }) 425 | this.$element.trigger(slideEvent) 426 | if (slideEvent.isDefaultPrevented()) return 427 | 428 | this.sliding = true 429 | 430 | isCycling && this.pause() 431 | 432 | if (this.$indicators.length) { 433 | this.$indicators.find('.active').removeClass('active') 434 | var $nextIndicator = $(this.$indicators.children()[this.getItemIndex($next)]) 435 | $nextIndicator && $nextIndicator.addClass('active') 436 | } 437 | 438 | var slidEvent = $.Event('slid.bs.carousel', { relatedTarget: relatedTarget, direction: direction }) // yes, "slid" 439 | if ($.support.transition && this.$element.hasClass('slide')) { 440 | $next.addClass(type) 441 | $next[0].offsetWidth // force reflow 442 | $active.addClass(direction) 443 | $next.addClass(direction) 444 | $active 445 | .one('bsTransitionEnd', function () { 446 | $next.removeClass([type, direction].join(' ')).addClass('active') 447 | $active.removeClass(['active', direction].join(' ')) 448 | that.sliding = false 449 | setTimeout(function () { 450 | that.$element.trigger(slidEvent) 451 | }, 0) 452 | }) 453 | .emulateTransitionEnd(Carousel.TRANSITION_DURATION) 454 | } else { 455 | $active.removeClass('active') 456 | $next.addClass('active') 457 | this.sliding = false 458 | this.$element.trigger(slidEvent) 459 | } 460 | 461 | isCycling && this.cycle() 462 | 463 | return this 464 | } 465 | 466 | 467 | // CAROUSEL PLUGIN DEFINITION 468 | // ========================== 469 | 470 | function Plugin(option) { 471 | return this.each(function () { 472 | var $this = $(this) 473 | var data = $this.data('bs.carousel') 474 | var options = $.extend({}, Carousel.DEFAULTS, $this.data(), typeof option == 'object' && option) 475 | var action = typeof option == 'string' ? option : options.slide 476 | 477 | if (!data) $this.data('bs.carousel', (data = new Carousel(this, options))) 478 | if (typeof option == 'number') data.to(option) 479 | else if (action) data[action]() 480 | else if (options.interval) data.pause().cycle() 481 | }) 482 | } 483 | 484 | var old = $.fn.carousel 485 | 486 | $.fn.carousel = Plugin 487 | $.fn.carousel.Constructor = Carousel 488 | 489 | 490 | // CAROUSEL NO CONFLICT 491 | // ==================== 492 | 493 | $.fn.carousel.noConflict = function () { 494 | $.fn.carousel = old 495 | return this 496 | } 497 | 498 | 499 | // CAROUSEL DATA-API 500 | // ================= 501 | 502 | var clickHandler = function (e) { 503 | var href 504 | var $this = $(this) 505 | var $target = $($this.attr('data-target') || (href = $this.attr('href')) && href.replace(/.*(?=#[^\s]+$)/, '')) // strip for ie7 506 | if (!$target.hasClass('carousel')) return 507 | var options = $.extend({}, $target.data(), $this.data()) 508 | var slideIndex = $this.attr('data-slide-to') 509 | if (slideIndex) options.interval = false 510 | 511 | Plugin.call($target, options) 512 | 513 | if (slideIndex) { 514 | $target.data('bs.carousel').to(slideIndex) 515 | } 516 | 517 | e.preventDefault() 518 | } 519 | 520 | $(document) 521 | .on('click.bs.carousel.data-api', '[data-slide]', clickHandler) 522 | .on('click.bs.carousel.data-api', '[data-slide-to]', clickHandler) 523 | 524 | $(window).on('load', function () { 525 | $('[data-ride="carousel"]').each(function () { 526 | var $carousel = $(this) 527 | Plugin.call($carousel, $carousel.data()) 528 | }) 529 | }) 530 | 531 | }(jQuery); 532 | 533 | /* ======================================================================== 534 | * Bootstrap: collapse.js v3.3.6 535 | * http://getbootstrap.com/javascript/#collapse 536 | * ======================================================================== 537 | * Copyright 2011-2015 Twitter, Inc. 538 | * Licensed under MIT (https://github.com/twbs/bootstrap/blob/master/LICENSE) 539 | * ======================================================================== */ 540 | 541 | 542 | +function ($) { 543 | 'use strict'; 544 | 545 | // COLLAPSE PUBLIC CLASS DEFINITION 546 | // ================================ 547 | 548 | var Collapse = function (element, options) { 549 | this.$element = $(element) 550 | this.options = $.extend({}, Collapse.DEFAULTS, options) 551 | this.$trigger = $('[data-toggle="collapse"][href="#' + element.id + '"],' + 552 | '[data-toggle="collapse"][data-target="#' + element.id + '"]') 553 | this.transitioning = null 554 | 555 | if (this.options.parent) { 556 | this.$parent = this.getParent() 557 | } else { 558 | this.addAriaAndCollapsedClass(this.$element, this.$trigger) 559 | } 560 | 561 | if (this.options.toggle) this.toggle() 562 | } 563 | 564 | Collapse.VERSION = '3.3.6' 565 | 566 | Collapse.TRANSITION_DURATION = 350 567 | 568 | Collapse.DEFAULTS = { 569 | toggle: true 570 | } 571 | 572 | Collapse.prototype.dimension = function () { 573 | var hasWidth = this.$element.hasClass('width') 574 | return hasWidth ? 'width' : 'height' 575 | } 576 | 577 | Collapse.prototype.show = function () { 578 | if (this.transitioning || this.$element.hasClass('in')) return 579 | 580 | var activesData 581 | var actives = this.$parent && this.$parent.children('.panel').children('.in, .collapsing') 582 | 583 | if (actives && actives.length) { 584 | activesData = actives.data('bs.collapse') 585 | if (activesData && activesData.transitioning) return 586 | } 587 | 588 | var startEvent = $.Event('show.bs.collapse') 589 | this.$element.trigger(startEvent) 590 | if (startEvent.isDefaultPrevented()) return 591 | 592 | if (actives && actives.length) { 593 | Plugin.call(actives, 'hide') 594 | activesData || actives.data('bs.collapse', null) 595 | } 596 | 597 | var dimension = this.dimension() 598 | 599 | this.$element 600 | .removeClass('collapse') 601 | .addClass('collapsing')[dimension](0) 602 | .attr('aria-expanded', true) 603 | 604 | this.$trigger 605 | .removeClass('collapsed') 606 | .attr('aria-expanded', true) 607 | 608 | this.transitioning = 1 609 | 610 | var complete = function () { 611 | this.$element 612 | .removeClass('collapsing') 613 | .addClass('collapse in')[dimension]('') 614 | this.transitioning = 0 615 | this.$element 616 | .trigger('shown.bs.collapse') 617 | } 618 | 619 | if (!$.support.transition) return complete.call(this) 620 | 621 | var scrollSize = $.camelCase(['scroll', dimension].join('-')) 622 | 623 | this.$element 624 | .one('bsTransitionEnd', $.proxy(complete, this)) 625 | .emulateTransitionEnd(Collapse.TRANSITION_DURATION)[dimension](this.$element[0][scrollSize]) 626 | } 627 | 628 | Collapse.prototype.hide = function () { 629 | if (this.transitioning || !this.$element.hasClass('in')) return 630 | 631 | var startEvent = $.Event('hide.bs.collapse') 632 | this.$element.trigger(startEvent) 633 | if (startEvent.isDefaultPrevented()) return 634 | 635 | var dimension = this.dimension() 636 | 637 | this.$element[dimension](this.$element[dimension]())[0].offsetHeight 638 | 639 | this.$element 640 | .addClass('collapsing') 641 | .removeClass('collapse in') 642 | .attr('aria-expanded', false) 643 | 644 | this.$trigger 645 | .addClass('collapsed') 646 | .attr('aria-expanded', false) 647 | 648 | this.transitioning = 1 649 | 650 | var complete = function () { 651 | this.transitioning = 0 652 | this.$element 653 | .removeClass('collapsing') 654 | .addClass('collapse') 655 | .trigger('hidden.bs.collapse') 656 | } 657 | 658 | if (!$.support.transition) return complete.call(this) 659 | 660 | this.$element 661 | [dimension](0) 662 | .one('bsTransitionEnd', $.proxy(complete, this)) 663 | .emulateTransitionEnd(Collapse.TRANSITION_DURATION) 664 | } 665 | 666 | Collapse.prototype.toggle = function () { 667 | this[this.$element.hasClass('in') ? 'hide' : 'show']() 668 | } 669 | 670 | Collapse.prototype.getParent = function () { 671 | return $(this.options.parent) 672 | .find('[data-toggle="collapse"][data-parent="' + this.options.parent + '"]') 673 | .each($.proxy(function (i, element) { 674 | var $element = $(element) 675 | this.addAriaAndCollapsedClass(getTargetFromTrigger($element), $element) 676 | }, this)) 677 | .end() 678 | } 679 | 680 | Collapse.prototype.addAriaAndCollapsedClass = function ($element, $trigger) { 681 | var isOpen = $element.hasClass('in') 682 | 683 | $element.attr('aria-expanded', isOpen) 684 | $trigger 685 | .toggleClass('collapsed', !isOpen) 686 | .attr('aria-expanded', isOpen) 687 | } 688 | 689 | function getTargetFromTrigger($trigger) { 690 | var href 691 | var target = $trigger.attr('data-target') 692 | || (href = $trigger.attr('href')) && href.replace(/.*(?=#[^\s]+$)/, '') // strip for ie7 693 | 694 | return $(target) 695 | } 696 | 697 | 698 | // COLLAPSE PLUGIN DEFINITION 699 | // ========================== 700 | 701 | function Plugin(option) { 702 | return this.each(function () { 703 | var $this = $(this) 704 | var data = $this.data('bs.collapse') 705 | var options = $.extend({}, Collapse.DEFAULTS, $this.data(), typeof option == 'object' && option) 706 | 707 | if (!data && options.toggle && /show|hide/.test(option)) options.toggle = false 708 | if (!data) $this.data('bs.collapse', (data = new Collapse(this, options))) 709 | if (typeof option == 'string') data[option]() 710 | }) 711 | } 712 | 713 | var old = $.fn.collapse 714 | 715 | $.fn.collapse = Plugin 716 | $.fn.collapse.Constructor = Collapse 717 | 718 | 719 | // COLLAPSE NO CONFLICT 720 | // ==================== 721 | 722 | $.fn.collapse.noConflict = function () { 723 | $.fn.collapse = old 724 | return this 725 | } 726 | 727 | 728 | // COLLAPSE DATA-API 729 | // ================= 730 | 731 | $(document).on('click.bs.collapse.data-api', '[data-toggle="collapse"]', function (e) { 732 | var $this = $(this) 733 | 734 | if (!$this.attr('data-target')) e.preventDefault() 735 | 736 | var $target = getTargetFromTrigger($this) 737 | var data = $target.data('bs.collapse') 738 | var option = data ? 'toggle' : $this.data() 739 | 740 | Plugin.call($target, option) 741 | }) 742 | 743 | }(jQuery); 744 | 745 | /* ======================================================================== 746 | * Bootstrap: dropdown.js v3.3.6 747 | * http://getbootstrap.com/javascript/#dropdowns 748 | * ======================================================================== 749 | * Copyright 2011-2015 Twitter, Inc. 750 | * Licensed under MIT (https://github.com/twbs/bootstrap/blob/master/LICENSE) 751 | * ======================================================================== */ 752 | 753 | 754 | +function ($) { 755 | 'use strict'; 756 | 757 | // DROPDOWN CLASS DEFINITION 758 | // ========================= 759 | 760 | var backdrop = '.dropdown-backdrop' 761 | var toggle = '[data-toggle="dropdown"]' 762 | var Dropdown = function (element) { 763 | $(element).on('click.bs.dropdown', this.toggle) 764 | } 765 | 766 | Dropdown.VERSION = '3.3.6' 767 | 768 | function getParent($this) { 769 | var selector = $this.attr('data-target') 770 | 771 | if (!selector) { 772 | selector = $this.attr('href') 773 | selector = selector && /#[A-Za-z]/.test(selector) && selector.replace(/.*(?=#[^\s]*$)/, '') // strip for ie7 774 | } 775 | 776 | var $parent = selector && $(selector) 777 | 778 | return $parent && $parent.length ? $parent : $this.parent() 779 | } 780 | 781 | function clearMenus(e) { 782 | if (e && e.which === 3) return 783 | $(backdrop).remove() 784 | $(toggle).each(function () { 785 | var $this = $(this) 786 | var $parent = getParent($this) 787 | var relatedTarget = { relatedTarget: this } 788 | 789 | if (!$parent.hasClass('open')) return 790 | 791 | if (e && e.type == 'click' && /input|textarea/i.test(e.target.tagName) && $.contains($parent[0], e.target)) return 792 | 793 | $parent.trigger(e = $.Event('hide.bs.dropdown', relatedTarget)) 794 | 795 | if (e.isDefaultPrevented()) return 796 | 797 | $this.attr('aria-expanded', 'false') 798 | $parent.removeClass('open').trigger($.Event('hidden.bs.dropdown', relatedTarget)) 799 | }) 800 | } 801 | 802 | Dropdown.prototype.toggle = function (e) { 803 | var $this = $(this) 804 | 805 | if ($this.is('.disabled, :disabled')) return 806 | 807 | var $parent = getParent($this) 808 | var isActive = $parent.hasClass('open') 809 | 810 | clearMenus() 811 | 812 | if (!isActive) { 813 | if ('ontouchstart' in document.documentElement && !$parent.closest('.navbar-nav').length) { 814 | // if mobile we use a backdrop because click events don't delegate 815 | $(document.createElement('div')) 816 | .addClass('dropdown-backdrop') 817 | .insertAfter($(this)) 818 | .on('click', clearMenus) 819 | } 820 | 821 | var relatedTarget = { relatedTarget: this } 822 | $parent.trigger(e = $.Event('show.bs.dropdown', relatedTarget)) 823 | 824 | if (e.isDefaultPrevented()) return 825 | 826 | $this 827 | .trigger('focus') 828 | .attr('aria-expanded', 'true') 829 | 830 | $parent 831 | .toggleClass('open') 832 | .trigger($.Event('shown.bs.dropdown', relatedTarget)) 833 | } 834 | 835 | return false 836 | } 837 | 838 | Dropdown.prototype.keydown = function (e) { 839 | if (!/(38|40|27|32)/.test(e.which) || /input|textarea/i.test(e.target.tagName)) return 840 | 841 | var $this = $(this) 842 | 843 | e.preventDefault() 844 | e.stopPropagation() 845 | 846 | if ($this.is('.disabled, :disabled')) return 847 | 848 | var $parent = getParent($this) 849 | var isActive = $parent.hasClass('open') 850 | 851 | if (!isActive && e.which != 27 || isActive && e.which == 27) { 852 | if (e.which == 27) $parent.find(toggle).trigger('focus') 853 | return $this.trigger('click') 854 | } 855 | 856 | var desc = ' li:not(.disabled):visible a' 857 | var $items = $parent.find('.dropdown-menu' + desc) 858 | 859 | if (!$items.length) return 860 | 861 | var index = $items.index(e.target) 862 | 863 | if (e.which == 38 && index > 0) index-- // up 864 | if (e.which == 40 && index < $items.length - 1) index++ // down 865 | if (!~index) index = 0 866 | 867 | $items.eq(index).trigger('focus') 868 | } 869 | 870 | 871 | // DROPDOWN PLUGIN DEFINITION 872 | // ========================== 873 | 874 | function Plugin(option) { 875 | return this.each(function () { 876 | var $this = $(this) 877 | var data = $this.data('bs.dropdown') 878 | 879 | if (!data) $this.data('bs.dropdown', (data = new Dropdown(this))) 880 | if (typeof option == 'string') data[option].call($this) 881 | }) 882 | } 883 | 884 | var old = $.fn.dropdown 885 | 886 | $.fn.dropdown = Plugin 887 | $.fn.dropdown.Constructor = Dropdown 888 | 889 | 890 | // DROPDOWN NO CONFLICT 891 | // ==================== 892 | 893 | $.fn.dropdown.noConflict = function () { 894 | $.fn.dropdown = old 895 | return this 896 | } 897 | 898 | 899 | // APPLY TO STANDARD DROPDOWN ELEMENTS 900 | // =================================== 901 | 902 | $(document) 903 | .on('click.bs.dropdown.data-api', clearMenus) 904 | .on('click.bs.dropdown.data-api', '.dropdown form', function (e) { e.stopPropagation() }) 905 | .on('click.bs.dropdown.data-api', toggle, Dropdown.prototype.toggle) 906 | .on('keydown.bs.dropdown.data-api', toggle, Dropdown.prototype.keydown) 907 | .on('keydown.bs.dropdown.data-api', '.dropdown-menu', Dropdown.prototype.keydown) 908 | 909 | }(jQuery); 910 | 911 | /* ======================================================================== 912 | * Bootstrap: modal.js v3.3.6 913 | * http://getbootstrap.com/javascript/#modals 914 | * ======================================================================== 915 | * Copyright 2011-2015 Twitter, Inc. 916 | * Licensed under MIT (https://github.com/twbs/bootstrap/blob/master/LICENSE) 917 | * ======================================================================== */ 918 | 919 | 920 | +function ($) { 921 | 'use strict'; 922 | 923 | // MODAL CLASS DEFINITION 924 | // ====================== 925 | 926 | var Modal = function (element, options) { 927 | this.options = options 928 | this.$body = $(document.body) 929 | this.$element = $(element) 930 | this.$dialog = this.$element.find('.modal-dialog') 931 | this.$backdrop = null 932 | this.isShown = null 933 | this.originalBodyPad = null 934 | this.scrollbarWidth = 0 935 | this.ignoreBackdropClick = false 936 | 937 | if (this.options.remote) { 938 | this.$element 939 | .find('.modal-content') 940 | .load(this.options.remote, $.proxy(function () { 941 | this.$element.trigger('loaded.bs.modal') 942 | }, this)) 943 | } 944 | } 945 | 946 | Modal.VERSION = '3.3.6' 947 | 948 | Modal.TRANSITION_DURATION = 300 949 | Modal.BACKDROP_TRANSITION_DURATION = 150 950 | 951 | Modal.DEFAULTS = { 952 | backdrop: true, 953 | keyboard: true, 954 | show: true 955 | } 956 | 957 | Modal.prototype.toggle = function (_relatedTarget) { 958 | return this.isShown ? this.hide() : this.show(_relatedTarget) 959 | } 960 | 961 | Modal.prototype.show = function (_relatedTarget) { 962 | var that = this 963 | var e = $.Event('show.bs.modal', { relatedTarget: _relatedTarget }) 964 | 965 | this.$element.trigger(e) 966 | 967 | if (this.isShown || e.isDefaultPrevented()) return 968 | 969 | this.isShown = true 970 | 971 | this.checkScrollbar() 972 | this.setScrollbar() 973 | this.$body.addClass('modal-open') 974 | 975 | this.escape() 976 | this.resize() 977 | 978 | this.$element.on('click.dismiss.bs.modal', '[data-dismiss="modal"]', $.proxy(this.hide, this)) 979 | 980 | this.$dialog.on('mousedown.dismiss.bs.modal', function () { 981 | that.$element.one('mouseup.dismiss.bs.modal', function (e) { 982 | if ($(e.target).is(that.$element)) that.ignoreBackdropClick = true 983 | }) 984 | }) 985 | 986 | this.backdrop(function () { 987 | var transition = $.support.transition && that.$element.hasClass('fade') 988 | 989 | if (!that.$element.parent().length) { 990 | that.$element.appendTo(that.$body) // don't move modals dom position 991 | } 992 | 993 | that.$element 994 | .show() 995 | .scrollTop(0) 996 | 997 | that.adjustDialog() 998 | 999 | if (transition) { 1000 | that.$element[0].offsetWidth // force reflow 1001 | } 1002 | 1003 | that.$element.addClass('in') 1004 | 1005 | that.enforceFocus() 1006 | 1007 | var e = $.Event('shown.bs.modal', { relatedTarget: _relatedTarget }) 1008 | 1009 | transition ? 1010 | that.$dialog // wait for modal to slide in 1011 | .one('bsTransitionEnd', function () { 1012 | that.$element.trigger('focus').trigger(e) 1013 | }) 1014 | .emulateTransitionEnd(Modal.TRANSITION_DURATION) : 1015 | that.$element.trigger('focus').trigger(e) 1016 | }) 1017 | } 1018 | 1019 | Modal.prototype.hide = function (e) { 1020 | if (e) e.preventDefault() 1021 | 1022 | e = $.Event('hide.bs.modal') 1023 | 1024 | this.$element.trigger(e) 1025 | 1026 | if (!this.isShown || e.isDefaultPrevented()) return 1027 | 1028 | this.isShown = false 1029 | 1030 | this.escape() 1031 | this.resize() 1032 | 1033 | $(document).off('focusin.bs.modal') 1034 | 1035 | this.$element 1036 | .removeClass('in') 1037 | .off('click.dismiss.bs.modal') 1038 | .off('mouseup.dismiss.bs.modal') 1039 | 1040 | this.$dialog.off('mousedown.dismiss.bs.modal') 1041 | 1042 | $.support.transition && this.$element.hasClass('fade') ? 1043 | this.$element 1044 | .one('bsTransitionEnd', $.proxy(this.hideModal, this)) 1045 | .emulateTransitionEnd(Modal.TRANSITION_DURATION) : 1046 | this.hideModal() 1047 | } 1048 | 1049 | Modal.prototype.enforceFocus = function () { 1050 | $(document) 1051 | .off('focusin.bs.modal') // guard against infinite focus loop 1052 | .on('focusin.bs.modal', $.proxy(function (e) { 1053 | if (this.$element[0] !== e.target && !this.$element.has(e.target).length) { 1054 | this.$element.trigger('focus') 1055 | } 1056 | }, this)) 1057 | } 1058 | 1059 | Modal.prototype.escape = function () { 1060 | if (this.isShown && this.options.keyboard) { 1061 | this.$element.on('keydown.dismiss.bs.modal', $.proxy(function (e) { 1062 | e.which == 27 && this.hide() 1063 | }, this)) 1064 | } else if (!this.isShown) { 1065 | this.$element.off('keydown.dismiss.bs.modal') 1066 | } 1067 | } 1068 | 1069 | Modal.prototype.resize = function () { 1070 | if (this.isShown) { 1071 | $(window).on('resize.bs.modal', $.proxy(this.handleUpdate, this)) 1072 | } else { 1073 | $(window).off('resize.bs.modal') 1074 | } 1075 | } 1076 | 1077 | Modal.prototype.hideModal = function () { 1078 | var that = this 1079 | this.$element.hide() 1080 | this.backdrop(function () { 1081 | that.$body.removeClass('modal-open') 1082 | that.resetAdjustments() 1083 | that.resetScrollbar() 1084 | that.$element.trigger('hidden.bs.modal') 1085 | }) 1086 | } 1087 | 1088 | Modal.prototype.removeBackdrop = function () { 1089 | this.$backdrop && this.$backdrop.remove() 1090 | this.$backdrop = null 1091 | } 1092 | 1093 | Modal.prototype.backdrop = function (callback) { 1094 | var that = this 1095 | var animate = this.$element.hasClass('fade') ? 'fade' : '' 1096 | 1097 | if (this.isShown && this.options.backdrop) { 1098 | var doAnimate = $.support.transition && animate 1099 | 1100 | this.$backdrop = $(document.createElement('div')) 1101 | .addClass('modal-backdrop ' + animate) 1102 | .appendTo(this.$body) 1103 | 1104 | this.$element.on('click.dismiss.bs.modal', $.proxy(function (e) { 1105 | if (this.ignoreBackdropClick) { 1106 | this.ignoreBackdropClick = false 1107 | return 1108 | } 1109 | if (e.target !== e.currentTarget) return 1110 | this.options.backdrop == 'static' 1111 | ? this.$element[0].focus() 1112 | : this.hide() 1113 | }, this)) 1114 | 1115 | if (doAnimate) this.$backdrop[0].offsetWidth // force reflow 1116 | 1117 | this.$backdrop.addClass('in') 1118 | 1119 | if (!callback) return 1120 | 1121 | doAnimate ? 1122 | this.$backdrop 1123 | .one('bsTransitionEnd', callback) 1124 | .emulateTransitionEnd(Modal.BACKDROP_TRANSITION_DURATION) : 1125 | callback() 1126 | 1127 | } else if (!this.isShown && this.$backdrop) { 1128 | this.$backdrop.removeClass('in') 1129 | 1130 | var callbackRemove = function () { 1131 | that.removeBackdrop() 1132 | callback && callback() 1133 | } 1134 | $.support.transition && this.$element.hasClass('fade') ? 1135 | this.$backdrop 1136 | .one('bsTransitionEnd', callbackRemove) 1137 | .emulateTransitionEnd(Modal.BACKDROP_TRANSITION_DURATION) : 1138 | callbackRemove() 1139 | 1140 | } else if (callback) { 1141 | callback() 1142 | } 1143 | } 1144 | 1145 | // these following methods are used to handle overflowing modals 1146 | 1147 | Modal.prototype.handleUpdate = function () { 1148 | this.adjustDialog() 1149 | } 1150 | 1151 | Modal.prototype.adjustDialog = function () { 1152 | var modalIsOverflowing = this.$element[0].scrollHeight > document.documentElement.clientHeight 1153 | 1154 | this.$element.css({ 1155 | paddingLeft: !this.bodyIsOverflowing && modalIsOverflowing ? this.scrollbarWidth : '', 1156 | paddingRight: this.bodyIsOverflowing && !modalIsOverflowing ? this.scrollbarWidth : '' 1157 | }) 1158 | } 1159 | 1160 | Modal.prototype.resetAdjustments = function () { 1161 | this.$element.css({ 1162 | paddingLeft: '', 1163 | paddingRight: '' 1164 | }) 1165 | } 1166 | 1167 | Modal.prototype.checkScrollbar = function () { 1168 | var fullWindowWidth = window.innerWidth 1169 | if (!fullWindowWidth) { // workaround for missing window.innerWidth in IE8 1170 | var documentElementRect = document.documentElement.getBoundingClientRect() 1171 | fullWindowWidth = documentElementRect.right - Math.abs(documentElementRect.left) 1172 | } 1173 | this.bodyIsOverflowing = document.body.clientWidth < fullWindowWidth 1174 | this.scrollbarWidth = this.measureScrollbar() 1175 | } 1176 | 1177 | Modal.prototype.setScrollbar = function () { 1178 | var bodyPad = parseInt((this.$body.css('padding-right') || 0), 10) 1179 | this.originalBodyPad = document.body.style.paddingRight || '' 1180 | if (this.bodyIsOverflowing) this.$body.css('padding-right', bodyPad + this.scrollbarWidth) 1181 | } 1182 | 1183 | Modal.prototype.resetScrollbar = function () { 1184 | this.$body.css('padding-right', this.originalBodyPad) 1185 | } 1186 | 1187 | Modal.prototype.measureScrollbar = function () { // thx walsh 1188 | var scrollDiv = document.createElement('div') 1189 | scrollDiv.className = 'modal-scrollbar-measure' 1190 | this.$body.append(scrollDiv) 1191 | var scrollbarWidth = scrollDiv.offsetWidth - scrollDiv.clientWidth 1192 | this.$body[0].removeChild(scrollDiv) 1193 | return scrollbarWidth 1194 | } 1195 | 1196 | 1197 | // MODAL PLUGIN DEFINITION 1198 | // ======================= 1199 | 1200 | function Plugin(option, _relatedTarget) { 1201 | return this.each(function () { 1202 | var $this = $(this) 1203 | var data = $this.data('bs.modal') 1204 | var options = $.extend({}, Modal.DEFAULTS, $this.data(), typeof option == 'object' && option) 1205 | 1206 | if (!data) $this.data('bs.modal', (data = new Modal(this, options))) 1207 | if (typeof option == 'string') data[option](_relatedTarget) 1208 | else if (options.show) data.show(_relatedTarget) 1209 | }) 1210 | } 1211 | 1212 | var old = $.fn.modal 1213 | 1214 | $.fn.modal = Plugin 1215 | $.fn.modal.Constructor = Modal 1216 | 1217 | 1218 | // MODAL NO CONFLICT 1219 | // ================= 1220 | 1221 | $.fn.modal.noConflict = function () { 1222 | $.fn.modal = old 1223 | return this 1224 | } 1225 | 1226 | 1227 | // MODAL DATA-API 1228 | // ============== 1229 | 1230 | $(document).on('click.bs.modal.data-api', '[data-toggle="modal"]', function (e) { 1231 | var $this = $(this) 1232 | var href = $this.attr('href') 1233 | var $target = $($this.attr('data-target') || (href && href.replace(/.*(?=#[^\s]+$)/, ''))) // strip for ie7 1234 | var option = $target.data('bs.modal') ? 'toggle' : $.extend({ remote: !/#/.test(href) && href }, $target.data(), $this.data()) 1235 | 1236 | if ($this.is('a')) e.preventDefault() 1237 | 1238 | $target.one('show.bs.modal', function (showEvent) { 1239 | if (showEvent.isDefaultPrevented()) return // only register focus restorer if modal will actually get shown 1240 | $target.one('hidden.bs.modal', function () { 1241 | $this.is(':visible') && $this.trigger('focus') 1242 | }) 1243 | }) 1244 | Plugin.call($target, option, this) 1245 | }) 1246 | 1247 | }(jQuery); 1248 | 1249 | /* ======================================================================== 1250 | * Bootstrap: tooltip.js v3.3.6 1251 | * http://getbootstrap.com/javascript/#tooltip 1252 | * Inspired by the original jQuery.tipsy by Jason Frame 1253 | * ======================================================================== 1254 | * Copyright 2011-2015 Twitter, Inc. 1255 | * Licensed under MIT (https://github.com/twbs/bootstrap/blob/master/LICENSE) 1256 | * ======================================================================== */ 1257 | 1258 | 1259 | +function ($) { 1260 | 'use strict'; 1261 | 1262 | // TOOLTIP PUBLIC CLASS DEFINITION 1263 | // =============================== 1264 | 1265 | var Tooltip = function (element, options) { 1266 | this.type = null 1267 | this.options = null 1268 | this.enabled = null 1269 | this.timeout = null 1270 | this.hoverState = null 1271 | this.$element = null 1272 | this.inState = null 1273 | 1274 | this.init('tooltip', element, options) 1275 | } 1276 | 1277 | Tooltip.VERSION = '3.3.6' 1278 | 1279 | Tooltip.TRANSITION_DURATION = 150 1280 | 1281 | Tooltip.DEFAULTS = { 1282 | animation: true, 1283 | placement: 'top', 1284 | selector: false, 1285 | template: '', 1286 | trigger: 'hover focus', 1287 | title: '', 1288 | delay: 0, 1289 | html: false, 1290 | container: false, 1291 | viewport: { 1292 | selector: 'body', 1293 | padding: 0 1294 | } 1295 | } 1296 | 1297 | Tooltip.prototype.init = function (type, element, options) { 1298 | this.enabled = true 1299 | this.type = type 1300 | this.$element = $(element) 1301 | this.options = this.getOptions(options) 1302 | this.$viewport = this.options.viewport && $($.isFunction(this.options.viewport) ? this.options.viewport.call(this, this.$element) : (this.options.viewport.selector || this.options.viewport)) 1303 | this.inState = { click: false, hover: false, focus: false } 1304 | 1305 | if (this.$element[0] instanceof document.constructor && !this.options.selector) { 1306 | throw new Error('`selector` option must be specified when initializing ' + this.type + ' on the window.document object!') 1307 | } 1308 | 1309 | var triggers = this.options.trigger.split(' ') 1310 | 1311 | for (var i = triggers.length; i--;) { 1312 | var trigger = triggers[i] 1313 | 1314 | if (trigger == 'click') { 1315 | this.$element.on('click.' + this.type, this.options.selector, $.proxy(this.toggle, this)) 1316 | } else if (trigger != 'manual') { 1317 | var eventIn = trigger == 'hover' ? 'mouseenter' : 'focusin' 1318 | var eventOut = trigger == 'hover' ? 'mouseleave' : 'focusout' 1319 | 1320 | this.$element.on(eventIn + '.' + this.type, this.options.selector, $.proxy(this.enter, this)) 1321 | this.$element.on(eventOut + '.' + this.type, this.options.selector, $.proxy(this.leave, this)) 1322 | } 1323 | } 1324 | 1325 | this.options.selector ? 1326 | (this._options = $.extend({}, this.options, { trigger: 'manual', selector: '' })) : 1327 | this.fixTitle() 1328 | } 1329 | 1330 | Tooltip.prototype.getDefaults = function () { 1331 | return Tooltip.DEFAULTS 1332 | } 1333 | 1334 | Tooltip.prototype.getOptions = function (options) { 1335 | options = $.extend({}, this.getDefaults(), this.$element.data(), options) 1336 | 1337 | if (options.delay && typeof options.delay == 'number') { 1338 | options.delay = { 1339 | show: options.delay, 1340 | hide: options.delay 1341 | } 1342 | } 1343 | 1344 | return options 1345 | } 1346 | 1347 | Tooltip.prototype.getDelegateOptions = function () { 1348 | var options = {} 1349 | var defaults = this.getDefaults() 1350 | 1351 | this._options && $.each(this._options, function (key, value) { 1352 | if (defaults[key] != value) options[key] = value 1353 | }) 1354 | 1355 | return options 1356 | } 1357 | 1358 | Tooltip.prototype.enter = function (obj) { 1359 | var self = obj instanceof this.constructor ? 1360 | obj : $(obj.currentTarget).data('bs.' + this.type) 1361 | 1362 | if (!self) { 1363 | self = new this.constructor(obj.currentTarget, this.getDelegateOptions()) 1364 | $(obj.currentTarget).data('bs.' + this.type, self) 1365 | } 1366 | 1367 | if (obj instanceof $.Event) { 1368 | self.inState[obj.type == 'focusin' ? 'focus' : 'hover'] = true 1369 | } 1370 | 1371 | if (self.tip().hasClass('in') || self.hoverState == 'in') { 1372 | self.hoverState = 'in' 1373 | return 1374 | } 1375 | 1376 | clearTimeout(self.timeout) 1377 | 1378 | self.hoverState = 'in' 1379 | 1380 | if (!self.options.delay || !self.options.delay.show) return self.show() 1381 | 1382 | self.timeout = setTimeout(function () { 1383 | if (self.hoverState == 'in') self.show() 1384 | }, self.options.delay.show) 1385 | } 1386 | 1387 | Tooltip.prototype.isInStateTrue = function () { 1388 | for (var key in this.inState) { 1389 | if (this.inState[key]) return true 1390 | } 1391 | 1392 | return false 1393 | } 1394 | 1395 | Tooltip.prototype.leave = function (obj) { 1396 | var self = obj instanceof this.constructor ? 1397 | obj : $(obj.currentTarget).data('bs.' + this.type) 1398 | 1399 | if (!self) { 1400 | self = new this.constructor(obj.currentTarget, this.getDelegateOptions()) 1401 | $(obj.currentTarget).data('bs.' + this.type, self) 1402 | } 1403 | 1404 | if (obj instanceof $.Event) { 1405 | self.inState[obj.type == 'focusout' ? 'focus' : 'hover'] = false 1406 | } 1407 | 1408 | if (self.isInStateTrue()) return 1409 | 1410 | clearTimeout(self.timeout) 1411 | 1412 | self.hoverState = 'out' 1413 | 1414 | if (!self.options.delay || !self.options.delay.hide) return self.hide() 1415 | 1416 | self.timeout = setTimeout(function () { 1417 | if (self.hoverState == 'out') self.hide() 1418 | }, self.options.delay.hide) 1419 | } 1420 | 1421 | Tooltip.prototype.show = function () { 1422 | var e = $.Event('show.bs.' + this.type) 1423 | 1424 | if (this.hasContent() && this.enabled) { 1425 | this.$element.trigger(e) 1426 | 1427 | var inDom = $.contains(this.$element[0].ownerDocument.documentElement, this.$element[0]) 1428 | if (e.isDefaultPrevented() || !inDom) return 1429 | var that = this 1430 | 1431 | var $tip = this.tip() 1432 | 1433 | var tipId = this.getUID(this.type) 1434 | 1435 | this.setContent() 1436 | $tip.attr('id', tipId) 1437 | this.$element.attr('aria-describedby', tipId) 1438 | 1439 | if (this.options.animation) $tip.addClass('fade') 1440 | 1441 | var placement = typeof this.options.placement == 'function' ? 1442 | this.options.placement.call(this, $tip[0], this.$element[0]) : 1443 | this.options.placement 1444 | 1445 | var autoToken = /\s?auto?\s?/i 1446 | var autoPlace = autoToken.test(placement) 1447 | if (autoPlace) placement = placement.replace(autoToken, '') || 'top' 1448 | 1449 | $tip 1450 | .detach() 1451 | .css({ top: 0, left: 0, display: 'block' }) 1452 | .addClass(placement) 1453 | .data('bs.' + this.type, this) 1454 | 1455 | this.options.container ? $tip.appendTo(this.options.container) : $tip.insertAfter(this.$element) 1456 | this.$element.trigger('inserted.bs.' + this.type) 1457 | 1458 | var pos = this.getPosition() 1459 | var actualWidth = $tip[0].offsetWidth 1460 | var actualHeight = $tip[0].offsetHeight 1461 | 1462 | if (autoPlace) { 1463 | var orgPlacement = placement 1464 | var viewportDim = this.getPosition(this.$viewport) 1465 | 1466 | placement = placement == 'bottom' && pos.bottom + actualHeight > viewportDim.bottom ? 'top' : 1467 | placement == 'top' && pos.top - actualHeight < viewportDim.top ? 'bottom' : 1468 | placement == 'right' && pos.right + actualWidth > viewportDim.width ? 'left' : 1469 | placement == 'left' && pos.left - actualWidth < viewportDim.left ? 'right' : 1470 | placement 1471 | 1472 | $tip 1473 | .removeClass(orgPlacement) 1474 | .addClass(placement) 1475 | } 1476 | 1477 | var calculatedOffset = this.getCalculatedOffset(placement, pos, actualWidth, actualHeight) 1478 | 1479 | this.applyPlacement(calculatedOffset, placement) 1480 | 1481 | var complete = function () { 1482 | var prevHoverState = that.hoverState 1483 | that.$element.trigger('shown.bs.' + that.type) 1484 | that.hoverState = null 1485 | 1486 | if (prevHoverState == 'out') that.leave(that) 1487 | } 1488 | 1489 | $.support.transition && this.$tip.hasClass('fade') ? 1490 | $tip 1491 | .one('bsTransitionEnd', complete) 1492 | .emulateTransitionEnd(Tooltip.TRANSITION_DURATION) : 1493 | complete() 1494 | } 1495 | } 1496 | 1497 | Tooltip.prototype.applyPlacement = function (offset, placement) { 1498 | var $tip = this.tip() 1499 | var width = $tip[0].offsetWidth 1500 | var height = $tip[0].offsetHeight 1501 | 1502 | // manually read margins because getBoundingClientRect includes difference 1503 | var marginTop = parseInt($tip.css('margin-top'), 10) 1504 | var marginLeft = parseInt($tip.css('margin-left'), 10) 1505 | 1506 | // we must check for NaN for ie 8/9 1507 | if (isNaN(marginTop)) marginTop = 0 1508 | if (isNaN(marginLeft)) marginLeft = 0 1509 | 1510 | offset.top += marginTop 1511 | offset.left += marginLeft 1512 | 1513 | // $.fn.offset doesn't round pixel values 1514 | // so we use setOffset directly with our own function B-0 1515 | $.offset.setOffset($tip[0], $.extend({ 1516 | using: function (props) { 1517 | $tip.css({ 1518 | top: Math.round(props.top), 1519 | left: Math.round(props.left) 1520 | }) 1521 | } 1522 | }, offset), 0) 1523 | 1524 | $tip.addClass('in') 1525 | 1526 | // check to see if placing tip in new offset caused the tip to resize itself 1527 | var actualWidth = $tip[0].offsetWidth 1528 | var actualHeight = $tip[0].offsetHeight 1529 | 1530 | if (placement == 'top' && actualHeight != height) { 1531 | offset.top = offset.top + height - actualHeight 1532 | } 1533 | 1534 | var delta = this.getViewportAdjustedDelta(placement, offset, actualWidth, actualHeight) 1535 | 1536 | if (delta.left) offset.left += delta.left 1537 | else offset.top += delta.top 1538 | 1539 | var isVertical = /top|bottom/.test(placement) 1540 | var arrowDelta = isVertical ? delta.left * 2 - width + actualWidth : delta.top * 2 - height + actualHeight 1541 | var arrowOffsetPosition = isVertical ? 'offsetWidth' : 'offsetHeight' 1542 | 1543 | $tip.offset(offset) 1544 | this.replaceArrow(arrowDelta, $tip[0][arrowOffsetPosition], isVertical) 1545 | } 1546 | 1547 | Tooltip.prototype.replaceArrow = function (delta, dimension, isVertical) { 1548 | this.arrow() 1549 | .css(isVertical ? 'left' : 'top', 50 * (1 - delta / dimension) + '%') 1550 | .css(isVertical ? 'top' : 'left', '') 1551 | } 1552 | 1553 | Tooltip.prototype.setContent = function () { 1554 | var $tip = this.tip() 1555 | var title = this.getTitle() 1556 | 1557 | $tip.find('.tooltip-inner')[this.options.html ? 'html' : 'text'](title) 1558 | $tip.removeClass('fade in top bottom left right') 1559 | } 1560 | 1561 | Tooltip.prototype.hide = function (callback) { 1562 | var that = this 1563 | var $tip = $(this.$tip) 1564 | var e = $.Event('hide.bs.' + this.type) 1565 | 1566 | function complete() { 1567 | if (that.hoverState != 'in') $tip.detach() 1568 | that.$element 1569 | .removeAttr('aria-describedby') 1570 | .trigger('hidden.bs.' + that.type) 1571 | callback && callback() 1572 | } 1573 | 1574 | this.$element.trigger(e) 1575 | 1576 | if (e.isDefaultPrevented()) return 1577 | 1578 | $tip.removeClass('in') 1579 | 1580 | $.support.transition && $tip.hasClass('fade') ? 1581 | $tip 1582 | .one('bsTransitionEnd', complete) 1583 | .emulateTransitionEnd(Tooltip.TRANSITION_DURATION) : 1584 | complete() 1585 | 1586 | this.hoverState = null 1587 | 1588 | return this 1589 | } 1590 | 1591 | Tooltip.prototype.fixTitle = function () { 1592 | var $e = this.$element 1593 | if ($e.attr('title') || typeof $e.attr('data-original-title') != 'string') { 1594 | $e.attr('data-original-title', $e.attr('title') || '').attr('title', '') 1595 | } 1596 | } 1597 | 1598 | Tooltip.prototype.hasContent = function () { 1599 | return this.getTitle() 1600 | } 1601 | 1602 | Tooltip.prototype.getPosition = function ($element) { 1603 | $element = $element || this.$element 1604 | 1605 | var el = $element[0] 1606 | var isBody = el.tagName == 'BODY' 1607 | 1608 | var elRect = el.getBoundingClientRect() 1609 | if (elRect.width == null) { 1610 | // width and height are missing in IE8, so compute them manually; see https://github.com/twbs/bootstrap/issues/14093 1611 | elRect = $.extend({}, elRect, { width: elRect.right - elRect.left, height: elRect.bottom - elRect.top }) 1612 | } 1613 | var elOffset = isBody ? { top: 0, left: 0 } : $element.offset() 1614 | var scroll = { scroll: isBody ? document.documentElement.scrollTop || document.body.scrollTop : $element.scrollTop() } 1615 | var outerDims = isBody ? { width: $(window).width(), height: $(window).height() } : null 1616 | 1617 | return $.extend({}, elRect, scroll, outerDims, elOffset) 1618 | } 1619 | 1620 | Tooltip.prototype.getCalculatedOffset = function (placement, pos, actualWidth, actualHeight) { 1621 | return placement == 'bottom' ? { top: pos.top + pos.height, left: pos.left + pos.width / 2 - actualWidth / 2 } : 1622 | placement == 'top' ? { top: pos.top - actualHeight, left: pos.left + pos.width / 2 - actualWidth / 2 } : 1623 | placement == 'left' ? { top: pos.top + pos.height / 2 - actualHeight / 2, left: pos.left - actualWidth } : 1624 | /* placement == 'right' */ { top: pos.top + pos.height / 2 - actualHeight / 2, left: pos.left + pos.width } 1625 | 1626 | } 1627 | 1628 | Tooltip.prototype.getViewportAdjustedDelta = function (placement, pos, actualWidth, actualHeight) { 1629 | var delta = { top: 0, left: 0 } 1630 | if (!this.$viewport) return delta 1631 | 1632 | var viewportPadding = this.options.viewport && this.options.viewport.padding || 0 1633 | var viewportDimensions = this.getPosition(this.$viewport) 1634 | 1635 | if (/right|left/.test(placement)) { 1636 | var topEdgeOffset = pos.top - viewportPadding - viewportDimensions.scroll 1637 | var bottomEdgeOffset = pos.top + viewportPadding - viewportDimensions.scroll + actualHeight 1638 | if (topEdgeOffset < viewportDimensions.top) { // top overflow 1639 | delta.top = viewportDimensions.top - topEdgeOffset 1640 | } else if (bottomEdgeOffset > viewportDimensions.top + viewportDimensions.height) { // bottom overflow 1641 | delta.top = viewportDimensions.top + viewportDimensions.height - bottomEdgeOffset 1642 | } 1643 | } else { 1644 | var leftEdgeOffset = pos.left - viewportPadding 1645 | var rightEdgeOffset = pos.left + viewportPadding + actualWidth 1646 | if (leftEdgeOffset < viewportDimensions.left) { // left overflow 1647 | delta.left = viewportDimensions.left - leftEdgeOffset 1648 | } else if (rightEdgeOffset > viewportDimensions.right) { // right overflow 1649 | delta.left = viewportDimensions.left + viewportDimensions.width - rightEdgeOffset 1650 | } 1651 | } 1652 | 1653 | return delta 1654 | } 1655 | 1656 | Tooltip.prototype.getTitle = function () { 1657 | var title 1658 | var $e = this.$element 1659 | var o = this.options 1660 | 1661 | title = $e.attr('data-original-title') 1662 | || (typeof o.title == 'function' ? o.title.call($e[0]) : o.title) 1663 | 1664 | return title 1665 | } 1666 | 1667 | Tooltip.prototype.getUID = function (prefix) { 1668 | do prefix += ~~(Math.random() * 1000000) 1669 | while (document.getElementById(prefix)) 1670 | return prefix 1671 | } 1672 | 1673 | Tooltip.prototype.tip = function () { 1674 | if (!this.$tip) { 1675 | this.$tip = $(this.options.template) 1676 | if (this.$tip.length != 1) { 1677 | throw new Error(this.type + ' `template` option must consist of exactly 1 top-level element!') 1678 | } 1679 | } 1680 | return this.$tip 1681 | } 1682 | 1683 | Tooltip.prototype.arrow = function () { 1684 | return (this.$arrow = this.$arrow || this.tip().find('.tooltip-arrow')) 1685 | } 1686 | 1687 | Tooltip.prototype.enable = function () { 1688 | this.enabled = true 1689 | } 1690 | 1691 | Tooltip.prototype.disable = function () { 1692 | this.enabled = false 1693 | } 1694 | 1695 | Tooltip.prototype.toggleEnabled = function () { 1696 | this.enabled = !this.enabled 1697 | } 1698 | 1699 | Tooltip.prototype.toggle = function (e) { 1700 | var self = this 1701 | if (e) { 1702 | self = $(e.currentTarget).data('bs.' + this.type) 1703 | if (!self) { 1704 | self = new this.constructor(e.currentTarget, this.getDelegateOptions()) 1705 | $(e.currentTarget).data('bs.' + this.type, self) 1706 | } 1707 | } 1708 | 1709 | if (e) { 1710 | self.inState.click = !self.inState.click 1711 | if (self.isInStateTrue()) self.enter(self) 1712 | else self.leave(self) 1713 | } else { 1714 | self.tip().hasClass('in') ? self.leave(self) : self.enter(self) 1715 | } 1716 | } 1717 | 1718 | Tooltip.prototype.destroy = function () { 1719 | var that = this 1720 | clearTimeout(this.timeout) 1721 | this.hide(function () { 1722 | that.$element.off('.' + that.type).removeData('bs.' + that.type) 1723 | if (that.$tip) { 1724 | that.$tip.detach() 1725 | } 1726 | that.$tip = null 1727 | that.$arrow = null 1728 | that.$viewport = null 1729 | }) 1730 | } 1731 | 1732 | 1733 | // TOOLTIP PLUGIN DEFINITION 1734 | // ========================= 1735 | 1736 | function Plugin(option) { 1737 | return this.each(function () { 1738 | var $this = $(this) 1739 | var data = $this.data('bs.tooltip') 1740 | var options = typeof option == 'object' && option 1741 | 1742 | if (!data && /destroy|hide/.test(option)) return 1743 | if (!data) $this.data('bs.tooltip', (data = new Tooltip(this, options))) 1744 | if (typeof option == 'string') data[option]() 1745 | }) 1746 | } 1747 | 1748 | var old = $.fn.tooltip 1749 | 1750 | $.fn.tooltip = Plugin 1751 | $.fn.tooltip.Constructor = Tooltip 1752 | 1753 | 1754 | // TOOLTIP NO CONFLICT 1755 | // =================== 1756 | 1757 | $.fn.tooltip.noConflict = function () { 1758 | $.fn.tooltip = old 1759 | return this 1760 | } 1761 | 1762 | }(jQuery); 1763 | 1764 | /* ======================================================================== 1765 | * Bootstrap: popover.js v3.3.6 1766 | * http://getbootstrap.com/javascript/#popovers 1767 | * ======================================================================== 1768 | * Copyright 2011-2015 Twitter, Inc. 1769 | * Licensed under MIT (https://github.com/twbs/bootstrap/blob/master/LICENSE) 1770 | * ======================================================================== */ 1771 | 1772 | 1773 | +function ($) { 1774 | 'use strict'; 1775 | 1776 | // POPOVER PUBLIC CLASS DEFINITION 1777 | // =============================== 1778 | 1779 | var Popover = function (element, options) { 1780 | this.init('popover', element, options) 1781 | } 1782 | 1783 | if (!$.fn.tooltip) throw new Error('Popover requires tooltip.js') 1784 | 1785 | Popover.VERSION = '3.3.6' 1786 | 1787 | Popover.DEFAULTS = $.extend({}, $.fn.tooltip.Constructor.DEFAULTS, { 1788 | placement: 'right', 1789 | trigger: 'click', 1790 | content: '', 1791 | template: '' 1792 | }) 1793 | 1794 | 1795 | // NOTE: POPOVER EXTENDS tooltip.js 1796 | // ================================ 1797 | 1798 | Popover.prototype = $.extend({}, $.fn.tooltip.Constructor.prototype) 1799 | 1800 | Popover.prototype.constructor = Popover 1801 | 1802 | Popover.prototype.getDefaults = function () { 1803 | return Popover.DEFAULTS 1804 | } 1805 | 1806 | Popover.prototype.setContent = function () { 1807 | var $tip = this.tip() 1808 | var title = this.getTitle() 1809 | var content = this.getContent() 1810 | 1811 | $tip.find('.popover-title')[this.options.html ? 'html' : 'text'](title) 1812 | $tip.find('.popover-content').children().detach().end()[ // we use append for html objects to maintain js events 1813 | this.options.html ? (typeof content == 'string' ? 'html' : 'append') : 'text' 1814 | ](content) 1815 | 1816 | $tip.removeClass('fade top bottom left right in') 1817 | 1818 | // IE8 doesn't accept hiding via the `:empty` pseudo selector, we have to do 1819 | // this manually by checking the contents. 1820 | if (!$tip.find('.popover-title').html()) $tip.find('.popover-title').hide() 1821 | } 1822 | 1823 | Popover.prototype.hasContent = function () { 1824 | return this.getTitle() || this.getContent() 1825 | } 1826 | 1827 | Popover.prototype.getContent = function () { 1828 | var $e = this.$element 1829 | var o = this.options 1830 | 1831 | return $e.attr('data-content') 1832 | || (typeof o.content == 'function' ? 1833 | o.content.call($e[0]) : 1834 | o.content) 1835 | } 1836 | 1837 | Popover.prototype.arrow = function () { 1838 | return (this.$arrow = this.$arrow || this.tip().find('.arrow')) 1839 | } 1840 | 1841 | 1842 | // POPOVER PLUGIN DEFINITION 1843 | // ========================= 1844 | 1845 | function Plugin(option) { 1846 | return this.each(function () { 1847 | var $this = $(this) 1848 | var data = $this.data('bs.popover') 1849 | var options = typeof option == 'object' && option 1850 | 1851 | if (!data && /destroy|hide/.test(option)) return 1852 | if (!data) $this.data('bs.popover', (data = new Popover(this, options))) 1853 | if (typeof option == 'string') data[option]() 1854 | }) 1855 | } 1856 | 1857 | var old = $.fn.popover 1858 | 1859 | $.fn.popover = Plugin 1860 | $.fn.popover.Constructor = Popover 1861 | 1862 | 1863 | // POPOVER NO CONFLICT 1864 | // =================== 1865 | 1866 | $.fn.popover.noConflict = function () { 1867 | $.fn.popover = old 1868 | return this 1869 | } 1870 | 1871 | }(jQuery); 1872 | 1873 | /* ======================================================================== 1874 | * Bootstrap: scrollspy.js v3.3.6 1875 | * http://getbootstrap.com/javascript/#scrollspy 1876 | * ======================================================================== 1877 | * Copyright 2011-2015 Twitter, Inc. 1878 | * Licensed under MIT (https://github.com/twbs/bootstrap/blob/master/LICENSE) 1879 | * ======================================================================== */ 1880 | 1881 | 1882 | +function ($) { 1883 | 'use strict'; 1884 | 1885 | // SCROLLSPY CLASS DEFINITION 1886 | // ========================== 1887 | 1888 | function ScrollSpy(element, options) { 1889 | this.$body = $(document.body) 1890 | this.$scrollElement = $(element).is(document.body) ? $(window) : $(element) 1891 | this.options = $.extend({}, ScrollSpy.DEFAULTS, options) 1892 | this.selector = (this.options.target || '') + ' .nav li > a' 1893 | this.offsets = [] 1894 | this.targets = [] 1895 | this.activeTarget = null 1896 | this.scrollHeight = 0 1897 | 1898 | this.$scrollElement.on('scroll.bs.scrollspy', $.proxy(this.process, this)) 1899 | this.refresh() 1900 | this.process() 1901 | } 1902 | 1903 | ScrollSpy.VERSION = '3.3.6' 1904 | 1905 | ScrollSpy.DEFAULTS = { 1906 | offset: 10 1907 | } 1908 | 1909 | ScrollSpy.prototype.getScrollHeight = function () { 1910 | return this.$scrollElement[0].scrollHeight || Math.max(this.$body[0].scrollHeight, document.documentElement.scrollHeight) 1911 | } 1912 | 1913 | ScrollSpy.prototype.refresh = function () { 1914 | var that = this 1915 | var offsetMethod = 'offset' 1916 | var offsetBase = 0 1917 | 1918 | this.offsets = [] 1919 | this.targets = [] 1920 | this.scrollHeight = this.getScrollHeight() 1921 | 1922 | if (!$.isWindow(this.$scrollElement[0])) { 1923 | offsetMethod = 'position' 1924 | offsetBase = this.$scrollElement.scrollTop() 1925 | } 1926 | 1927 | this.$body 1928 | .find(this.selector) 1929 | .map(function () { 1930 | var $el = $(this) 1931 | var href = $el.data('target') || $el.attr('href') 1932 | var $href = /^#./.test(href) && $(href) 1933 | 1934 | return ($href 1935 | && $href.length 1936 | && $href.is(':visible') 1937 | && [[$href[offsetMethod]().top + offsetBase, href]]) || null 1938 | }) 1939 | .sort(function (a, b) { return a[0] - b[0] }) 1940 | .each(function () { 1941 | that.offsets.push(this[0]) 1942 | that.targets.push(this[1]) 1943 | }) 1944 | } 1945 | 1946 | ScrollSpy.prototype.process = function () { 1947 | var scrollTop = this.$scrollElement.scrollTop() + this.options.offset 1948 | var scrollHeight = this.getScrollHeight() 1949 | var maxScroll = this.options.offset + scrollHeight - this.$scrollElement.height() 1950 | var offsets = this.offsets 1951 | var targets = this.targets 1952 | var activeTarget = this.activeTarget 1953 | var i 1954 | 1955 | if (this.scrollHeight != scrollHeight) { 1956 | this.refresh() 1957 | } 1958 | 1959 | if (scrollTop >= maxScroll) { 1960 | return activeTarget != (i = targets[targets.length - 1]) && this.activate(i) 1961 | } 1962 | 1963 | if (activeTarget && scrollTop < offsets[0]) { 1964 | this.activeTarget = null 1965 | return this.clear() 1966 | } 1967 | 1968 | for (i = offsets.length; i--;) { 1969 | activeTarget != targets[i] 1970 | && scrollTop >= offsets[i] 1971 | && (offsets[i + 1] === undefined || scrollTop < offsets[i + 1]) 1972 | && this.activate(targets[i]) 1973 | } 1974 | } 1975 | 1976 | ScrollSpy.prototype.activate = function (target) { 1977 | this.activeTarget = target 1978 | 1979 | this.clear() 1980 | 1981 | var selector = this.selector + 1982 | '[data-target="' + target + '"],' + 1983 | this.selector + '[href="' + target + '"]' 1984 | 1985 | var active = $(selector) 1986 | .parents('li') 1987 | .addClass('active') 1988 | 1989 | if (active.parent('.dropdown-menu').length) { 1990 | active = active 1991 | .closest('li.dropdown') 1992 | .addClass('active') 1993 | } 1994 | 1995 | active.trigger('activate.bs.scrollspy') 1996 | } 1997 | 1998 | ScrollSpy.prototype.clear = function () { 1999 | $(this.selector) 2000 | .parentsUntil(this.options.target, '.active') 2001 | .removeClass('active') 2002 | } 2003 | 2004 | 2005 | // SCROLLSPY PLUGIN DEFINITION 2006 | // =========================== 2007 | 2008 | function Plugin(option) { 2009 | return this.each(function () { 2010 | var $this = $(this) 2011 | var data = $this.data('bs.scrollspy') 2012 | var options = typeof option == 'object' && option 2013 | 2014 | if (!data) $this.data('bs.scrollspy', (data = new ScrollSpy(this, options))) 2015 | if (typeof option == 'string') data[option]() 2016 | }) 2017 | } 2018 | 2019 | var old = $.fn.scrollspy 2020 | 2021 | $.fn.scrollspy = Plugin 2022 | $.fn.scrollspy.Constructor = ScrollSpy 2023 | 2024 | 2025 | // SCROLLSPY NO CONFLICT 2026 | // ===================== 2027 | 2028 | $.fn.scrollspy.noConflict = function () { 2029 | $.fn.scrollspy = old 2030 | return this 2031 | } 2032 | 2033 | 2034 | // SCROLLSPY DATA-API 2035 | // ================== 2036 | 2037 | $(window).on('load.bs.scrollspy.data-api', function () { 2038 | $('[data-spy="scroll"]').each(function () { 2039 | var $spy = $(this) 2040 | Plugin.call($spy, $spy.data()) 2041 | }) 2042 | }) 2043 | 2044 | }(jQuery); 2045 | 2046 | /* ======================================================================== 2047 | * Bootstrap: tab.js v3.3.6 2048 | * http://getbootstrap.com/javascript/#tabs 2049 | * ======================================================================== 2050 | * Copyright 2011-2015 Twitter, Inc. 2051 | * Licensed under MIT (https://github.com/twbs/bootstrap/blob/master/LICENSE) 2052 | * ======================================================================== */ 2053 | 2054 | 2055 | +function ($) { 2056 | 'use strict'; 2057 | 2058 | // TAB CLASS DEFINITION 2059 | // ==================== 2060 | 2061 | var Tab = function (element) { 2062 | // jscs:disable requireDollarBeforejQueryAssignment 2063 | this.element = $(element) 2064 | // jscs:enable requireDollarBeforejQueryAssignment 2065 | } 2066 | 2067 | Tab.VERSION = '3.3.6' 2068 | 2069 | Tab.TRANSITION_DURATION = 150 2070 | 2071 | Tab.prototype.show = function () { 2072 | var $this = this.element 2073 | var $ul = $this.closest('ul:not(.dropdown-menu)') 2074 | var selector = $this.data('target') 2075 | 2076 | if (!selector) { 2077 | selector = $this.attr('href') 2078 | selector = selector && selector.replace(/.*(?=#[^\s]*$)/, '') // strip for ie7 2079 | } 2080 | 2081 | if ($this.parent('li').hasClass('active')) return 2082 | 2083 | var $previous = $ul.find('.active:last a') 2084 | var hideEvent = $.Event('hide.bs.tab', { 2085 | relatedTarget: $this[0] 2086 | }) 2087 | var showEvent = $.Event('show.bs.tab', { 2088 | relatedTarget: $previous[0] 2089 | }) 2090 | 2091 | $previous.trigger(hideEvent) 2092 | $this.trigger(showEvent) 2093 | 2094 | if (showEvent.isDefaultPrevented() || hideEvent.isDefaultPrevented()) return 2095 | 2096 | var $target = $(selector) 2097 | 2098 | this.activate($this.closest('li'), $ul) 2099 | this.activate($target, $target.parent(), function () { 2100 | $previous.trigger({ 2101 | type: 'hidden.bs.tab', 2102 | relatedTarget: $this[0] 2103 | }) 2104 | $this.trigger({ 2105 | type: 'shown.bs.tab', 2106 | relatedTarget: $previous[0] 2107 | }) 2108 | }) 2109 | } 2110 | 2111 | Tab.prototype.activate = function (element, container, callback) { 2112 | var $active = container.find('> .active') 2113 | var transition = callback 2114 | && $.support.transition 2115 | && ($active.length && $active.hasClass('fade') || !!container.find('> .fade').length) 2116 | 2117 | function next() { 2118 | $active 2119 | .removeClass('active') 2120 | .find('> .dropdown-menu > .active') 2121 | .removeClass('active') 2122 | .end() 2123 | .find('[data-toggle="tab"]') 2124 | .attr('aria-expanded', false) 2125 | 2126 | element 2127 | .addClass('active') 2128 | .find('[data-toggle="tab"]') 2129 | .attr('aria-expanded', true) 2130 | 2131 | if (transition) { 2132 | element[0].offsetWidth // reflow for transition 2133 | element.addClass('in') 2134 | } else { 2135 | element.removeClass('fade') 2136 | } 2137 | 2138 | if (element.parent('.dropdown-menu').length) { 2139 | element 2140 | .closest('li.dropdown') 2141 | .addClass('active') 2142 | .end() 2143 | .find('[data-toggle="tab"]') 2144 | .attr('aria-expanded', true) 2145 | } 2146 | 2147 | callback && callback() 2148 | } 2149 | 2150 | $active.length && transition ? 2151 | $active 2152 | .one('bsTransitionEnd', next) 2153 | .emulateTransitionEnd(Tab.TRANSITION_DURATION) : 2154 | next() 2155 | 2156 | $active.removeClass('in') 2157 | } 2158 | 2159 | 2160 | // TAB PLUGIN DEFINITION 2161 | // ===================== 2162 | 2163 | function Plugin(option) { 2164 | return this.each(function () { 2165 | var $this = $(this) 2166 | var data = $this.data('bs.tab') 2167 | 2168 | if (!data) $this.data('bs.tab', (data = new Tab(this))) 2169 | if (typeof option == 'string') data[option]() 2170 | }) 2171 | } 2172 | 2173 | var old = $.fn.tab 2174 | 2175 | $.fn.tab = Plugin 2176 | $.fn.tab.Constructor = Tab 2177 | 2178 | 2179 | // TAB NO CONFLICT 2180 | // =============== 2181 | 2182 | $.fn.tab.noConflict = function () { 2183 | $.fn.tab = old 2184 | return this 2185 | } 2186 | 2187 | 2188 | // TAB DATA-API 2189 | // ============ 2190 | 2191 | var clickHandler = function (e) { 2192 | e.preventDefault() 2193 | Plugin.call($(this), 'show') 2194 | } 2195 | 2196 | $(document) 2197 | .on('click.bs.tab.data-api', '[data-toggle="tab"]', clickHandler) 2198 | .on('click.bs.tab.data-api', '[data-toggle="pill"]', clickHandler) 2199 | 2200 | }(jQuery); 2201 | 2202 | /* ======================================================================== 2203 | * Bootstrap: affix.js v3.3.6 2204 | * http://getbootstrap.com/javascript/#affix 2205 | * ======================================================================== 2206 | * Copyright 2011-2015 Twitter, Inc. 2207 | * Licensed under MIT (https://github.com/twbs/bootstrap/blob/master/LICENSE) 2208 | * ======================================================================== */ 2209 | 2210 | 2211 | +function ($) { 2212 | 'use strict'; 2213 | 2214 | // AFFIX CLASS DEFINITION 2215 | // ====================== 2216 | 2217 | var Affix = function (element, options) { 2218 | this.options = $.extend({}, Affix.DEFAULTS, options) 2219 | 2220 | this.$target = $(this.options.target) 2221 | .on('scroll.bs.affix.data-api', $.proxy(this.checkPosition, this)) 2222 | .on('click.bs.affix.data-api', $.proxy(this.checkPositionWithEventLoop, this)) 2223 | 2224 | this.$element = $(element) 2225 | this.affixed = null 2226 | this.unpin = null 2227 | this.pinnedOffset = null 2228 | 2229 | this.checkPosition() 2230 | } 2231 | 2232 | Affix.VERSION = '3.3.6' 2233 | 2234 | Affix.RESET = 'affix affix-top affix-bottom' 2235 | 2236 | Affix.DEFAULTS = { 2237 | offset: 0, 2238 | target: window 2239 | } 2240 | 2241 | Affix.prototype.getState = function (scrollHeight, height, offsetTop, offsetBottom) { 2242 | var scrollTop = this.$target.scrollTop() 2243 | var position = this.$element.offset() 2244 | var targetHeight = this.$target.height() 2245 | 2246 | if (offsetTop != null && this.affixed == 'top') return scrollTop < offsetTop ? 'top' : false 2247 | 2248 | if (this.affixed == 'bottom') { 2249 | if (offsetTop != null) return (scrollTop + this.unpin <= position.top) ? false : 'bottom' 2250 | return (scrollTop + targetHeight <= scrollHeight - offsetBottom) ? false : 'bottom' 2251 | } 2252 | 2253 | var initializing = this.affixed == null 2254 | var colliderTop = initializing ? scrollTop : position.top 2255 | var colliderHeight = initializing ? targetHeight : height 2256 | 2257 | if (offsetTop != null && scrollTop <= offsetTop) return 'top' 2258 | if (offsetBottom != null && (colliderTop + colliderHeight >= scrollHeight - offsetBottom)) return 'bottom' 2259 | 2260 | return false 2261 | } 2262 | 2263 | Affix.prototype.getPinnedOffset = function () { 2264 | if (this.pinnedOffset) return this.pinnedOffset 2265 | this.$element.removeClass(Affix.RESET).addClass('affix') 2266 | var scrollTop = this.$target.scrollTop() 2267 | var position = this.$element.offset() 2268 | return (this.pinnedOffset = position.top - scrollTop) 2269 | } 2270 | 2271 | Affix.prototype.checkPositionWithEventLoop = function () { 2272 | setTimeout($.proxy(this.checkPosition, this), 1) 2273 | } 2274 | 2275 | Affix.prototype.checkPosition = function () { 2276 | if (!this.$element.is(':visible')) return 2277 | 2278 | var height = this.$element.height() 2279 | var offset = this.options.offset 2280 | var offsetTop = offset.top 2281 | var offsetBottom = offset.bottom 2282 | var scrollHeight = Math.max($(document).height(), $(document.body).height()) 2283 | 2284 | if (typeof offset != 'object') offsetBottom = offsetTop = offset 2285 | if (typeof offsetTop == 'function') offsetTop = offset.top(this.$element) 2286 | if (typeof offsetBottom == 'function') offsetBottom = offset.bottom(this.$element) 2287 | 2288 | var affix = this.getState(scrollHeight, height, offsetTop, offsetBottom) 2289 | 2290 | if (this.affixed != affix) { 2291 | if (this.unpin != null) this.$element.css('top', '') 2292 | 2293 | var affixType = 'affix' + (affix ? '-' + affix : '') 2294 | var e = $.Event(affixType + '.bs.affix') 2295 | 2296 | this.$element.trigger(e) 2297 | 2298 | if (e.isDefaultPrevented()) return 2299 | 2300 | this.affixed = affix 2301 | this.unpin = affix == 'bottom' ? this.getPinnedOffset() : null 2302 | 2303 | this.$element 2304 | .removeClass(Affix.RESET) 2305 | .addClass(affixType) 2306 | .trigger(affixType.replace('affix', 'affixed') + '.bs.affix') 2307 | } 2308 | 2309 | if (affix == 'bottom') { 2310 | this.$element.offset({ 2311 | top: scrollHeight - height - offsetBottom 2312 | }) 2313 | } 2314 | } 2315 | 2316 | 2317 | // AFFIX PLUGIN DEFINITION 2318 | // ======================= 2319 | 2320 | function Plugin(option) { 2321 | return this.each(function () { 2322 | var $this = $(this) 2323 | var data = $this.data('bs.affix') 2324 | var options = typeof option == 'object' && option 2325 | 2326 | if (!data) $this.data('bs.affix', (data = new Affix(this, options))) 2327 | if (typeof option == 'string') data[option]() 2328 | }) 2329 | } 2330 | 2331 | var old = $.fn.affix 2332 | 2333 | $.fn.affix = Plugin 2334 | $.fn.affix.Constructor = Affix 2335 | 2336 | 2337 | // AFFIX NO CONFLICT 2338 | // ================= 2339 | 2340 | $.fn.affix.noConflict = function () { 2341 | $.fn.affix = old 2342 | return this 2343 | } 2344 | 2345 | 2346 | // AFFIX DATA-API 2347 | // ============== 2348 | 2349 | $(window).on('load', function () { 2350 | $('[data-spy="affix"]').each(function () { 2351 | var $spy = $(this) 2352 | var data = $spy.data() 2353 | 2354 | data.offset = data.offset || {} 2355 | 2356 | if (data.offsetBottom != null) data.offset.bottom = data.offsetBottom 2357 | if (data.offsetTop != null) data.offset.top = data.offsetTop 2358 | 2359 | Plugin.call($spy, data) 2360 | }) 2361 | }) 2362 | 2363 | }(jQuery); 2364 | --------------------------------------------------------------------------------