The iPortal application is used to impersonate different users within Qlik Sense.
55 | It is primarily used to test the results of security rule changes on a population of users
56 | that belong to various security groups.
57 |
Contributors
58 |
59 |
Eric Bracke
60 |
Brad Peterman
61 |
Jeffrey Goldberg
62 |
Tom McAlees
63 |
64 |
65 |
68 |
69 |
70 |
71 |
72 |
73 |
Qlik Sense Impersonation Portal (iPortal). Refer to about page for more details.
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 | 
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 | 
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 | 
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 | 
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 | 
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 | 
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 | 
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 | 
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: '