From 4ca5d9b04bea4a4403abd4ade25b37825e1bc417 Mon Sep 17 00:00:00 2001 From: Joseph Schorr Date: Tue, 3 Mar 2015 19:58:42 -0500 Subject: [PATCH 01/96] Add support for filtering github login by org --- endpoints/callbacks.py | 26 ++++++++++++++++--- static/css/core-ui.css | 9 +++++++ .../directives/config/config-setup-tool.html | 22 ++++++++++++++++ static/js/controllers/setup.js | 2 +- static/js/services/key-service.js | 2 +- util/config/validator.py | 8 ++++++ util/oauth.py | 22 ++++++++++++++++ 7 files changed, 86 insertions(+), 5 deletions(-) diff --git a/endpoints/callbacks.py b/endpoints/callbacks.py index a8bb05dbe..f381074ad 100644 --- a/endpoints/callbacks.py +++ b/endpoints/callbacks.py @@ -157,7 +157,10 @@ def github_oauth_callback(): if error: return render_ologin_error('GitHub', error) + # Exchange the OAuth code. token = exchange_code_for_token(request.args.get('code'), github_login) + + # Retrieve the user's information. user_data = get_user(github_login, token) if not user_data or not 'login' in user_data: return render_ologin_error('GitHub') @@ -172,16 +175,33 @@ def github_oauth_callback(): token_param = { 'access_token': token, } + + # Retrieve the user's orgnizations (if organization filtering is turned on) + if github_login.allowed_organizations() is not None: + get_orgs = client.get(github_login.orgs_endpoint(), params=token_param, + headers={'Accept': 'application/vnd.github.moondragon+json'}) + + organizations = set([org.get('login') for org in get_orgs.json()]) + if not (organizations & set(github_login.allowed_organizations())): + err = """You are not a member of an allowed GitHub organization. + Please contact your system administrator if you believe this is in error.""" + return render_ologin_error('GitHub', err) + + # Find the e-mail address for the user: we will accept any email, but we prefer the primary get_email = client.get(github_login.email_endpoint(), params=token_param, headers=v3_media_type) - # We will accept any email, but we prefer the primary found_email = None for user_email in get_email.json(): - found_email = user_email['email'] - if user_email['primary']: + if not user_email['primary'] or not user_email['verified']: break + found_email = user_email['email'] + + if found_email is None: + err = 'There is no verified e-mail address attached to the GitHub account.' + return render_ologin_error('GitHub', err) + metadata = { 'service_username': username } diff --git a/static/css/core-ui.css b/static/css/core-ui.css index 2012129c1..218b3b72d 100644 --- a/static/css/core-ui.css +++ b/static/css/core-ui.css @@ -262,6 +262,15 @@ display: block; } +.config-list-field-element input { + vertical-align: middle; +} + +.config-list-field-element .item-delete { + display: inline-block; + margin-left: 20px; +} + .config-list-field-element input { width: 350px; } diff --git a/static/directives/config/config-setup-tool.html b/static/directives/config/config-setup-tool.html index 2ed51dd5c..b769797fa 100644 --- a/static/directives/config/config-setup-tool.html +++ b/static/directives/config/config-setup-tool.html @@ -400,6 +400,28 @@ + + Organization Filtering: + +
+ + +
+ +
+ If enabled, only members of specified GitHub + Enterprise organizations will be allowed to login via GitHub + Enterprise. +
+ + + + + diff --git a/static/js/controllers/setup.js b/static/js/controllers/setup.js index 9dc76a17f..8bebad19f 100644 --- a/static/js/controllers/setup.js +++ b/static/js/controllers/setup.js @@ -124,7 +124,7 @@ function SetupCtrl($scope, $timeout, ApiService, Features, UserService, Containe $scope.showSuperuserPanel = function() { $('#setupModal').modal('hide'); var prefix = $scope.hasSSL ? 'https' : 'http'; - var hostname = $scope.hostname; + var hostname = $scope.hostname || document.location.hostname; window.location = prefix + '://' + hostname + '/superuser'; }; diff --git a/static/js/services/key-service.js b/static/js/services/key-service.js index 1ab153635..1c419b25e 100644 --- a/static/js/services/key-service.js +++ b/static/js/services/key-service.js @@ -23,7 +23,7 @@ angular.module('quay').factory('KeyService', ['$location', 'Config', function($l keyService['githubTriggerEndpoint'] = oauth['GITHUB_TRIGGER_CONFIG']['GITHUB_ENDPOINT']; keyService['githubTriggerAuthorizeUrl'] = oauth['GITHUB_TRIGGER_CONFIG']['AUTHORIZE_ENDPOINT']; - keyService['githubLoginScope'] = 'user:email'; + keyService['githubLoginScope'] = 'user:email,read:org'; keyService['googleLoginScope'] = 'openid email'; keyService.isEnterprise = function(service) { diff --git a/util/config/validator.py b/util/config/validator.py index 271ce678e..d27bf2106 100644 --- a/util/config/validator.py +++ b/util/config/validator.py @@ -122,12 +122,20 @@ def _validate_github_with_key(config_key, config): if not github_config.get('CLIENT_SECRET'): raise Exception('Missing Client Secret') + if github_config.get('ORG_RESTRICT') and not github_config.get('ALLOWED_ORGANIZATIONS'): + raise Exception('Organization restriction must have at least one allowed organization') + client = app.config['HTTPCLIENT'] oauth = GithubOAuthConfig(config, config_key) result = oauth.validate_client_id_and_secret(client) if not result: raise Exception('Invalid client id or client secret') + if github_config.get('ALLOWED_ORGANIZATIONS'): + for org_id in github_config.get('ALLOWED_ORGANIZATIONS'): + if not oauth.validate_organization(org_id, client): + raise Exception('Invalid organization: %s' % org_id) + def _validate_google_login(config): """ Validates the Google Login client ID and secret. """ diff --git a/util/oauth.py b/util/oauth.py index ede8823aa..731cec81a 100644 --- a/util/oauth.py +++ b/util/oauth.py @@ -1,4 +1,5 @@ import urlparse +import github class OAuthConfig(object): def __init__(self, config, key_name): @@ -40,6 +41,12 @@ class GithubOAuthConfig(OAuthConfig): def service_name(self): return 'GitHub' + def allowed_organizations(self): + if not self.config.get('ORG_RESTRICT', False): + return None + + return self.config.get('ALLOWED_ORGANIZATIONS', None) + def _endpoint(self): endpoint = self.config.get('GITHUB_ENDPOINT', 'https://github.com') if not endpoint.endswith('/'): @@ -66,6 +73,10 @@ class GithubOAuthConfig(OAuthConfig): api_endpoint = self._api_endpoint() return self._get_url(api_endpoint, 'user/emails') + def orgs_endpoint(self): + api_endpoint = self._api_endpoint() + return self._get_url(api_endpoint, 'user/orgs') + def validate_client_id_and_secret(self, http_client): # First: Verify that the github endpoint is actually Github by checking for the # X-GitHub-Request-Id here. @@ -91,6 +102,17 @@ class GithubOAuthConfig(OAuthConfig): timeout=5) return result.status_code == 404 + def validate_organization(self, organization_id, http_client): + api_endpoint = self._api_endpoint() + org_endpoint = self._get_url(api_endpoint, 'orgs/%s' % organization_id) + + result = http_client.get(org_endpoint, + headers={'Accept': 'application/vnd.github.moondragon+json'}, + timeout=5) + + return result.status_code == 200 + + def get_public_config(self): return { 'CLIENT_ID': self.client_id(), From 2e840654d3d696dc331668e2c5c35c1fdfcb3459 Mon Sep 17 00:00:00 2001 From: Joseph Schorr Date: Thu, 5 Mar 2015 12:07:39 -0500 Subject: [PATCH 02/96] PR changes --- endpoints/callbacks.py | 6 ++++-- static/js/services/key-service.js | 6 +++++- util/oauth.py | 3 ++- 3 files changed, 11 insertions(+), 4 deletions(-) diff --git a/endpoints/callbacks.py b/endpoints/callbacks.py index f381074ad..aa5ee307c 100644 --- a/endpoints/callbacks.py +++ b/endpoints/callbacks.py @@ -193,10 +193,12 @@ def github_oauth_callback(): found_email = None for user_email in get_email.json(): - if not user_email['primary'] or not user_email['verified']: - break + if not user_email['verified']: + continue found_email = user_email['email'] + if user_email['primary']: + break if found_email is None: err = 'There is no verified e-mail address attached to the GitHub account.' diff --git a/static/js/services/key-service.js b/static/js/services/key-service.js index 1c419b25e..deaab705b 100644 --- a/static/js/services/key-service.js +++ b/static/js/services/key-service.js @@ -23,7 +23,11 @@ angular.module('quay').factory('KeyService', ['$location', 'Config', function($l keyService['githubTriggerEndpoint'] = oauth['GITHUB_TRIGGER_CONFIG']['GITHUB_ENDPOINT']; keyService['githubTriggerAuthorizeUrl'] = oauth['GITHUB_TRIGGER_CONFIG']['AUTHORIZE_ENDPOINT']; - keyService['githubLoginScope'] = 'user:email,read:org'; + keyService['githubLoginScope'] = 'user:email'; + if (oauth['GITHUB_LOGIN_CONFIG']['ORG_RESTRICT']) { + keyService['githubLoginScope'] += ',read:org'; + } + keyService['googleLoginScope'] = 'openid email'; keyService.isEnterprise = function(service) { diff --git a/util/oauth.py b/util/oauth.py index 731cec81a..466db4d98 100644 --- a/util/oauth.py +++ b/util/oauth.py @@ -117,7 +117,8 @@ class GithubOAuthConfig(OAuthConfig): return { 'CLIENT_ID': self.client_id(), 'AUTHORIZE_ENDPOINT': self.authorize_endpoint(), - 'GITHUB_ENDPOINT': self._endpoint() + 'GITHUB_ENDPOINT': self._endpoint(), + 'ORG_RESTRICT': self.config.get('ORG_RESTRICT', False) } From 99c56b7f98e6d94326314c13522ee6e067c6ec17 Mon Sep 17 00:00:00 2001 From: Joseph Schorr Date: Wed, 18 Mar 2015 15:43:53 -0400 Subject: [PATCH 03/96] Delink the build-mini-status if the user is not the repo admin --- static/directives/build-mini-status.html | 18 +++++++++++++++--- .../directives/repo-view/repo-panel-info.html | 3 ++- static/js/directives/ui/build-mini-status.js | 3 ++- 3 files changed, 19 insertions(+), 5 deletions(-) diff --git a/static/directives/build-mini-status.html b/static/directives/build-mini-status.html index e16086195..ec743ce94 100644 --- a/static/directives/build-mini-status.html +++ b/static/directives/build-mini-status.html @@ -1,5 +1,17 @@ - -
+ + +
+ + + + + +
+
+
+ +
@@ -7,4 +19,4 @@
- \ No newline at end of file + \ No newline at end of file diff --git a/static/directives/repo-view/repo-panel-info.html b/static/directives/repo-view/repo-panel-info.html index 79ee118cb..24f779460 100644 --- a/static/directives/repo-view/repo-panel-info.html +++ b/static/directives/repo-view/repo-panel-info.html @@ -48,7 +48,8 @@
-
+
diff --git a/static/js/directives/ui/build-mini-status.js b/static/js/directives/ui/build-mini-status.js index 70c026e8b..a6698bd74 100644 --- a/static/js/directives/ui/build-mini-status.js +++ b/static/js/directives/ui/build-mini-status.js @@ -9,7 +9,8 @@ angular.module('quay').directive('buildMiniStatus', function () { transclude: false, restrict: 'C', scope: { - 'build': '=build' + 'build': '=build', + 'isAdmin': '=isAdmin' }, controller: function($scope, $element) { $scope.isBuilding = function(build) { From c1d58bdd6c510e5b727f30ff856b0c059f9a9d09 Mon Sep 17 00:00:00 2001 From: Joseph Schorr Date: Wed, 18 Mar 2015 16:26:27 -0400 Subject: [PATCH 04/96] Make messaging around the connected user invoking builds better --- static/directives/repo-view/repo-panel-builds.html | 3 ++- static/js/directives/repo-view/repo-panel-builds.js | 10 +++++++++- 2 files changed, 11 insertions(+), 2 deletions(-) diff --git a/static/directives/repo-view/repo-panel-builds.html b/static/directives/repo-view/repo-panel-builds.html index c9e2b65a8..31c0c10e7 100644 --- a/static/directives/repo-view/repo-panel-builds.html +++ b/static/directives/repo-view/repo-panel-builds.html @@ -137,7 +137,8 @@ - + Run Trigger Now diff --git a/static/js/directives/repo-view/repo-panel-builds.js b/static/js/directives/repo-view/repo-panel-builds.js index 46ad68a13..bdb53a8e7 100644 --- a/static/js/directives/repo-view/repo-panel-builds.js +++ b/static/js/directives/repo-view/repo-panel-builds.js @@ -12,11 +12,13 @@ angular.module('quay').directive('repoPanelBuilds', function () { 'repository': '=repository', 'builds': '=builds' }, - controller: function($scope, $element, $filter, $routeParams, ApiService, TriggerService) { + controller: function($scope, $element, $filter, $routeParams, ApiService, TriggerService, UserService) { var orderBy = $filter('orderBy'); $scope.TriggerService = TriggerService; + UserService.updateUserIn($scope); + $scope.options = { 'filter': 'recent', 'reverse': false, @@ -165,6 +167,12 @@ angular.module('quay').directive('repoPanelBuilds', function () { }; $scope.askRunTrigger = function(trigger) { + if ($scope.user.username != trigger.connected_user) { + bootbox.alert('For security reasons, only user "' + trigger.connected_user + + '" can manually invoke this trigger'); + return; + } + $scope.currentStartTrigger = trigger; $scope.showTriggerStartDialogCounter++; }; From 70aec00914521791d01b6ba840db51c19fb46f1d Mon Sep 17 00:00:00 2001 From: Joseph Schorr Date: Thu, 19 Mar 2015 15:08:18 -0400 Subject: [PATCH 05/96] Add a fetch tag dialog --- .../directives/repo-view/repo-panel-info.css | 11 ++ .../directives/repo-view/repo-panel-tags.css | 5 + static/css/directives/ui/fetch-tag-dialog.css | 19 ++++ static/css/quay.css | 14 +++ static/directives/fetch-tag-dialog.html | 70 ++++++++++++ .../directives/repo-view/repo-panel-info.html | 7 ++ .../directives/repo-view/repo-panel-tags.html | 11 +- static/img/docker.png | Bin 0 -> 1475 bytes static/img/rocket.png | Bin 0 -> 618 bytes .../directives/repo-view/repo-panel-info.js | 11 +- static/js/directives/ui/fetch-tag-dialog.js | 104 ++++++++++++++++++ static/js/services/features-config.js | 4 + static/js/services/user-service.js | 4 + 13 files changed, 258 insertions(+), 2 deletions(-) create mode 100644 static/css/directives/ui/fetch-tag-dialog.css create mode 100644 static/directives/fetch-tag-dialog.html create mode 100644 static/img/docker.png create mode 100644 static/img/rocket.png create mode 100644 static/js/directives/ui/fetch-tag-dialog.js diff --git a/static/css/directives/repo-view/repo-panel-info.css b/static/css/directives/repo-view/repo-panel-info.css index 2291fd852..a0d35b312 100644 --- a/static/css/directives/repo-view/repo-panel-info.css +++ b/static/css/directives/repo-view/repo-panel-info.css @@ -1,3 +1,14 @@ +.repo-panel-info-element .right-controls { + margin-bottom: 20px; + float: right; +} + +.repo-panel-info-element .right-controls .copy-box { + width: 400px; + display: inline-block; + margin-left: 10px; +} + .repo-panel-info-element .stat-col { border-right: 2px solid #eee; } diff --git a/static/css/directives/repo-view/repo-panel-tags.css b/static/css/directives/repo-view/repo-panel-tags.css index aebf689f8..9df9c2679 100644 --- a/static/css/directives/repo-view/repo-panel-tags.css +++ b/static/css/directives/repo-view/repo-panel-tags.css @@ -63,4 +63,9 @@ .repo-panel-tags-element .options-col { padding-left: 20px; +} + +.repo-panel-tags-element .options-col .fa-download { + color: #999; + cursor: pointer; } \ No newline at end of file diff --git a/static/css/directives/ui/fetch-tag-dialog.css b/static/css/directives/ui/fetch-tag-dialog.css new file mode 100644 index 000000000..8155650a5 --- /dev/null +++ b/static/css/directives/ui/fetch-tag-dialog.css @@ -0,0 +1,19 @@ +.fetch-tag-dialog .modal-table { + width: 100%; +} + +.fetch-tag-dialog .modal-table .first-col { + width: 140px; +} + +.fetch-tag-dialog .co-dialog .modal-body { + padding: 20px; +} + +.fetch-tag-dialog .entity-search { + margin: 10px; +} + +.fetch-tag-dialog pre.command { + margin-top: 10px; +} \ No newline at end of file diff --git a/static/css/quay.css b/static/css/quay.css index bcc0f6584..333fbd411 100644 --- a/static/css/quay.css +++ b/static/css/quay.css @@ -4787,6 +4787,20 @@ i.slack-icon { height: 16px; } +i.docker-icon { + background-image: url(/static/img/docker.png); + background-size: 16px; + width: 16px; + height: 16px; +} + +i.rocket-icon { + background-image: url(/static/img/rocket.png); + background-size: 16px; + width: 16px; + height: 16px; +} + .external-notification-view-element { margin: 10px; padding: 6px; diff --git a/static/directives/fetch-tag-dialog.html b/static/directives/fetch-tag-dialog.html new file mode 100644 index 000000000..befe6cedc --- /dev/null +++ b/static/directives/fetch-tag-dialog.html @@ -0,0 +1,70 @@ +
+ + +
\ No newline at end of file diff --git a/static/directives/repo-view/repo-panel-info.html b/static/directives/repo-view/repo-panel-info.html index 24f779460..245e3ba22 100644 --- a/static/directives/repo-view/repo-panel-info.html +++ b/static/directives/repo-view/repo-panel-info.html @@ -62,7 +62,14 @@
+ + + +

Description

+
Image + + + + + @@ -98,4 +104,7 @@
\ No newline at end of file + action-handler="tagActionHandler">
+ +
+
\ No newline at end of file diff --git a/static/img/docker.png b/static/img/docker.png new file mode 100644 index 0000000000000000000000000000000000000000..ee01a5ee8a9ea542f17123ed9c587510a2e26aef GIT binary patch literal 1475 zcmV;!1w8tRP)+(fofHYbct z7*K_dWFwmr35jN}Fd7pRjiT8b<4YG~_HK!0x)@EIj7*}LWu##SsR%e!jR>VYCg?1bo3s&Wq=Fp7Z;D&i6dO=Q)oO5$YSi5ceL*k{DJLkk$3kJZT{rY7j31U11z*grKSiAPVO#SF|u>ah6@nZo* znTD^7uUP$^M8L03ldH=i=`1QNGc`F*ICULuG|ln+1|;`C`=jEjri~lctbC@g|HP$_ zj?Y;I)}+EjX{>WtZLr$lY=YAo=LcY8M8J6|H&>1gO-+qaN7s(2ZQoio)wT6!yOi?h zbzuGGXF9dmywX;@Tzk7Xhtmde4kdzk0V=X!6P%wyDTRqD3pQ@qvJ!l(qkOP;-{8L! z78bPFTyt}r&JlKP+I;58zV$y;>Muo*pNLd$0VuXDeyP~;>21zh(&duM8y}ZSV{A;R zR3xz$6PC$pL#8f=PGYL9_ia75JvK>4YqgJ?qm56&%Auo7T6%8qx!s#)Ac%PXjIw{&`Lv)Au$G*L?lWfnJflDdp=P^ zlS)z`5_l{pFGM=jERQC%@F06h26GQ8Fd~&kzz?Z$S)Z}g7 zzTM*#7IPmD3)rAQ?a!SxZEhE85-A2|4fgT1|fRF6Hk^V`?%|LiwY>8$w3 z!YGri6TnL%w4XwZhl(Q-XK}_uJBtTMkU}|!3o9gk3I+JpQm93Z2%-tn4YF`)<3w90 zm%}Q{_P=@}J302IUoHKkb@*i8?~e`)19)PBO&3>gTDqX6Ku&vzOKRJJ0&YAj)UGXx z7-ro0PaGgoI)&C9|B6mWRYodhnlB8{e)j0@zPI0c>((kza>Rs{o}^kve1GQoQL_}) zZ8oJ)z+@D1rCLTQ#lpeA9r*fzKkc9Gb}uz~^+@LYX~ruNrF6hY+@p9X_BfGyOVrKWNI5X3W-lNpG6vQJRZh7& zu7q}WZ&WQRQH)fpU>z8r=FzhhPQUYFUvKZxxeU{9oH#Moa&_o8>9B+g6C4_+J)F`= z9MkMV9vT^-T_*?%rEch2qm+WgBGYB0TmdKG1vE}wrt{Rnm;btN&rjzP2T&rymUZje zFD-lQkk|34&IG~p42`~~BV*{Nke6%{6Vn7+OcqvUKp! zkH5R;weQbm{Y?M>PdwHA_~)M9`et{}=2p)bQmGWp`38PBJjILs!zis#2v}DWwzjrf zsJ$zUArns1cH!?Mi!UDE{pWpeyf&ZkEdT(^*KB-rL(dmpdFu1sj}{geYI`J(R|dxT z<$Gf@Gt^AipcEQMU8P9V=(+Kh(KEkpzxeJ;e>n8+h1(Io1)!E+WjB6t=L-)$vg)fH zot+OaDir*DLzcHcDDkV)S1E=uei9Oti)6~zuho~Xp3F~N-nVFEaPQv3htJ=x@SFfQ zfcTw{tX|%}Wa-1Xy6mRFsCC82R;Qz6zi*@Cb=A^PS dzXxy+;Gc5Vg6*aX&Q$;a002ovPDHLkV1knE)4Biv literal 0 HcmV?d00001 diff --git a/static/img/rocket.png b/static/img/rocket.png new file mode 100644 index 0000000000000000000000000000000000000000..e42c081418cc1d45930d25de275a50f19a506a6b GIT binary patch literal 618 zcmV-w0+s!VP)mOi3!cJ{msL>r!L)5^cCI|%OtE7cWplXofuoChTJ_JHnN&#vPzwjnX5M*~-2Rer6~cHtY?%QmrSoHn6h-g3KtQzB zVCHP00zh5N6rXfTRkahbe6upV0568yvVA%)=RSn&{bJ!_Mt&45n z@H1Cs1`!xOVl8B~jE>Yl{d{I!)71861AqWs?xApQ|1W!33BJ5C271KTBUL z* Date: Thu, 19 Mar 2015 15:10:16 -0400 Subject: [PATCH 06/96] Clarify pull text --- static/directives/repo-view/repo-panel-info.html | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/static/directives/repo-view/repo-panel-info.html b/static/directives/repo-view/repo-panel-info.html index 245e3ba22..3cef46919 100644 --- a/static/directives/repo-view/repo-panel-info.html +++ b/static/directives/repo-view/repo-panel-info.html @@ -65,7 +65,7 @@

Description

From 3959ea2ff9761936b9a0155bfc881a0ee8e7fa99 Mon Sep 17 00:00:00 2001 From: Joseph Schorr Date: Thu, 19 Mar 2015 15:23:34 -0400 Subject: [PATCH 07/96] Add ability to add/move a tag in the tag view --- static/directives/repo-view/repo-panel-tags.html | 3 +++ static/js/directives/repo-view/repo-panel-tags.js | 4 ++++ 2 files changed, 7 insertions(+) diff --git a/static/directives/repo-view/repo-panel-tags.html b/static/directives/repo-view/repo-panel-tags.html index 253ec1583..867009d4a 100644 --- a/static/directives/repo-view/repo-panel-tags.html +++ b/static/directives/repo-view/repo-panel-tags.html @@ -86,6 +86,9 @@ Delete Tag + + Add New Tag +
diff --git a/static/js/directives/repo-view/repo-panel-tags.js b/static/js/directives/repo-view/repo-panel-tags.js index c96098b8d..c84cf017b 100644 --- a/static/js/directives/repo-view/repo-panel-tags.js +++ b/static/js/directives/repo-view/repo-panel-tags.js @@ -166,6 +166,10 @@ angular.module('quay').directive('repoPanelTags', function () { $scope.tagActionHandler.askDeleteMultipleTags(tags); }; + $scope.askAddTag = function(tag) { + $scope.tagActionHandler.askAddTag(tag.image_id); + }; + $scope.orderBy = function(predicate) { if (predicate == $scope.options.predicate) { $scope.options.reverse = !$scope.options.reverse; From 049148cb87074ddf0a291810160483075ac9b2d8 Mon Sep 17 00:00:00 2001 From: Joseph Schorr Date: Fri, 20 Mar 2015 17:46:02 -0400 Subject: [PATCH 08/96] Work in progress: new image view --- endpoints/api/image.py | 37 ++++--- endpoints/api/tag.py | 2 +- static/css/directives/ui/image-view-layer.css | 76 ++++++++++++++ static/css/pages/image-view.css | 16 +++ static/directives/image-view-layer.html | 15 +++ static/js/directives/ui/image-view-layer.js | 48 +++++++++ static/js/pages/image-view.js | 42 +++++++- static/partials/image-view.html | 99 +++++-------------- static/partials/old-image-view.html | 82 +++++++++++++++ 9 files changed, 325 insertions(+), 92 deletions(-) create mode 100644 static/css/directives/ui/image-view-layer.css create mode 100644 static/css/pages/image-view.css create mode 100644 static/directives/image-view-layer.html create mode 100644 static/js/directives/ui/image-view-layer.js create mode 100644 static/partials/old-image-view.html diff --git a/endpoints/api/image.py b/endpoints/api/image.py index 71171d572..939a87d98 100644 --- a/endpoints/api/image.py +++ b/endpoints/api/image.py @@ -9,7 +9,7 @@ from data import model from util.cache import cache_control_flask_restful -def image_view(image, image_map): +def image_view(image, image_map, include_locations=True, include_ancestors=True): extended_props = image if image.storage and image.storage.id: extended_props = image.storage @@ -20,24 +20,35 @@ def image_view(image, image_map): if not aid or not aid in image_map: return '' - return image_map[aid] + return image_map[aid].docker_image_id - # Calculate the ancestors string, with the DBID's replaced with the docker IDs. - ancestors = [docker_id(a) for a in image.ancestors.split('/')] - ancestors_string = '/'.join(ancestors) - - return { + image_data = { 'id': image.docker_image_id, 'created': format_date(extended_props.created), 'comment': extended_props.comment, 'command': json.loads(command) if command else None, 'size': extended_props.image_size, - 'locations': list(image.storage.locations), 'uploading': image.storage.uploading, - 'ancestors': ancestors_string, - 'sort_index': len(image.ancestors) + 'sort_index': len(image.ancestors), } + if include_locations: + image_data['locations'] = list(image.storage.locations) + + if include_ancestors: + # Calculate the ancestors string, with the DBID's replaced with the docker IDs. + ancestors = [docker_id(a) for a in image.ancestors.split('/')] + image_data['ancestors'] = '/'.join(ancestors) + + return image_data + + +def historical_image_view(image, image_map): + ancestors = [image_map[a] for a in image.ancestors.split('/')[1:-1]] + normal_view = image_view(image, image_map) + normal_view['history'] = [image_view(parent, image_map, False, False) for parent in ancestors] + return normal_view + @resource('/v1/repository//image/') @path_param('repository', 'The full path of the repository. e.g. namespace/name') @@ -62,7 +73,7 @@ class RepositoryImageList(RepositoryParamResource): filtered_images = [] for image in all_images: if str(image.id) in found_image_ids: - image_map[str(image.id)] = image.docker_image_id + image_map[str(image.id)] = image filtered_images.append(image) def add_tags(image_json): @@ -90,9 +101,9 @@ class RepositoryImage(RepositoryParamResource): # Lookup all the ancestor images for the image. image_map = {} for current_image in model.get_parent_images(namespace, repository, image): - image_map[str(current_image.id)] = image.docker_image_id + image_map[str(current_image.id)] = current_image - return image_view(image, image_map) + return historical_image_view(image, image_map) @resource('/v1/repository//image//changes') diff --git a/endpoints/api/tag.py b/endpoints/api/tag.py index 21972fc19..d5d7c94df 100644 --- a/endpoints/api/tag.py +++ b/endpoints/api/tag.py @@ -92,7 +92,7 @@ class RepositoryTagImages(RepositoryParamResource): parent_images = model.get_parent_images(namespace, repository, tag_image) image_map = {} for image in parent_images: - image_map[str(image.id)] = image.docker_image_id + image_map[str(image.id)] = image parents = list(parent_images) parents.reverse() diff --git a/static/css/directives/ui/image-view-layer.css b/static/css/directives/ui/image-view-layer.css new file mode 100644 index 000000000..f0fcd0c82 --- /dev/null +++ b/static/css/directives/ui/image-view-layer.css @@ -0,0 +1,76 @@ +.image-view-layer-element { + position: relative; + padding: 10px; + padding-left: 170px; +} + +.image-view-layer-element .image-id { + font-family: monospace; + position: absolute; + top: 10px; + left: 10px; + width: 110px; + text-align: right; +} + +.image-view-layer-element .image-id a { + color: #aaa; +} + +.image-view-layer-element.first .image-id { + font-weight: bold; + font-size: 110%; +} + +.image-view-layer-element.first .image-id a { + color: black; +} + +.image-view-layer-element .nondocker-command { + font-family: monospace; + padding: 2px; +} + +.image-view-layer-element .nondocker-command:before { + content: "\f120"; + font-family: "FontAwesome"; + font-size: 16px; + margin-right: 6px; + color: #999; +} + +.image-view-layer-element .image-layer-line { + position: absolute; + top: 0px; + bottom: 0px; + left: 140px; + + border-left: 2px solid #428bca; + width: 0px; +} + +.image-view-layer-element.first .image-layer-line { + top: 20px; +} + +.image-view-layer-element.last .image-layer-line { + bottom: 20px; +} + +.image-view-layer-element .image-layer-dot { + position: absolute; + top: 14px; + left: 135px; + border: 2px solid #428bca; + border-radius: 50%; + width: 12px; + height: 12px; + background: white; + + z-index: 2; +} + + +.image-view-layer-element.first .image-layer-dot { + background: #428bca; +} diff --git a/static/css/pages/image-view.css b/static/css/pages/image-view.css new file mode 100644 index 000000000..de8d4f07b --- /dev/null +++ b/static/css/pages/image-view.css @@ -0,0 +1,16 @@ +.image-view .image-view-header { + padding: 10px; + background: #e8f1f6; + margin: -10px; + margin-bottom: 20px; +} + +.image-view .image-view-header .section-icon { + margin-right: 6px; +} + +.image-view .image-view-header .section { + padding: 4px; + display: inline-block; + margin-right: 20px; +} \ No newline at end of file diff --git a/static/directives/image-view-layer.html b/static/directives/image-view-layer.html new file mode 100644 index 000000000..289a2acd6 --- /dev/null +++ b/static/directives/image-view-layer.html @@ -0,0 +1,15 @@ +
+ +
{{ image.comment }}
+
+
{{ image.command.join(' ') }}
+
+
+
+
+
\ No newline at end of file diff --git a/static/js/directives/ui/image-view-layer.js b/static/js/directives/ui/image-view-layer.js new file mode 100644 index 000000000..fa0c346cb --- /dev/null +++ b/static/js/directives/ui/image-view-layer.js @@ -0,0 +1,48 @@ +/** + * An element which displays a single layer representing an image in the image view. + */ +angular.module('quay').directive('imageViewLayer', function () { + var directiveDefinitionObject = { + priority: 0, + templateUrl: '/static/directives/image-view-layer.html', + replace: false, + transclude: true, + restrict: 'C', + scope: { + 'repository': '=repository', + 'image': '=image', + 'images': '=images' + }, + controller: function($scope, $element) { + $scope.getDockerfileCommand = function(command) { + if (!command) { return ''; } + + // ["/bin/sh", "-c", "#(nop) RUN foo"] + var commandPrefix = '#(nop)' + + if (command.length != 3) { return ''; } + if (command[0] != '/bin/sh' || command[1] != '-c') { return ''; } + + var cmd = command[2]; + + if (cmd.substring(0, commandPrefix.length) != commandPrefix) { return ''; } + + return command[2].substr(commandPrefix.length + 1); + }; + + $scope.getClass = function() { + var index = $.inArray($scope.image, $scope.images); + if (index < 0) { + return 'first'; + } + + if (index == $scope.images.length - 1) { + return 'last'; + } + + return ''; + }; + } + }; + return directiveDefinitionObject; +}); \ No newline at end of file diff --git a/static/js/pages/image-view.js b/static/js/pages/image-view.js index a8f04deda..757c030cc 100644 --- a/static/js/pages/image-view.js +++ b/static/js/pages/image-view.js @@ -3,10 +3,48 @@ * Page to view the details of a single image. */ angular.module('quayPages').config(['pages', function(pages) { - pages.create('image-view', 'image-view.html', ImageViewCtrl); + pages.create('image-view', 'image-view.html', ImageViewCtrl, { + 'newLayout': true, + 'title': '{{ image.id }}', + 'description': 'Image {{ image.id }}' + }, ['layout']) + + pages.create('image-view', 'old-image-view.html', OldImageViewCtrl, { + }, ['old-layout']); }]); - function ImageViewCtrl($scope, $routeParams, $rootScope, $timeout, ApiService, ImageMetadataService) { + function ImageViewCtrl($scope, $routeParams, $rootScope, ApiService, ImageMetadataService) { + var namespace = $routeParams.namespace; + var name = $routeParams.name; + var imageid = $routeParams.image; + + var loadImage = function() { + var params = { + 'repository': namespace + '/' + name, + 'image_id': imageid + }; + + $scope.imageResource = ApiService.getImageAsResource(params).get(function(image) { + $scope.image = image; + $scope.reversedHistory = image.history.reverse(); + }); + }; + + var loadRepository = function() { + var params = { + 'repository': namespace + '/' + name + }; + + $scope.repositoryResource = ApiService.getRepoAsResource(params).get(function(repo) { + $scope.repository = repo; + }); + }; + + loadImage(); + loadRepository(); + } + + function OldImageViewCtrl($scope, $routeParams, $rootScope, $timeout, ApiService, ImageMetadataService) { var namespace = $routeParams.namespace; var name = $routeParams.name; var imageid = $routeParams.image; diff --git a/static/partials/image-view.html b/static/partials/image-view.html index a4b8eaabb..4de310026 100644 --- a/static/partials/image-view.html +++ b/static/partials/image-view.html @@ -1,82 +1,29 @@ -
-
-
- -

- - -

+
+
+
+ + + + {{ repository.namespace }}/{{ repository.name }} + + + + + {{ image.id.substr(0, 12) }} +
- -
- -
- - -
-
Full Image ID
-
-
-
-
Created
-
-
Compressed Image Size
-
{{ image.value.size | bytes }} -
- -
Command
-
-
{{ getFormattedCommand(image.value) }}
-
-
- - -
- File Changes: -
-
- -
- - -
- -
-
-
-
- Showing {{(combinedChanges | filter:search | limitTo:50).length}} of {{(combinedChanges | filter:search).length}} results -
-
- -
-
-
-
-
- No matching changes -
-
- - - - {{folder}}/{{getFilename(change.file)}} - -
-
-
+
+
+
{{ image.id }}
+
{{ image.created | amDateFormat:'dddd, MMMM Do YYYY, h:mm:ss a' }}
- -
-
-
+
+
-
+
\ No newline at end of file diff --git a/static/partials/old-image-view.html b/static/partials/old-image-view.html new file mode 100644 index 000000000..a4b8eaabb --- /dev/null +++ b/static/partials/old-image-view.html @@ -0,0 +1,82 @@ +
+
+
+ +

+ + +

+
+ + +
+ +
+ + +
+
Full Image ID
+
+
+
+
Created
+
+
Compressed Image Size
+
{{ image.value.size | bytes }} +
+ +
Command
+
+
{{ getFormattedCommand(image.value) }}
+
+
+ + +
+ File Changes: +
+
+ +
+ + +
+ +
+
+
+
+ Showing {{(combinedChanges | filter:search | limitTo:50).length}} of {{(combinedChanges | filter:search).length}} results +
+
+ +
+
+
+
+
+ No matching changes +
+
+ + + + {{folder}}/{{getFilename(change.file)}} + +
+
+
+
+ + +
+
+
+
+
+
From 8042f1a9857b8dc39fed673342e543c3c0eed4a5 Mon Sep 17 00:00:00 2001 From: Joseph Schorr Date: Mon, 23 Mar 2015 15:22:22 -0400 Subject: [PATCH 09/96] - Finished new image view - Add the CoreOS icon font --- external_libraries.py | 5 +++ static/css/directives/ui/image-view-layer.css | 7 +++- static/css/pages/image-view.css | 11 ++++- static/css/quay.css | 1 + static/js/directives/ui/image-view-layer.js | 5 ++- static/js/pages/image-view.js | 40 ++++++++++++++++++- static/partials/image-view.html | 39 +++++++++++++----- 7 files changed, 93 insertions(+), 15 deletions(-) diff --git a/external_libraries.py b/external_libraries.py index 3ab6bfd4a..113295353 100644 --- a/external_libraries.py +++ b/external_libraries.py @@ -21,12 +21,17 @@ EXTERNAL_CSS = [ 'netdna.bootstrapcdn.com/font-awesome/4.2.0/css/font-awesome.css', 'netdna.bootstrapcdn.com/bootstrap/3.3.2/css/bootstrap.min.css', 'fonts.googleapis.com/css?family=Source+Sans+Pro:400,700', + 'cdn.core-os.net/icons/core-icons.css' ] EXTERNAL_FONTS = [ 'netdna.bootstrapcdn.com/font-awesome/4.2.0/fonts/fontawesome-webfont.woff?v=4.2.0', 'netdna.bootstrapcdn.com/font-awesome/4.2.0/fonts/fontawesome-webfont.ttf?v=4.2.0', 'netdna.bootstrapcdn.com/font-awesome/4.2.0/fonts/fontawesome-webfont.svg?v=4.2.0', + + 'cdn.core-os.net/icons/core-icons.woff', + 'cdn.core-os.net/icons/core-icons.ttf', + 'cdn.core-os.net/icons/core-icons.svg', ] diff --git a/static/css/directives/ui/image-view-layer.css b/static/css/directives/ui/image-view-layer.css index f0fcd0c82..85fc70d99 100644 --- a/static/css/directives/ui/image-view-layer.css +++ b/static/css/directives/ui/image-view-layer.css @@ -26,6 +26,10 @@ color: black; } +.image-view-layer-element .image-comment { + margin-bottom: 10px; +} + .image-view-layer-element .nondocker-command { font-family: monospace; padding: 2px; @@ -54,7 +58,7 @@ } .image-view-layer-element.last .image-layer-line { - bottom: 20px; + height: 16px; } .image-view-layer-element .image-layer-dot { @@ -70,7 +74,6 @@ z-index: 2; } - .image-view-layer-element.first .image-layer-dot { background: #428bca; } diff --git a/static/css/pages/image-view.css b/static/css/pages/image-view.css index de8d4f07b..f91ceacc9 100644 --- a/static/css/pages/image-view.css +++ b/static/css/pages/image-view.css @@ -13,4 +13,13 @@ padding: 4px; display: inline-block; margin-right: 20px; -} \ No newline at end of file +} + +.image-view .co-tab-content { + padding: 20px; + padding-top: 10px; +} + +.image-view .co-tab-content h3 { + margin-bottom: 20px; +} diff --git a/static/css/quay.css b/static/css/quay.css index 333fbd411..9d10c888d 100644 --- a/static/css/quay.css +++ b/static/css/quay.css @@ -277,6 +277,7 @@ nav.navbar-default .navbar-nav>li>a.active { .dockerfile-command .command-title { font-family: Consolas, "Lucida Console", Monaco, monospace; padding-left: 90px; + display: inline-block; } .dockerfile-command .label { diff --git a/static/js/directives/ui/image-view-layer.js b/static/js/directives/ui/image-view-layer.js index fa0c346cb..e5a781172 100644 --- a/static/js/directives/ui/image-view-layer.js +++ b/static/js/directives/ui/image-view-layer.js @@ -24,8 +24,9 @@ angular.module('quay').directive('imageViewLayer', function () { if (command[0] != '/bin/sh' || command[1] != '-c') { return ''; } var cmd = command[2]; - - if (cmd.substring(0, commandPrefix.length) != commandPrefix) { return ''; } + if (cmd.substring(0, commandPrefix.length) != commandPrefix) { + return 'RUN ' + cmd; + } return command[2].substr(commandPrefix.length + 1); }; diff --git a/static/js/pages/image-view.js b/static/js/pages/image-view.js index 757c030cc..945fdc66e 100644 --- a/static/js/pages/image-view.js +++ b/static/js/pages/image-view.js @@ -13,7 +13,7 @@ }, ['old-layout']); }]); - function ImageViewCtrl($scope, $routeParams, $rootScope, ApiService, ImageMetadataService) { + function ImageViewCtrl($scope, $routeParams, $rootScope, $timeout, ApiService, ImageMetadataService) { var namespace = $routeParams.namespace; var name = $routeParams.name; var imageid = $routeParams.image; @@ -42,6 +42,44 @@ loadImage(); loadRepository(); + + $scope.downloadChanges = function() { + if ($scope.changesResource) { return; } + + var params = { + 'repository': namespace + '/' + name, + 'image_id': imageid + }; + + $scope.changesResource = ApiService.getImageChangesAsResource(params).get(function(changes) { + var combinedChanges = []; + var addCombinedChanges = function(c, kind) { + for (var i = 0; i < c.length; ++i) { + combinedChanges.push({ + 'kind': kind, + 'file': c[i] + }); + } + }; + + addCombinedChanges(changes.added, 'added'); + addCombinedChanges(changes.removed, 'removed'); + addCombinedChanges(changes.changed, 'changed'); + + $scope.combinedChanges = combinedChanges; + $scope.imageChanges = changes; + $scope.initializeTree(); + }); + }; + + $scope.initializeTree = function() { + if ($scope.tree) { return; } + + $scope.tree = new ImageFileChangeTree($scope.image, $scope.combinedChanges); + $timeout(function() { + $scope.tree.draw('changes-tree-container'); + }, 100); + }; } function OldImageViewCtrl($scope, $routeParams, $rootScope, $timeout, ApiService, ImageMetadataService) { diff --git a/static/partials/image-view.html b/static/partials/image-view.html index 4de310026..421c685dd 100644 --- a/static/partials/image-view.html +++ b/static/partials/image-view.html @@ -10,20 +10,41 @@ - + {{ image.id.substr(0, 12) }}
-
-
-
{{ image.id }}
-
{{ image.created | amDateFormat:'dddd, MMMM Do YYYY, h:mm:ss a' }}
-
-
-
+
+
+ + + + + + +
+ +
+ +
+

Image Layers

+
+
+
+ + +
+
+

Image File Changes

+
+
+
+
\ No newline at end of file From 3f4801ea7e9b4e5e8d4062dc439e95b9c38a72ad Mon Sep 17 00:00:00 2001 From: Joseph Schorr Date: Mon, 23 Mar 2015 15:23:34 -0400 Subject: [PATCH 10/96] Switch the squashed image icon --- static/js/directives/ui/fetch-tag-dialog.js | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/static/js/directives/ui/fetch-tag-dialog.js b/static/js/directives/ui/fetch-tag-dialog.js index 820339e6c..163418cc9 100644 --- a/static/js/directives/ui/fetch-tag-dialog.js +++ b/static/js/directives/ui/fetch-tag-dialog.js @@ -21,7 +21,7 @@ angular.module('quay').directive('fetchTagDialog', function () { $scope.formats = [ { 'title': 'Squashed Docker Image', - 'icon': 'fa-file-archive-o', + 'icon': 'ci-squashed', 'command': 'curl -L -f {http}://{pull_user}:{pull_password}@{hostname}/c1/squash/{namespace}/{name}/{tag} | docker load', 'require_creds': true }, From 1ed814a4694b59068c8fa8136e9b8b0110694cf1 Mon Sep 17 00:00:00 2001 From: Joseph Schorr Date: Mon, 23 Mar 2015 15:44:17 -0400 Subject: [PATCH 11/96] Better filtering of repo builds --- static/js/directives/repo-view/repo-panel-builds.js | 6 +++++- 1 file changed, 5 insertions(+), 1 deletion(-) diff --git a/static/js/directives/repo-view/repo-panel-builds.js b/static/js/directives/repo-view/repo-panel-builds.js index bdb53a8e7..74361e7f9 100644 --- a/static/js/directives/repo-view/repo-panel-builds.js +++ b/static/js/directives/repo-view/repo-panel-builds.js @@ -65,18 +65,22 @@ angular.module('quay').directive('repoPanelBuilds', function () { if ($scope.buildsResource && filter == $scope.currentFilter) { return; } var since = null; + var limit = 10; if ($scope.options.filter == '48hour') { since = Math.floor(moment().subtract(2, 'days').valueOf() / 1000); + limit = 100; } else if ($scope.options.filter == '30day') { since = Math.floor(moment().subtract(30, 'days').valueOf() / 1000); + limit = 100; } else { since = null; + limit = 10; } var params = { 'repository': $scope.repository.namespace + '/' + $scope.repository.name, - 'limit': 100, + 'limit': limit, 'since': since }; From d9c3c6689a0bf57d29bd98349af87738dffde6a5 Mon Sep 17 00:00:00 2001 From: Joseph Schorr Date: Mon, 23 Mar 2015 16:10:33 -0400 Subject: [PATCH 12/96] - Upgrade angular-strap to fix an NPE - Start the download of the rest of the repo information after the repo itself loads --- static/directives/cor-tab.html | 16 +++++++++------- static/js/pages/repo-view.js | 21 ++++++++++++++------- static/lib/angular-strap.min.js | 8 ++++---- static/lib/angular-strap.tpl.min.js | 5 ++--- 4 files changed, 29 insertions(+), 21 deletions(-) diff --git a/static/directives/cor-tab.html b/static/directives/cor-tab.html index f22d3bdac..61c8b327f 100644 --- a/static/directives/cor-tab.html +++ b/static/directives/cor-tab.html @@ -1,11 +1,13 @@
  • - - + ng-click="tabInit()"> + + +
  • \ No newline at end of file diff --git a/static/js/pages/repo-view.js b/static/js/pages/repo-view.js index 81fc620f2..707784fe6 100644 --- a/static/js/pages/repo-view.js +++ b/static/js/pages/repo-view.js @@ -13,7 +13,7 @@ }, ['old-layout']); }]); - function RepoViewCtrl($scope, $routeParams, $location, ApiService, UserService, AngularPollChannel) { + function RepoViewCtrl($scope, $routeParams, $location, $timeout, ApiService, UserService, AngularPollChannel) { $scope.namespace = $routeParams.namespace; $scope.name = $routeParams.name; @@ -64,11 +64,19 @@ $scope.repositoryResource = ApiService.getRepoAsResource(params).get(function(repo) { $scope.viewScope.repository = repo; - $scope.setTags($routeParams.tag); - // Track builds. - buildPollChannel = AngularPollChannel.create($scope, loadRepositoryBuilds, 5000 /* 5s */); - buildPollChannel.start(); + // Load the remainder of the data async, so we don't block the initial view from + // showing. + $timeout(function() { + $scope.setTags($routeParams.tag); + + // Load the images. + loadImages(); + + // Track builds. + buildPollChannel = AngularPollChannel.create($scope, loadRepositoryBuilds, 5000 /* 5s */); + buildPollChannel.start(); + }, 10); }); }; @@ -98,9 +106,8 @@ }, errorHandler); }; - // Load the repository and images. + // Load the repository. loadRepository(); - loadImages(); $scope.setTags = function(tagNames) { if (!tagNames) { diff --git a/static/lib/angular-strap.min.js b/static/lib/angular-strap.min.js index c358b9e1c..950a81b4e 100644 --- a/static/lib/angular-strap.min.js +++ b/static/lib/angular-strap.min.js @@ -1,10 +1,10 @@ /** * angular-strap - * @version v2.0.0-rc.4 - 2014-03-07 + * @version v2.2.1 - 2015-03-10 * @link http://mgcrea.github.io/angular-strap * @author Olivier Louvignes (olivier@mg-crea.com) * @license MIT License, http://www.opensource.org/licenses/MIT */ -!function(a,b){"use strict";angular.module("mgcrea.ngStrap",["mgcrea.ngStrap.modal","mgcrea.ngStrap.aside","mgcrea.ngStrap.alert","mgcrea.ngStrap.button","mgcrea.ngStrap.select","mgcrea.ngStrap.datepicker","mgcrea.ngStrap.timepicker","mgcrea.ngStrap.navbar","mgcrea.ngStrap.tooltip","mgcrea.ngStrap.popover","mgcrea.ngStrap.dropdown","mgcrea.ngStrap.typeahead","mgcrea.ngStrap.scrollspy","mgcrea.ngStrap.affix","mgcrea.ngStrap.tab"]),angular.module("mgcrea.ngStrap.affix",["mgcrea.ngStrap.helpers.dimensions"]).provider("$affix",function(){var a=this.defaults={offsetTop:"auto"};this.$get=["$window","dimensions",function(b,c){function d(d,f){function g(a,b,c){var d=h(),e=i();return t>=d?"top":null!==a&&d+a<=b.top?"middle":null!==u&&b.top+c+n>=e-u?"bottom":"middle"}function h(){return l[0]===b?b.pageYOffset:l[0]===b}function i(){return l[0]===b?b.document.body.scrollHeight:l[0].scrollHeight}var j={},k=angular.extend({},a,f),l=k.target,m="affix affix-top affix-bottom",n=0,o=0,p=null,q=null,r=d.parent();if(k.offsetParent)if(k.offsetParent.match(/^\d+$/))for(var s=0;s<1*k.offsetParent-1;s++)r=r.parent();else r=angular.element(k.offsetParent);var t=0;k.offsetTop&&("auto"===k.offsetTop&&(k.offsetTop="+0"),k.offsetTop.match(/^[-+]\d+$/)?(n-=1*k.offsetTop,t=k.offsetParent?c.offset(r[0]).top+1*k.offsetTop:c.offset(d[0]).top-c.css(d[0],"marginTop",!0)+1*k.offsetTop):t=1*k.offsetTop);var u=0;return k.offsetBottom&&(u=k.offsetParent&&k.offsetBottom.match(/^[-+]\d+$/)?i()-(c.offset(r[0]).top+c.height(r[0]))+1*k.offsetBottom+1:1*k.offsetBottom),j.init=function(){o=c.offset(d[0]).top+n,l.on("scroll",this.checkPosition),l.on("click",this.checkPositionWithEventLoop),this.checkPosition(),this.checkPositionWithEventLoop()},j.destroy=function(){l.off("scroll",this.checkPosition),l.off("click",this.checkPositionWithEventLoop)},j.checkPositionWithEventLoop=function(){setTimeout(this.checkPosition,1)},j.checkPosition=function(){var a=h(),b=c.offset(d[0]),f=c.height(d[0]),i=g(q,b,f);p!==i&&(p=i,d.removeClass(m).addClass("affix"+("middle"!==i?"-"+i:"")),"top"===i?(q=null,d.css("position",k.offsetParent?"":"relative"),d.css("top","")):"bottom"===i?(q=k.offsetUnpin?-(1*k.offsetUnpin):b.top-a,d.css("position",k.offsetParent?"":"relative"),d.css("top",k.offsetParent?"":e[0].offsetHeight-u-f-o+"px")):(q=null,d.css("position","fixed"),d.css("top",n+"px")))},j.init(),j}var e=angular.element(b.document.body);return d}]}).directive("bsAffix",["$affix","$window",function(a,b){return{restrict:"EAC",require:"^?bsAffixTarget",link:function(c,d,e,f){var g={scope:c,offsetTop:"auto",target:f?f.$element:angular.element(b)};angular.forEach(["offsetTop","offsetBottom","offsetParent","offsetUnpin"],function(a){angular.isDefined(e[a])&&(g[a]=e[a])});var h=a(d,g);c.$on("$destroy",function(){g=null,h=null})}}}]).directive("bsAffixTarget",function(){return{controller:["$element",function(a){this.$element=a}]}}),angular.module("mgcrea.ngStrap.alert",[]).provider("$alert",function(){var a=this.defaults={animation:"am-fade",prefixClass:"alert",placement:null,template:"alert/alert.tpl.html",container:!1,element:null,backdrop:!1,keyboard:!0,show:!0,duration:!1,type:!1};this.$get=["$modal","$timeout",function(b,c){function d(d){var e={},f=angular.extend({},a,d);e=b(f),f.type&&(e.$scope.type=f.type);var g=e.show;return f.duration&&(e.show=function(){g(),c(function(){e.hide()},1e3*f.duration)}),e}return d}]}).directive("bsAlert",["$window","$location","$sce","$alert",function(a,b,c,d){a.requestAnimationFrame||a.setTimeout;return{restrict:"EAC",scope:!0,link:function(a,b,e){var f={scope:a,element:b,show:!1};angular.forEach(["template","placement","keyboard","html","container","animation","duration"],function(a){angular.isDefined(e[a])&&(f[a]=e[a])}),angular.forEach(["title","content","type"],function(b){e[b]&&e.$observe(b,function(d){a[b]=c.trustAsHtml(d)})}),e.bsAlert&&a.$watch(e.bsAlert,function(b){angular.isObject(b)?angular.extend(a,b):a.content=b},!0);var g=d(f);b.on(e.trigger||"click",g.toggle),a.$on("$destroy",function(){g.destroy(),f=null,g=null})}}}]),angular.module("mgcrea.ngStrap.aside",["mgcrea.ngStrap.modal"]).provider("$aside",function(){var a=this.defaults={animation:"am-fade-and-slide-right",prefixClass:"aside",placement:"right",template:"aside/aside.tpl.html",contentTemplate:!1,container:!1,element:null,backdrop:!0,keyboard:!0,html:!1,show:!0};this.$get=["$modal",function(b){function c(c){var d={},e=angular.extend({},a,c);return d=b(e)}return c}]}).directive("bsAside",["$window","$location","$sce","$aside",function(a,b,c,d){a.requestAnimationFrame||a.setTimeout;return{restrict:"EAC",scope:!0,link:function(a,b,e){var f={scope:a,element:b,show:!1};angular.forEach(["template","contentTemplate","placement","backdrop","keyboard","html","container","animation"],function(a){angular.isDefined(e[a])&&(f[a]=e[a])}),angular.forEach(["title","content"],function(b){e[b]&&e.$observe(b,function(d){a[b]=c.trustAsHtml(d)})}),e.bsAside&&a.$watch(e.bsAside,function(b){angular.isObject(b)?angular.extend(a,b):a.content=b},!0);var g=d(f);b.on(e.trigger||"click",g.toggle),a.$on("$destroy",function(){g.destroy(),f=null,g=null})}}}]),angular.module("mgcrea.ngStrap.button",["ngAnimate"]).provider("$button",function(){var a=this.defaults={activeClass:"active",toggleEvent:"click"};this.$get=function(){return{defaults:a}}}).directive("bsCheckboxGroup",function(){return{restrict:"A",require:"ngModel",compile:function(a,b){a.attr("data-toggle","buttons"),a.removeAttr("ng-model");var c=a[0].querySelectorAll('input[type="checkbox"]');angular.forEach(c,function(a){var c=angular.element(a);c.attr("bs-checkbox",""),c.attr("ng-model",b.ngModel+"."+c.attr("value"))})}}}).directive("bsCheckbox",["$button","$$animateReflow",function(a,b){var c=a.defaults,d=/^(true|false|\d+)$/;return{restrict:"A",require:"ngModel",link:function(a,e,f,g){var h=c,i="INPUT"===e[0].nodeName,j=i?e.parent():e,k=angular.isDefined(f.trueValue)?f.trueValue:!0;d.test(f.trueValue)&&(k=a.$eval(f.trueValue));var l=angular.isDefined(f.falseValue)?f.falseValue:!1;d.test(f.falseValue)&&(l=a.$eval(f.falseValue));var m="boolean"!=typeof k||"boolean"!=typeof l;m&&(g.$parsers.push(function(a){return a?k:l}),a.$watch(f.ngModel,function(){g.$render()})),g.$render=function(){var a=angular.equals(g.$modelValue,k);b(function(){i&&(e[0].checked=a),j.toggleClass(h.activeClass,a)})},e.bind(h.toggleEvent,function(){a.$apply(function(){i||g.$setViewValue(!j.hasClass("active")),m||g.$render()})})}}}]).directive("bsRadioGroup",function(){return{restrict:"A",require:"ngModel",compile:function(a,b){a.attr("data-toggle","buttons"),a.removeAttr("ng-model");var c=a[0].querySelectorAll('input[type="radio"]');angular.forEach(c,function(a){angular.element(a).attr("bs-radio",""),angular.element(a).attr("ng-model",b.ngModel)})}}}).directive("bsRadio",["$button","$$animateReflow",function(a,b){var c=a.defaults,d=/^(true|false|\d+)$/;return{restrict:"A",require:"ngModel",link:function(a,e,f,g){var h=c,i="INPUT"===e[0].nodeName,j=i?e.parent():e,k=d.test(f.value)?a.$eval(f.value):f.value;g.$render=function(){var a=angular.equals(g.$modelValue,k);b(function(){i&&(e[0].checked=a),j.toggleClass(h.activeClass,a)})},e.bind(h.toggleEvent,function(){a.$apply(function(){g.$setViewValue(k),g.$render()})})}}}]),angular.module("mgcrea.ngStrap.datepicker",["mgcrea.ngStrap.helpers.dateParser","mgcrea.ngStrap.tooltip"]).provider("$datepicker",function(){var a=this.defaults={animation:"am-fade",prefixClass:"datepicker",placement:"bottom-left",template:"datepicker/datepicker.tpl.html",trigger:"focus",container:!1,keyboard:!0,html:!1,delay:0,useNative:!1,dateType:"date",dateFormat:"shortDate",strictFormat:!1,autoclose:!1,minDate:-1/0,maxDate:+1/0,startView:0,minView:0,startWeek:0};this.$get=["$window","$document","$rootScope","$sce","$locale","dateFilter","datepickerViews","$tooltip",function(b,c,d,e,f,g,h,i){function j(b,c,d){function e(a){a.selected=g.$isSelected(a.date)}function f(){b[0].focus()}var g=i(b,angular.extend({},a,d)),j=d.scope,m=g.$options,n=g.$scope;m.startView&&(m.startView-=m.minView);var o=h(g);g.$views=o.views;var p=o.viewDate;n.$mode=m.startView;var q=g.$views[n.$mode];n.$select=function(a){g.select(a)},n.$selectPane=function(a){g.$selectPane(a)},n.$toggleMode=function(){g.setMode((n.$mode+1)%g.$views.length)},g.update=function(a){angular.isDate(a)&&!isNaN(a.getTime())&&(g.$date=a,q.update.call(q,a)),g.$build(!0)},g.select=function(a,b){angular.isDate(c.$dateValue)||(c.$dateValue=new Date(a)),c.$dateValue.setFullYear(a.getFullYear(),a.getMonth(),a.getDate()),!n.$mode||b?(c.$setViewValue(c.$dateValue),c.$render(),m.autoclose&&!b&&g.hide(!0)):(angular.extend(p,{year:a.getFullYear(),month:a.getMonth(),date:a.getDate()}),g.setMode(n.$mode-1),g.$build())},g.setMode=function(a){n.$mode=a,q=g.$views[n.$mode],g.$build()},g.$build=function(a){a===!0&&q.built||(a!==!1||q.built)&&q.build.call(q)},g.$updateSelected=function(){for(var a=0,b=n.rows.length;b>a;a++)angular.forEach(n.rows[a],e)},g.$isSelected=function(a){return q.isSelected(a)},g.$selectPane=function(a){var b=q.steps,c=new Date(Date.UTC(p.year+(b.year||0)*a,p.month+(b.month||0)*a,p.date+(b.day||0)*a));angular.extend(p,{year:c.getUTCFullYear(),month:c.getUTCMonth(),date:c.getUTCDate()}),g.$build()},g.$onMouseDown=function(a){if(a.preventDefault(),a.stopPropagation(),k){var b=angular.element(a.target);"button"!==b[0].nodeName.toLowerCase()&&(b=b.parent()),b.triggerHandler("click")}},g.$onKeyDown=function(a){if(/(38|37|39|40|13)/.test(a.keyCode)&&!a.shiftKey&&!a.altKey){if(a.preventDefault(),a.stopPropagation(),13===a.keyCode)return n.$mode?n.$apply(function(){g.setMode(n.$mode-1)}):g.hide(!0);q.onKeyDown(a),j.$digest()}};var r=g.init;g.init=function(){return l&&m.useNative?(b.prop("type","date"),void b.css("-webkit-appearance","textfield")):(k&&(b.prop("type","text"),b.attr("readonly","true"),b.on("click",f)),void r())};var s=g.destroy;g.destroy=function(){l&&m.useNative&&b.off("click",f),s()};var t=g.show;g.show=function(){t(),setTimeout(function(){g.$element.on(k?"touchstart":"mousedown",g.$onMouseDown),m.keyboard&&b.on("keydown",g.$onKeyDown)})};var u=g.hide;return g.hide=function(a){g.$element.off(k?"touchstart":"mousedown",g.$onMouseDown),m.keyboard&&b.off("keydown",g.$onKeyDown),u(a)},g}var k=(angular.element(b.document.body),"createTouch"in b.document),l=/(ip(a|o)d|iphone|android)/gi.test(b.navigator.userAgent);return a.lang||(a.lang=f.id),j.defaults=a,j}]}).directive("bsDatepicker",["$window","$parse","$q","$locale","dateFilter","$datepicker","$dateParser","$timeout",function(a,b,c,d,e,f,g){{var h=(f.defaults,/(ip(a|o)d|iphone|android)/gi.test(a.navigator.userAgent));a.requestAnimationFrame||a.setTimeout}return{restrict:"EAC",require:"ngModel",link:function(a,b,c,d){var i={scope:a,controller:d};angular.forEach(["placement","container","delay","trigger","keyboard","html","animation","template","autoclose","dateType","dateFormat","strictFormat","startWeek","useNative","lang","startView","minView"],function(a){angular.isDefined(c[a])&&(i[a]=c[a])}),h&&i.useNative&&(i.dateFormat="yyyy-MM-dd");var j=f(b,d,i);i=j.$options,angular.forEach(["minDate","maxDate"],function(a){angular.isDefined(c[a])&&c.$observe(a,function(b){if("today"===b){var c=new Date;j.$options[a]=+new Date(c.getFullYear(),c.getMonth(),c.getDate()+("maxDate"===a?1:0),0,0,0,"minDate"===a?0:-1)}else j.$options[a]=angular.isString(b)&&b.match(/^".+"$/)?+new Date(b.substr(1,b.length-2)):+new Date(b);!isNaN(j.$options[a])&&j.$build(!1)})}),a.$watch(c.ngModel,function(){j.update(d.$dateValue)},!0);var k=g({format:i.dateFormat,lang:i.lang,strict:i.strictFormat});d.$parsers.unshift(function(a){if(!a)return void d.$setValidity("date",!0);var b=k.parse(a,d.$dateValue);if(!b||isNaN(b.getTime()))d.$setValidity("date",!1);else{var c=b.getTime()>=i.minDate&&b.getTime()<=i.maxDate;d.$setValidity("date",c),c&&(d.$dateValue=b)}return"string"===i.dateType?e(a,i.dateFormat):"number"===i.dateType?d.$dateValue.getTime():"iso"===i.dateType?d.$dateValue.toISOString():new Date(d.$dateValue)}),d.$formatters.push(function(a){if(!angular.isUndefined(a)&&null!==a){var b=angular.isDate(a)?a:new Date(a);return d.$dateValue=b,d.$dateValue}}),d.$render=function(){b.val(!d.$dateValue||isNaN(d.$dateValue.getTime())?"":e(d.$dateValue,i.dateFormat))},a.$on("$destroy",function(){j.destroy(),i=null,j=null})}}}]).provider("datepickerViews",function(){function a(a,b){for(var c=[];a.length>0;)c.push(a.splice(0,b));return c}this.defaults={dayFormat:"dd",daySplit:7};this.$get=["$locale","$sce","dateFilter",function(b,c,d){return function(e){var f=e.$scope,g=e.$options,h=b.DATETIME_FORMATS.SHORTDAY,i=h.slice(g.startWeek).concat(h.slice(0,g.startWeek)),j=c.trustAsHtml(''+i.join('')+""),k=e.$date||new Date,l={year:k.getFullYear(),month:k.getMonth(),date:k.getDate()},m=(6e4*k.getTimezoneOffset(),[{format:"dd",split:7,steps:{month:1},update:function(a,b){!this.built||b||a.getFullYear()!==l.year||a.getMonth()!==l.month?(angular.extend(l,{year:e.$date.getFullYear(),month:e.$date.getMonth(),date:e.$date.getDate()}),e.$build()):a.getDate()!==l.date&&(l.date=e.$date.getDate(),e.$updateSelected())},build:function(){for(var b,c=new Date(l.year,l.month,1),h=new Date(+c-864e5*(c.getDay()-g.startWeek)),i=[],k=0;42>k;k++)b=new Date(h.getFullYear(),h.getMonth(),h.getDate()+k),i.push({date:b,label:d(b,this.format),selected:e.$date&&this.isSelected(b),muted:b.getMonth()!==l.month,disabled:this.isDisabled(b)});f.title=d(c,"MMMM yyyy"),f.labels=j,f.rows=a(i,this.split),this.built=!0},isSelected:function(a){return e.$date&&a.getFullYear()===e.$date.getFullYear()&&a.getMonth()===e.$date.getMonth()&&a.getDate()===e.$date.getDate()},isDisabled:function(a){return a.getTime()g.maxDate},onKeyDown:function(a){var b=e.$date.getTime();37===a.keyCode?e.select(new Date(b-864e5),!0):38===a.keyCode?e.select(new Date(b-6048e5),!0):39===a.keyCode?e.select(new Date(b+864e5),!0):40===a.keyCode&&e.select(new Date(b+6048e5),!0)}},{name:"month",format:"MMM",split:4,steps:{year:1},update:function(a){this.built&&a.getFullYear()===l.year?a.getMonth()!==l.month&&(angular.extend(l,{month:e.$date.getMonth(),date:e.$date.getDate()}),e.$updateSelected()):(angular.extend(l,{year:e.$date.getFullYear(),month:e.$date.getMonth(),date:e.$date.getDate()}),e.$build())},build:function(){for(var b,c=(new Date(l.year,0,1),[]),g=0;12>g;g++)b=new Date(l.year,g,1),c.push({date:b,label:d(b,this.format),selected:e.$isSelected(b),disabled:this.isDisabled(b)});f.title=d(b,"yyyy"),f.labels=!1,f.rows=a(c,this.split),this.built=!0},isSelected:function(a){return e.$date&&a.getFullYear()===e.$date.getFullYear()&&a.getMonth()===e.$date.getMonth()},isDisabled:function(a){var b=+new Date(a.getFullYear(),a.getMonth()+1,0);return bg.maxDate},onKeyDown:function(a){var b=e.$date.getMonth();37===a.keyCode?e.select(e.$date.setMonth(b-1),!0):38===a.keyCode?e.select(e.$date.setMonth(b-4),!0):39===a.keyCode?e.select(e.$date.setMonth(b+1),!0):40===a.keyCode&&e.select(e.$date.setMonth(b+4),!0)}},{name:"year",format:"yyyy",split:4,steps:{year:12},update:function(a,b){!this.built||b||parseInt(a.getFullYear()/20,10)!==parseInt(l.year/20,10)?(angular.extend(l,{year:e.$date.getFullYear(),month:e.$date.getMonth(),date:e.$date.getDate()}),e.$build()):a.getFullYear()!==l.year&&(angular.extend(l,{year:e.$date.getFullYear(),month:e.$date.getMonth(),date:e.$date.getDate()}),e.$updateSelected())},build:function(){for(var b,c=l.year-l.year%(3*this.split),g=[],h=0;12>h;h++)b=new Date(c+h,0,1),g.push({date:b,label:d(b,this.format),selected:e.$isSelected(b),disabled:this.isDisabled(b)});f.title=g[0].label+"-"+g[g.length-1].label,f.labels=!1,f.rows=a(g,this.split),this.built=!0},isSelected:function(a){return e.$date&&a.getFullYear()===e.$date.getFullYear()},isDisabled:function(a){var b=+new Date(a.getFullYear()+1,0,0);return bg.maxDate},onKeyDown:function(a){var b=e.$date.getFullYear();37===a.keyCode?e.select(e.$date.setYear(b-1),!0):38===a.keyCode?e.select(e.$date.setYear(b-4),!0):39===a.keyCode?e.select(e.$date.setYear(b+1),!0):40===a.keyCode&&e.select(e.$date.setYear(b+4),!0)}}]);return{views:g.minView?Array.prototype.slice.call(m,g.minView):m,viewDate:l}}}]}),angular.module("mgcrea.ngStrap.dropdown",["mgcrea.ngStrap.tooltip"]).provider("$dropdown",function(){var a=this.defaults={animation:"am-fade",prefixClass:"dropdown",placement:"bottom-left",template:"dropdown/dropdown.tpl.html",trigger:"click",container:!1,keyboard:!0,html:!1,delay:0};this.$get=["$window","$rootScope","$tooltip",function(b,c,d){function e(b,e){function h(a){return a.target!==b[0]?a.target!==b[0]&&i.hide():void 0}{var i={},j=angular.extend({},a,e);i.$scope=j.scope&&j.scope.$new()||c.$new()}i=d(b,j),i.$onKeyDown=function(a){if(/(38|40)/.test(a.keyCode)){a.preventDefault(),a.stopPropagation();var b=angular.element(i.$element[0].querySelectorAll("li:not(.divider) a"));if(b.length){var c;angular.forEach(b,function(a,b){g&&g.call(a,":focus")&&(c=b)}),38===a.keyCode&&c>0?c--:40===a.keyCode&&c1){var g=f.search(c[b]);a=a.split(c[b]).join(""),m[c[b]]&&(d[g]=m[c[b]])}return angular.forEach(d,function(a){e.push(a)}),e}function f(a){return a.replace(/\//g,"[\\/]").replace("/-/g","[-]").replace(/\./g,"[.]").replace(/\\s/g,"[\\s]")}function g(a){var b,c=Object.keys(l),d=a;for(b=0;bj?d=setTimeout(i,b-j):(d=null,c||(h=a.apply(f,e)))},j=c&&!d;return d||(d=setTimeout(i,b)),j&&(h=a.apply(f,e)),h}}).constant("throttle",function(a,b,c){var d,e,f,g=null,h=0;c||(c={});var i=function(){h=c.leading===!1?0:new Date,g=null,f=a.apply(d,e)};return function(){var j=new Date;h||c.leading!==!1||(h=j);var k=b-(j-h);return d=this,e=arguments,0>=k?(clearTimeout(g),g=null,h=j,f=a.apply(d,e)):g||c.trailing===!1||(g=setTimeout(i,k)),f}}),angular.module("mgcrea.ngStrap.helpers.dimensions",[]).factory("dimensions",["$document","$window",function(){var b=(angular.element,{}),c=b.nodeName=function(a,b){return a.nodeName&&a.nodeName.toLowerCase()===b.toLowerCase()};b.css=function(b,c,d){var e;return e=b.currentStyle?b.currentStyle[c]:a.getComputedStyle?a.getComputedStyle(b)[c]:b.style[c],d===!0?parseFloat(e)||0:e},b.offset=function(b){var c=b.getBoundingClientRect(),d=b.ownerDocument;return{width:b.offsetWidth,height:b.offsetHeight,top:c.top+(a.pageYOffset||d.documentElement.scrollTop)-(d.documentElement.clientTop||0),left:c.left+(a.pageXOffset||d.documentElement.scrollLeft)-(d.documentElement.clientLeft||0)}},b.position=function(a){var e,f,g={top:0,left:0};return"fixed"===b.css(a,"position")?f=a.getBoundingClientRect():(e=d(a),f=b.offset(a),f=b.offset(a),c(e,"html")||(g=b.offset(e)),g.top+=b.css(e,"borderTopWidth",!0),g.left+=b.css(e,"borderLeftWidth",!0)),{width:a.offsetWidth,height:a.offsetHeight,top:f.top-g.top-b.css(a,"marginTop",!0),left:f.left-g.left-b.css(a,"marginLeft",!0)}};var d=function(a){var d=a.ownerDocument,e=a.offsetParent||d;if(c(e,"#document"))return d.documentElement;for(;e&&!c(e,"html")&&"static"===b.css(e,"position");)e=e.offsetParent;return e||d.documentElement};return b.height=function(a,c){var d=a.offsetHeight;return c?d+=b.css(a,"marginTop",!0)+b.css(a,"marginBottom",!0):d-=b.css(a,"paddingTop",!0)+b.css(a,"paddingBottom",!0)+b.css(a,"borderTopWidth",!0)+b.css(a,"borderBottomWidth",!0),d},b.width=function(a,c){var d=a.offsetWidth;return c?d+=b.css(a,"marginLeft",!0)+b.css(a,"marginRight",!0):d-=b.css(a,"paddingLeft",!0)+b.css(a,"paddingRight",!0)+b.css(a,"borderLeftWidth",!0)+b.css(a,"borderRightWidth",!0),d},b}]),angular.module("mgcrea.ngStrap.helpers.parseOptions",[]).provider("$parseOptions",function(){var a=this.defaults={regexp:/^\s*(.*?)(?:\s+as\s+(.*?))?(?:\s+group\s+by\s+(.*))?\s+for\s+(?:([\$\w][\$\w]*)|(?:\(\s*([\$\w][\$\w]*)\s*,\s*([\$\w][\$\w]*)\s*\)))\s+in\s+(.*?)(?:\s+track\s+by\s+(.*?))?$/};this.$get=["$parse","$q",function(b,c){function d(d,e){function f(a){return a.map(function(a,b){var c,d,e={};return e[k]=a,c=j(e),d=n(e)||b,{label:c,value:d}})}var g={},h=angular.extend({},a,e);g.$values=[];var i,j,k,l,m,n,o;return g.init=function(){g.$match=i=d.match(h.regexp),j=b(i[2]||i[1]),k=i[4]||i[6],l=i[5],m=b(i[3]||""),n=b(i[2]?i[1]:k),o=b(i[7])},g.valuesFn=function(a,b){return c.when(o(a,b)).then(function(a){return g.$values=a?f(a):{},g.$values})},g.init(),g}return d}]}),angular.module("mgcrea.ngStrap.modal",["mgcrea.ngStrap.helpers.dimensions"]).provider("$modal",function(){var a=this.defaults={animation:"am-fade",backdropAnimation:"am-fade",prefixClass:"modal",placement:"top",template:"modal/modal.tpl.html",contentTemplate:!1,container:!1,element:null,backdrop:!0,keyboard:!0,html:!1,show:!0};this.$get=["$window","$rootScope","$compile","$q","$templateCache","$http","$animate","$timeout","dimensions",function(c,d,e,f,g,h,i){function j(b){function c(a){a.target===a.currentTarget&&("static"===g.backdrop?f.focus():f.hide())}var f={},g=angular.extend({},a,b);f.$promise=l(g.template);var h=f.$scope=g.scope&&g.scope.$new()||d.$new();g.element||g.container||(g.container="body"),m(["title","content"],function(a){g[a]&&(h[a]=g[a])}),h.$hide=function(){h.$$postDigest(function(){f.hide()})},h.$show=function(){h.$$postDigest(function(){f.show()})},h.$toggle=function(){h.$$postDigest(function(){f.toggle()})},g.contentTemplate&&(f.$promise=f.$promise.then(function(a){var c=angular.element(a);return l(g.contentTemplate).then(function(a){var d=k('[ng-bind="content"]',c[0]).removeAttr("ng-bind").html(a);return b.template||d.next().remove(),c[0].outerHTML})}));var j,r,s=angular.element('
    ');return f.$promise.then(function(a){angular.isObject(a)&&(a=a.data),g.html&&(a=a.replace(q,'ng-bind-html="')),a=n.apply(a),j=e(a),f.init()}),f.init=function(){g.show&&h.$$postDigest(function(){f.show()})},f.destroy=function(){r&&(r.remove(),r=null),s&&(s.remove(),s=null),h.$destroy()},f.show=function(){var a=g.container?k(g.container):null,b=g.container?null:g.element;r=f.$element=j(h,function(){}),r.css({display:"block"}).addClass(g.placement),g.animation&&(g.backdrop&&s.addClass(g.backdropAnimation),r.addClass(g.animation)),g.backdrop&&i.enter(s,p,null,function(){}),i.enter(r,a,b,function(){}),h.$isShown=!0,h.$$phase||h.$digest();var d=r[0];o(function(){d.focus()}),p.addClass(g.prefixClass+"-open"),g.animation&&p.addClass(g.prefixClass+"-with-"+g.animation),g.backdrop&&(r.on("click",c),s.on("click",c)),g.keyboard&&r.on("keyup",f.$onKeyUp)},f.hide=function(){i.leave(r,function(){p.removeClass(g.prefixClass+"-open"),g.animation&&p.addClass(g.prefixClass+"-with-"+g.animation)}),g.backdrop&&i.leave(s,function(){}),h.$isShown=!1,h.$$phase||h.$digest(),g.backdrop&&(r.off("click",c),s.off("click",c)),g.keyboard&&r.off("keyup",f.$onKeyUp)},f.toggle=function(){h.$isShown?f.hide():f.show()},f.focus=function(){r[0].focus()},f.$onKeyUp=function(a){27===a.which&&f.hide()},f}function k(a,c){return angular.element((c||b).querySelectorAll(a))}function l(a){return f.when(g.get(a)||h.get(a)).then(function(b){return angular.isObject(b)?(g.put(a,b.data),b.data):b})}var m=angular.forEach,n=String.prototype.trim,o=c.requestAnimationFrame||c.setTimeout,p=angular.element(c.document.body),q=/ng-bind="/gi;return j}]}).directive("bsModal",["$window","$location","$sce","$modal",function(a,b,c,d){return{restrict:"EAC",scope:!0,link:function(a,b,e){var f={scope:a,element:b,show:!1};angular.forEach(["template","contentTemplate","placement","backdrop","keyboard","html","container","animation"],function(a){angular.isDefined(e[a])&&(f[a]=e[a])}),angular.forEach(["title","content"],function(b){e[b]&&e.$observe(b,function(d){a[b]=c.trustAsHtml(d)})}),e.bsModal&&a.$watch(e.bsModal,function(b){angular.isObject(b)?angular.extend(a,b):a.content=b},!0);var g=d(f);b.on(e.trigger||"click",g.toggle),a.$on("$destroy",function(){g.destroy(),f=null,g=null})}}}]),angular.module("mgcrea.ngStrap.navbar",[]).provider("$navbar",function(){var a=this.defaults={activeClass:"active",routeAttr:"data-match-route",strict:!1};this.$get=function(){return{defaults:a}}}).directive("bsNavbar",["$window","$location","$navbar",function(a,b,c){var d=c.defaults;return{restrict:"A",link:function(a,c,e){var f=angular.copy(d);angular.forEach(Object.keys(d),function(a){angular.isDefined(e[a])&&(f[a]=e[a])}),a.$watch(function(){return b.path()},function(a){var b=c[0].querySelectorAll("li["+f.routeAttr+"]");angular.forEach(b,function(b){var c=angular.element(b),d=c.attr(f.routeAttr).replace("/","\\/");f.strict&&(d="^"+d+"$");var e=new RegExp(d,["i"]);e.test(a)?c.addClass(f.activeClass):c.removeClass(f.activeClass)})})}}}]),angular.module("mgcrea.ngStrap.popover",["mgcrea.ngStrap.tooltip"]).provider("$popover",function(){var a=this.defaults={animation:"am-fade",placement:"right",template:"popover/popover.tpl.html",contentTemplate:!1,trigger:"click",keyboard:!0,html:!1,title:"",content:"",delay:0,container:!1};this.$get=["$tooltip",function(b){function c(c,d){var e=angular.extend({},a,d),f=b(c,e);return e.content&&(f.$scope.content=e.content),f}return c}]}).directive("bsPopover",["$window","$location","$sce","$popover",function(a,b,c,d){var e=a.requestAnimationFrame||a.setTimeout;return{restrict:"EAC",scope:!0,link:function(a,b,f){var g={scope:a};angular.forEach(["template","contentTemplate","placement","container","delay","trigger","keyboard","html","animation"],function(a){angular.isDefined(f[a])&&(g[a]=f[a])}),angular.forEach(["title","content"],function(b){f[b]&&f.$observe(b,function(d,f){a[b]=c.trustAsHtml(d),angular.isDefined(f)&&e(function(){h&&h.$applyPlacement()})})}),f.bsPopover&&a.$watch(f.bsPopover,function(b,c){angular.isObject(b)?angular.extend(a,b):a.content=b,angular.isDefined(c)&&e(function(){h&&h.$applyPlacement()})},!0);var h=d(b,g);a.$on("$destroy",function(){h.destroy(),g=null,h=null})}}}]),angular.module("mgcrea.ngStrap.scrollspy",["mgcrea.ngStrap.helpers.debounce","mgcrea.ngStrap.helpers.dimensions"]).provider("$scrollspy",function(){var a=this.$$spies={},c=this.defaults={debounce:150,throttle:100,offset:100};this.$get=["$window","$document","$rootScope","dimensions","debounce","throttle",function(d,e,f,g,h,i){function j(a,b){return a[0].nodeName&&a[0].nodeName.toLowerCase()===b.toLowerCase()}function k(e){var k=angular.extend({},c,e);k.element||(k.element=n);var o=j(k.element,"body"),p=o?l:k.element,q=o?"window":k.id;if(a[q])return a[q].$$count++,a[q];var r,s,t,u,v,w,x,y,z={},A=z.$trackedElements=[],B=[];return z.init=function(){this.$$count=1,u=h(this.checkPosition,k.debounce),v=i(this.checkPosition,k.throttle),p.on("click",this.checkPositionWithEventLoop),l.on("resize",u),p.on("scroll",v),w=h(this.checkOffsets,k.debounce),r=f.$on("$viewContentLoaded",w),s=f.$on("$includeContentLoaded",w),w(),q&&(a[q]=z)},z.destroy=function(){this.$$count--,this.$$count>0||(p.off("click",this.checkPositionWithEventLoop),l.off("resize",u),p.off("scroll",u),r(),s())},z.checkPosition=function(){if(B.length){if(y=(o?d.pageYOffset:p.prop("scrollTop"))||0,x=Math.max(d.innerHeight,m.prop("clientHeight")),yB[a+1].offsetTop))return z.$activateElement(B[a])}},z.checkPositionWithEventLoop=function(){setTimeout(this.checkPosition,1)},z.$activateElement=function(a){if(t){var b=z.$getTrackedElement(t);b&&(b.source.removeClass("active"),j(b.source,"li")&&j(b.source.parent().parent(),"li")&&b.source.parent().parent().removeClass("active"))}t=a.target,a.source.addClass("active"),j(a.source,"li")&&j(a.source.parent().parent(),"li")&&a.source.parent().parent().addClass("active")},z.$getTrackedElement=function(a){return A.filter(function(b){return b.target===a})[0]},z.checkOffsets=function(){angular.forEach(A,function(a){var c=b.querySelector(a.target);a.offsetTop=c?g.offset(c).top:null,k.offset&&null!==a.offsetTop&&(a.offsetTop-=1*k.offset)}),B=A.filter(function(a){return null!==a.offsetTop}).sort(function(a,b){return a.offsetTop-b.offsetTop}),u()},z.trackElement=function(a,b){A.push({target:a,source:b})},z.untrackElement=function(a,b){for(var c,d=A.length;d--;)if(A[d].target===a&&A[d].source===b){c=d;break}A=A.splice(c,1)},z.activate=function(a){A[a].addClass("active")},z.init(),z}var l=angular.element(d),m=angular.element(e.prop("documentElement")),n=angular.element(d.document.body);return k}]}).directive("bsScrollspy",["$rootScope","debounce","dimensions","$scrollspy",function(a,b,c,d){return{restrict:"EAC",link:function(a,b,c){var e={scope:a}; -angular.forEach(["offset","target"],function(a){angular.isDefined(c[a])&&(e[a]=c[a])});var f=d(e);f.trackElement(e.target,b),a.$on("$destroy",function(){f.untrackElement(e.target,b),f.destroy(),e=null,f=null})}}}]).directive("bsScrollspyList",["$rootScope","debounce","dimensions","$scrollspy",function(){return{restrict:"A",compile:function(a){var b=a[0].querySelectorAll("li > a[href]");angular.forEach(b,function(a){var b=angular.element(a);b.parent().attr("bs-scrollspy","").attr("data-target",b.attr("href"))})}}}]),angular.module("mgcrea.ngStrap.select",["mgcrea.ngStrap.tooltip","mgcrea.ngStrap.helpers.parseOptions"]).provider("$select",function(){var a=this.defaults={animation:"am-fade",prefixClass:"select",placement:"bottom-left",template:"select/select.tpl.html",trigger:"focus",container:!1,keyboard:!0,html:!1,delay:0,multiple:!1,sort:!0,caretHtml:' ',placeholder:"Choose among the following...",maxLength:3,maxLengthHtml:"selected"};this.$get=["$window","$document","$rootScope","$tooltip",function(b,c,d,e){function f(b,c,d){var f={},h=angular.extend({},a,d);f=e(b,h);var i=d.scope,j=f.$scope;j.$matches=[],j.$activeIndex=0,j.$isMultiple=h.multiple,j.$activate=function(a){j.$$postDigest(function(){f.activate(a)})},j.$select=function(a){j.$$postDigest(function(){f.select(a)})},j.$isVisible=function(){return f.$isVisible()},j.$isActive=function(a){return f.$isActive(a)},f.update=function(a){j.$matches=a,f.$updateActiveIndex()},f.activate=function(a){return h.multiple?(j.$activeIndex.sort(),f.$isActive(a)?j.$activeIndex.splice(j.$activeIndex.indexOf(a),1):j.$activeIndex.push(a),h.sort&&j.$activeIndex.sort()):j.$activeIndex=a,j.$activeIndex},f.select=function(a){var d=j.$matches[a].value;f.activate(a),c.$setViewValue(h.multiple?j.$activeIndex.map(function(a){return j.$matches[a].value}):d),c.$render(),i&&i.$digest(),h.multiple||("focus"===h.trigger?b[0].blur():f.$isShown&&f.hide()),j.$emit("$select.select",d,a)},f.$updateActiveIndex=function(){c.$modelValue&&j.$matches.length?j.$activeIndex=h.multiple&&angular.isArray(c.$modelValue)?c.$modelValue.map(function(a){return f.$getIndex(a)}):f.$getIndex(c.$modelValue):j.$activeIndex>=j.$matches.length&&(j.$activeIndex=h.multiple?[]:0)},f.$isVisible=function(){return h.minLength&&c?j.$matches.length&&c.$viewValue.length>=h.minLength:j.$matches.length},f.$isActive=function(a){return h.multiple?-1!==j.$activeIndex.indexOf(a):j.$activeIndex===a},f.$getIndex=function(a){var b=j.$matches.length,c=b;if(b){for(c=b;c--&&j.$matches[c].value!==a;);if(!(0>c))return c}},f.$onMouseDown=function(a){if(a.preventDefault(),a.stopPropagation(),g){var b=angular.element(a.target);b.triggerHandler("click")}},f.$onKeyDown=function(a){if(/(38|40|13)/.test(a.keyCode)){if(a.preventDefault(),a.stopPropagation(),13===a.keyCode)return f.select(j.$activeIndex);38===a.keyCode&&j.$activeIndex>0?j.$activeIndex--:40===a.keyCode&&j.$activeIndex'),i.after(b)}var j=e(c.ngOptions),k=d(b,g,h),l=j.$match[7].replace(/\|.+/,"").trim();a.$watch(l,function(){j.valuesFn(a,g).then(function(a){k.update(a),g.$render()})},!0),a.$watch(c.ngModel,function(){k.$updateActiveIndex()},!0),g.$render=function(){var a,d;h.multiple&&angular.isArray(g.$modelValue)?(a=g.$modelValue.map(function(a){return d=k.$getIndex(a),angular.isDefined(d)?k.$scope.$matches[d].label:!1}).filter(angular.isDefined),a=a.length>(h.maxLength||f.maxLength)?a.length+" "+(h.maxLengthHtml||f.maxLengthHtml):a.join(", ")):(d=k.$getIndex(g.$modelValue),a=angular.isDefined(d)?k.$scope.$matches[d].label:!1),b.html((a?a:c.placeholder||f.placeholder)+f.caretHtml)},a.$on("$destroy",function(){k.destroy(),h=null,k=null})}}}]),angular.module("mgcrea.ngStrap.tab",[]).run(["$templateCache",function(a){a.put("$pane","{{pane.content}}")}]).provider("$tab",function(){var a=this.defaults={animation:"am-fade",template:"tab/tab.tpl.html"};this.$get=function(){return{defaults:a}}}).directive("bsTabs",["$window","$animate","$tab",function(a,b,c){var d=c.defaults;return{restrict:"EAC",scope:!0,require:"?ngModel",templateUrl:function(a,b){return b.template||d.template},link:function(a,b,c,e){var f=d;angular.forEach(["animation"],function(a){angular.isDefined(c[a])&&(f[a]=c[a])}),c.bsTabs&&a.$watch(c.bsTabs,function(b){a.panes=b},!0),b.addClass("tabs"),f.animation&&b.addClass(f.animation),a.active=a.activePane=0,a.setActive=function(b){a.active=b,e&&e.$setViewValue(b)},e&&(e.$render=function(){a.active=1*e.$modelValue})}}}]),angular.module("mgcrea.ngStrap.timepicker",["mgcrea.ngStrap.helpers.dateParser","mgcrea.ngStrap.tooltip"]).provider("$timepicker",function(){var a=this.defaults={animation:"am-fade",prefixClass:"timepicker",placement:"bottom-left",template:"timepicker/timepicker.tpl.html",trigger:"focus",container:!1,keyboard:!0,html:!1,delay:0,useNative:!0,timeType:"date",timeFormat:"shortTime",autoclose:!1,minTime:-1/0,maxTime:+1/0,length:5,hourStep:1,minuteStep:5};this.$get=["$window","$document","$rootScope","$sce","$locale","dateFilter","$tooltip",function(b,c,d,e,f,g,h){function i(b,c,d){function e(a,c){if(b[0].createTextRange){var d=b[0].createTextRange();d.collapse(!0),d.moveStart("character",a),d.moveEnd("character",c),d.select()}else b[0].setSelectionRange?b[0].setSelectionRange(a,c):angular.isUndefined(b[0].selectionStart)&&(b[0].selectionStart=a,b[0].selectionEnd=c)}function i(){b[0].focus()}var l=h(b,angular.extend({},a,d)),m=d.scope,n=l.$options,o=l.$scope,p=0,q=c.$dateValue||new Date,r={hour:q.getHours(),meridian:q.getHours()<12,minute:q.getMinutes(),second:q.getSeconds(),millisecond:q.getMilliseconds()},s=f.DATETIME_FORMATS[n.timeFormat]||n.timeFormat,t=/(h+)[:]?(m+)[ ]?(a?)/i.exec(s).slice(1);o.$select=function(a,b){l.select(a,b)},o.$moveIndex=function(a,b){l.$moveIndex(a,b)},o.$switchMeridian=function(a){l.switchMeridian(a)},l.update=function(a){angular.isDate(a)&&!isNaN(a.getTime())?(l.$date=a,angular.extend(r,{hour:a.getHours(),minute:a.getMinutes(),second:a.getSeconds(),millisecond:a.getMilliseconds()}),l.$build()):l.$isBuilt||l.$build()},l.select=function(a,b,d){isNaN(c.$dateValue.getTime())&&(c.$dateValue=new Date(1970,0,1)),angular.isDate(a)||(a=new Date(a)),0===b?c.$dateValue.setHours(a.getHours()):1===b&&c.$dateValue.setMinutes(a.getMinutes()),c.$setViewValue(c.$dateValue),c.$render(),n.autoclose&&!d&&l.hide(!0)},l.switchMeridian=function(a){var b=(a||c.$dateValue).getHours();c.$dateValue.setHours(12>b?b+12:b-12),c.$render()},l.$build=function(){var a,b,c=o.midIndex=parseInt(n.length/2,10),d=[];for(a=0;an.maxTime},l.$moveIndex=function(a,b){var c;0===b?(c=new Date(1970,0,1,r.hour+a*n.length,r.minute),angular.extend(r,{hour:c.getHours()})):1===b&&(c=new Date(1970,0,1,r.hour,r.minute+a*n.length*5),angular.extend(r,{minute:c.getMinutes()})),l.$build()},l.$onMouseDown=function(a){if("input"!==a.target.nodeName.toLowerCase()&&a.preventDefault(),a.stopPropagation(),j){var b=angular.element(a.target);"button"!==b[0].nodeName.toLowerCase()&&(b=b.parent()),b.triggerHandler("click")}},l.$onKeyDown=function(a){if(/(38|37|39|40|13)/.test(a.keyCode)&&!a.shiftKey&&!a.altKey){if(a.preventDefault(),a.stopPropagation(),13===a.keyCode)return l.hide(!0);var b=new Date(l.$date),c=b.getHours(),d=g(b,"h").length,f=b.getMinutes(),h=g(b,"mm").length,i=/(37|39)/.test(a.keyCode),j=2+1*!!t[2];if(i&&(37===a.keyCode?p=1>p?j-1:p-1:39===a.keyCode&&(p=j-1>p?p+1:0)),0===p){if(i)return e(0,d);38===a.keyCode?b.setHours(c-n.hourStep):40===a.keyCode&&b.setHours(c+n.hourStep)}else if(1===p){if(i)return e(d+1,d+1+h);38===a.keyCode?b.setMinutes(f-n.minuteStep):40===a.keyCode&&b.setMinutes(f+n.minuteStep)}else if(2===p){if(i)return e(d+1+h+1,d+1+h+3);l.switchMeridian()}l.select(b,p,!0),m.$digest()}};var u=l.init;l.init=function(){return k&&n.useNative?(b.prop("type","time"),void b.css("-webkit-appearance","textfield")):(j&&(b.prop("type","text"),b.attr("readonly","true"),b.on("click",i)),void u())};var v=l.destroy;l.destroy=function(){k&&n.useNative&&b.off("click",i),v()};var w=l.show;l.show=function(){w(),setTimeout(function(){l.$element.on(j?"touchstart":"mousedown",l.$onMouseDown),n.keyboard&&b.on("keydown",l.$onKeyDown)})};var x=l.hide;return l.hide=function(a){l.$element.off(j?"touchstart":"mousedown",l.$onMouseDown),n.keyboard&&b.off("keydown",l.$onKeyDown),x(a)},l}var j=(angular.element(b.document.body),"createTouch"in b.document),k=/(ip(a|o)d|iphone|android)/gi.test(b.navigator.userAgent);return a.lang||(a.lang=f.id),i.defaults=a,i}]}).directive("bsTimepicker",["$window","$parse","$q","$locale","dateFilter","$timepicker","$dateParser","$timeout",function(a,b,c,d,e,f,g){{var h=f.defaults,i=/(ip(a|o)d|iphone|android)/gi.test(a.navigator.userAgent);a.requestAnimationFrame||a.setTimeout}return{restrict:"EAC",require:"ngModel",link:function(a,b,c,d){var j={scope:a,controller:d};angular.forEach(["placement","container","delay","trigger","keyboard","html","animation","template","autoclose","timeType","timeFormat","useNative","lang"],function(a){angular.isDefined(c[a])&&(j[a]=c[a])}),i&&(j.useNative||h.useNative)&&(j.timeFormat="HH:mm");var k=f(b,d,j);j=k.$options;var l=g({format:j.timeFormat,lang:j.lang});angular.forEach(["minTime","maxTime"],function(a){angular.isDefined(c[a])&&c.$observe(a,function(b){k.$options[a]="now"===b?(new Date).setFullYear(1970,0,1):angular.isString(b)&&b.match(/^".+"$/)?+new Date(b.substr(1,b.length-2)):l.parse(b),!isNaN(k.$options[a])&&k.$build()})}),a.$watch(c.ngModel,function(){k.update(d.$dateValue)},!0),d.$parsers.unshift(function(a){if(!a)return void d.$setValidity("date",!0);var b=l.parse(a,d.$dateValue);if(!b||isNaN(b.getTime()))d.$setValidity("date",!1);else{var c=b.getTime()>=j.minTime&&b.getTime()<=j.maxTime;d.$setValidity("date",c),c&&(d.$dateValue=b)}return"string"===j.timeType?e(a,j.timeFormat):"number"===j.timeType?d.$dateValue.getTime():"iso"===j.timeType?d.$dateValue.toISOString():d.$dateValue}),d.$formatters.push(function(a){var b="string"===j.timeType?l.parse(a,d.$dateValue):new Date(a);return d.$dateValue=b,d.$dateValue}),d.$render=function(){b.val(isNaN(d.$dateValue.getTime())?"":e(d.$dateValue,j.timeFormat))},a.$on("$destroy",function(){k.destroy(),j=null,k=null})}}}]),angular.module("mgcrea.ngStrap.tooltip",["ngAnimate","mgcrea.ngStrap.helpers.dimensions"]).provider("$tooltip",function(){var a=this.defaults={animation:"am-fade",prefixClass:"tooltip",container:!1,placement:"top",template:"tooltip/tooltip.tpl.html",contentTemplate:!1,trigger:"hover focus",keyboard:!1,html:!1,show:!1,title:"",type:"",delay:0};this.$get=["$window","$rootScope","$compile","$q","$templateCache","$http","$animate","$timeout","dimensions","$$animateReflow",function(c,d,e,f,g,h,i,j,k,l){function m(b,c){function f(){return"body"===j.container?k.offset(b[0]):k.position(b[0])}function g(a,b,c,d){var e,f=a.split("-");switch(f[0]){case"right":e={top:b.top+b.height/2-d/2,left:b.left+b.width};break;case"bottom":e={top:b.top+b.height,left:b.left+b.width/2-c/2};break;case"left":e={top:b.top+b.height/2-d/2,left:b.left-c};break;default:e={top:b.top-d,left:b.left+b.width/2-c/2}}if(!f[1])return e;if("top"===f[0]||"bottom"===f[0])switch(f[1]){case"left":e.left=b.left;break;case"right":e.left=b.left+b.width-c}else if("left"===f[0]||"right"===f[0])switch(f[1]){case"top":e.top=b.top-d;break;case"bottom":e.top=b.top+b.height}return e}var h={},j=h.$options=angular.extend({},a,c);h.$promise=o(j.template);var m=h.$scope=j.scope&&j.scope.$new()||d.$new();j.delay&&angular.isString(j.delay)&&(j.delay=parseFloat(j.delay)),j.title&&(h.$scope.title=j.title),m.$hide=function(){m.$$postDigest(function(){h.hide()})},m.$show=function(){m.$$postDigest(function(){h.show()})},m.$toggle=function(){m.$$postDigest(function(){h.toggle()})},h.$isShown=m.$isShown=!1;var s,t;j.contentTemplate&&(h.$promise=h.$promise.then(function(a){var b=angular.element(a);return o(j.contentTemplate).then(function(a){return n('[ng-bind="content"]',b[0]).removeAttr("ng-bind").html(a),b[0].outerHTML})}));var u,v,w,x;return h.$promise.then(function(a){angular.isObject(a)&&(a=a.data),j.html&&(a=a.replace(r,'ng-bind-html="')),a=p.apply(a),w=a,u=e(a),h.init()}),h.init=function(){j.delay&&angular.isNumber(j.delay)&&(j.delay={show:j.delay,hide:j.delay}),"self"===j.container?x=b:j.container&&(x=n(j.container));var a=j.trigger.split(" ");angular.forEach(a,function(a){"click"===a?b.on("click",h.toggle):"manual"!==a&&(b.on("hover"===a?"mouseenter":"focus",h.enter),b.on("hover"===a?"mouseleave":"blur",h.leave),"hover"!==a&&b.on(q?"touchstart":"mousedown",h.$onFocusElementMouseDown))}),j.show&&m.$$postDigest(function(){"focus"===j.trigger?b[0].focus():h.show()})},h.destroy=function(){for(var a=j.trigger.split(" "),c=a.length;c--;){var d=a[c];"click"===d?b.off("click",h.toggle):"manual"!==d&&(b.off("hover"===d?"mouseenter":"focus",h.enter),b.off("hover"===d?"mouseleave":"blur",h.leave),"hover"!==d&&b.off(q?"touchstart":"mousedown",h.$onFocusElementMouseDown))}v&&(v.remove(),v=null),m.$destroy()},h.enter=function(){return clearTimeout(s),t="in",j.delay&&j.delay.show?void(s=setTimeout(function(){"in"===t&&h.show()},j.delay.show)):h.show()},h.show=function(){var a=j.container?x:null,c=j.container?null:b;v&&v.remove(),v=h.$element=u(m,function(){}),v.css({top:"0px",left:"0px",display:"block"}).addClass(j.placement),j.animation&&v.addClass(j.animation),j.type&&v.addClass(j.prefixClass+"-"+j.type),i.enter(v,a,c,function(){}),h.$isShown=m.$isShown=!0,m.$$phase||m.$digest(),l(h.$applyPlacement),j.keyboard&&("focus"!==j.trigger?(h.focus(),v.on("keyup",h.$onKeyUp)):b.on("keyup",h.$onFocusKeyUp))},h.leave=function(){return clearTimeout(s),t="out",j.delay&&j.delay.hide?void(s=setTimeout(function(){"out"===t&&h.hide()},j.delay.hide)):h.hide()},h.hide=function(a){return h.$isShown?(i.leave(v,function(){v=null}),h.$isShown=m.$isShown=!1,m.$$phase||m.$digest(),j.keyboard&&v.off("keyup",h.$onKeyUp),a&&"focus"===j.trigger?b[0].blur():void 0):void 0},h.toggle=function(){h.$isShown?h.leave():h.enter()},h.focus=function(){v[0].focus()},h.$applyPlacement=function(){if(v){var a=f(),b=v.prop("offsetWidth"),c=v.prop("offsetHeight"),d=g(j.placement,a,b,c);d.top+="px",d.left+="px",v.css(d)}},h.$onKeyUp=function(a){27===a.which&&h.hide()},h.$onFocusKeyUp=function(a){27===a.which&&b[0].blur()},h.$onFocusElementMouseDown=function(a){a.preventDefault(),a.stopPropagation(),h.$isShown?b[0].blur():b[0].focus()},h}function n(a,c){return angular.element((c||b).querySelectorAll(a))}function o(a){return f.when(g.get(a)||h.get(a)).then(function(b){return angular.isObject(b)?(g.put(a,b.data),b.data):b})}var p=String.prototype.trim,q="createTouch"in c.document,r=/ng-bind="/gi;return m}]}).directive("bsTooltip",["$window","$location","$sce","$tooltip","$$animateReflow",function(a,b,c,d,e){return{restrict:"EAC",scope:!0,link:function(a,b,f){var g={scope:a};angular.forEach(["template","contentTemplate","placement","container","delay","trigger","keyboard","html","animation","type"],function(a){angular.isDefined(f[a])&&(g[a]=f[a])}),angular.forEach(["title"],function(b){f[b]&&f.$observe(b,function(d,f){a[b]=c.trustAsHtml(d),angular.isDefined(f)&&e(function(){h&&h.$applyPlacement()})})}),f.bsTooltip&&a.$watch(f.bsTooltip,function(b,c){angular.isObject(b)?angular.extend(a,b):a.content=b,angular.isDefined(c)&&e(function(){h&&h.$applyPlacement()})},!0);var h=d(b,g);a.$on("$destroy",function(){h.destroy(),g=null,h=null})}}}]),angular.module("mgcrea.ngStrap.typeahead",["mgcrea.ngStrap.tooltip","mgcrea.ngStrap.helpers.parseOptions"]).provider("$typeahead",function(){var a=this.defaults={animation:"am-fade",prefixClass:"typeahead",placement:"bottom-left",template:"typeahead/typeahead.tpl.html",trigger:"focus",container:!1,keyboard:!0,html:!1,delay:0,minLength:1,filter:"filter",limit:6};this.$get=["$window","$rootScope","$tooltip",function(b,c,d){function e(b,c){var e={},f=angular.extend({},a,c),g=f.controller;e=d(b,f);var h=c.scope,i=e.$scope;i.$matches=[],i.$activeIndex=0,i.$activate=function(a){i.$$postDigest(function(){e.activate(a)})},i.$select=function(a){i.$$postDigest(function(){e.select(a)})},i.$isVisible=function(){return e.$isVisible()},e.update=function(a){i.$matches=a,i.$activeIndex>=a.length&&(i.$activeIndex=0)},e.activate=function(a){i.$activeIndex=a},e.select=function(a){var c=i.$matches[a].value;g&&(g.$setViewValue(c),g.$render(),h&&h.$digest()),"focus"===f.trigger?b[0].blur():e.$isShown&&e.hide(),i.$activeIndex=0,i.$emit("$typeahead.select",c,a)},e.$isVisible=function(){return f.minLength&&g?i.$matches.length&&angular.isString(g.$viewValue)&&g.$viewValue.length>=f.minLength:!!i.$matches.length},e.$onMouseDown=function(a){a.preventDefault(),a.stopPropagation()},e.$onKeyDown=function(a){if(/(38|40|13)/.test(a.keyCode)){if(a.preventDefault(),a.stopPropagation(),13===a.keyCode)return e.select(i.$activeIndex);38===a.keyCode&&i.$activeIndex>0?i.$activeIndex--:40===a.keyCode&&i.$activeIndexj&&(a=a.slice(0,j)),m.update(a)})}),a.$on("$destroy",function(){m.destroy(),h=null,m=null})}}}])}(window,document),function(){"use strict";angular.module("mgcrea.ngStrap.alert").run(["$templateCache",function(a){a.put("alert/alert.tpl.html",'
     
    ')}]),angular.module("mgcrea.ngStrap.aside").run(["$templateCache",function(a){a.put("aside/aside.tpl.html",'')}]),angular.module("mgcrea.ngStrap.datepicker").run(["$templateCache",function(a){a.put("datepicker/datepicker.tpl.html",'')}]),angular.module("mgcrea.ngStrap.dropdown").run(["$templateCache",function(a){a.put("dropdown/dropdown.tpl.html",'')}]),angular.module("mgcrea.ngStrap.modal").run(["$templateCache",function(a){a.put("modal/modal.tpl.html",'')}]),angular.module("mgcrea.ngStrap.popover").run(["$templateCache",function(a){a.put("popover/popover.tpl.html",'

    ')}]),angular.module("mgcrea.ngStrap.select").run(["$templateCache",function(a){a.put("select/select.tpl.html",'')}]),angular.module("mgcrea.ngStrap.tab").run(["$templateCache",function(a){a.put("tab/tab.tpl.html",'
    ')}]),angular.module("mgcrea.ngStrap.timepicker").run(["$templateCache",function(a){a.put("timepicker/timepicker.tpl.html",'')}]),angular.module("mgcrea.ngStrap.tooltip").run(["$templateCache",function(a){a.put("tooltip/tooltip.tpl.html",'
    ')}]),angular.module("mgcrea.ngStrap.typeahead").run(["$templateCache",function(a){a.put("typeahead/typeahead.tpl.html",'')}])}(window,document); -//# sourceMappingURL=angular-strap.min.map \ No newline at end of file +!function(e,t,n){"use strict";angular.module("mgcrea.ngStrap",["mgcrea.ngStrap.modal","mgcrea.ngStrap.aside","mgcrea.ngStrap.alert","mgcrea.ngStrap.button","mgcrea.ngStrap.select","mgcrea.ngStrap.datepicker","mgcrea.ngStrap.timepicker","mgcrea.ngStrap.navbar","mgcrea.ngStrap.tooltip","mgcrea.ngStrap.popover","mgcrea.ngStrap.dropdown","mgcrea.ngStrap.typeahead","mgcrea.ngStrap.scrollspy","mgcrea.ngStrap.affix","mgcrea.ngStrap.tab","mgcrea.ngStrap.collapse"]),angular.module("mgcrea.ngStrap.affix",["mgcrea.ngStrap.helpers.dimensions","mgcrea.ngStrap.helpers.debounce"]).provider("$affix",function(){var e=this.defaults={offsetTop:"auto",inlineStyles:!0};this.$get=["$window","debounce","dimensions",function(t,n,a){function o(o,s){function l(e,t,n){var a=u(),o=c();return v>=a?"top":null!==e&&a+e<=t.top?"middle":null!==y&&t.top+n+$>=o-y?"bottom":"middle"}function u(){return p[0]===t?t.pageYOffset:p[0].scrollTop}function c(){return p[0]===t?t.document.body.scrollHeight:p[0].scrollHeight}var d={},f=angular.extend({},e,s),p=f.target,g="affix affix-top affix-bottom",m=!1,$=0,h=0,v=0,y=0,w=null,b=null,D=o.parent();if(f.offsetParent)if(f.offsetParent.match(/^\d+$/))for(var k=0;k<1*f.offsetParent-1;k++)D=D.parent();else D=angular.element(f.offsetParent);return d.init=function(){this.$parseOffsets(),h=a.offset(o[0]).top+$,m=!o[0].style.width,p.on("scroll",this.checkPosition),p.on("click",this.checkPositionWithEventLoop),r.on("resize",this.$debouncedOnResize),this.checkPosition(),this.checkPositionWithEventLoop()},d.destroy=function(){p.off("scroll",this.checkPosition),p.off("click",this.checkPositionWithEventLoop),r.off("resize",this.$debouncedOnResize)},d.checkPositionWithEventLoop=function(){setTimeout(d.checkPosition,1)},d.checkPosition=function(){var e=u(),t=a.offset(o[0]),n=a.height(o[0]),r=l(b,t,n);w!==r&&(w=r,o.removeClass(g).addClass("affix"+("middle"!==r?"-"+r:"")),"top"===r?(b=null,m&&o.css("width",""),f.inlineStyles&&(o.css("position",f.offsetParent?"":"relative"),o.css("top",""))):"bottom"===r?(b=f.offsetUnpin?-(1*f.offsetUnpin):t.top-e,m&&o.css("width",""),f.inlineStyles&&(o.css("position",f.offsetParent?"":"relative"),o.css("top",f.offsetParent?"":i[0].offsetHeight-y-n-h+"px"))):(b=null,m&&o.css("width",o[0].offsetWidth+"px"),f.inlineStyles&&(o.css("position","fixed"),o.css("top",$+"px"))))},d.$onResize=function(){d.$parseOffsets(),d.checkPosition()},d.$debouncedOnResize=n(d.$onResize,50),d.$parseOffsets=function(){var e=o.css("position");f.inlineStyles&&o.css("position",f.offsetParent?"":"relative"),f.offsetTop&&("auto"===f.offsetTop&&(f.offsetTop="+0"),f.offsetTop.match(/^[-+]\d+$/)?($=1*-f.offsetTop,v=f.offsetParent?a.offset(D[0]).top+1*f.offsetTop:a.offset(o[0]).top-a.css(o[0],"marginTop",!0)+1*f.offsetTop):v=1*f.offsetTop),f.offsetBottom&&(y=f.offsetParent&&f.offsetBottom.match(/^[-+]\d+$/)?c()-(a.offset(D[0]).top+a.height(D[0]))+1*f.offsetBottom+1:1*f.offsetBottom),f.inlineStyles&&o.css("position",e)},d.init(),d}var i=angular.element(t.document.body),r=angular.element(t);return o}]}).directive("bsAffix",["$affix","$window",function(e,t){return{restrict:"EAC",require:"^?bsAffixTarget",link:function(n,a,o,i){var r={scope:n,target:i?i.$element:angular.element(t)};angular.forEach(["offsetTop","offsetBottom","offsetParent","offsetUnpin","inlineStyles"],function(e){if(angular.isDefined(o[e])){var t=o[e];/true/i.test(t)&&(t=!0),/false/i.test(t)&&(t=!1),r[e]=t}});var s=e(a,r);n.$on("$destroy",function(){s&&s.destroy(),r=null,s=null})}}}]).directive("bsAffixTarget",function(){return{controller:["$element",function(e){this.$element=e}]}}),angular.module("mgcrea.ngStrap.aside",["mgcrea.ngStrap.modal"]).provider("$aside",function(){var e=this.defaults={animation:"am-fade-and-slide-right",prefixClass:"aside",prefixEvent:"aside",placement:"right",template:"aside/aside.tpl.html",contentTemplate:!1,container:!1,element:null,backdrop:!0,keyboard:!0,html:!1,show:!0};this.$get=["$modal",function(t){function n(n){var a={},o=angular.extend({},e,n);return a=t(o)}return n}]}).directive("bsAside",["$window","$sce","$aside",function(e,t,n){e.requestAnimationFrame||e.setTimeout;return{restrict:"EAC",scope:!0,link:function(e,a,o){var i={scope:e,element:a,show:!1};angular.forEach(["template","contentTemplate","placement","backdrop","keyboard","html","container","animation"],function(e){angular.isDefined(o[e])&&(i[e]=o[e])}),angular.forEach(["title","content"],function(n){o[n]&&o.$observe(n,function(a){e[n]=t.trustAsHtml(a)})}),o.bsAside&&e.$watch(o.bsAside,function(t){angular.isObject(t)?angular.extend(e,t):e.content=t},!0);var r=n(i);a.on(o.trigger||"click",r.toggle),e.$on("$destroy",function(){r&&r.destroy(),i=null,r=null})}}}]),angular.module("mgcrea.ngStrap.alert",["mgcrea.ngStrap.modal"]).provider("$alert",function(){var e=this.defaults={animation:"am-fade",prefixClass:"alert",prefixEvent:"alert",placement:null,template:"alert/alert.tpl.html",container:!1,element:null,backdrop:!1,keyboard:!0,show:!0,duration:!1,type:!1,dismissable:!0};this.$get=["$modal","$timeout",function(t,n){function a(a){var o={},i=angular.extend({},e,a);o=t(i),o.$scope.dismissable=!!i.dismissable,i.type&&(o.$scope.type=i.type);var r=o.show;return i.duration&&(o.show=function(){r(),n(function(){o.hide()},1e3*i.duration)}),o}return a}]}).directive("bsAlert",["$window","$sce","$alert",function(e,t,n){e.requestAnimationFrame||e.setTimeout;return{restrict:"EAC",scope:!0,link:function(e,a,o){var i={scope:e,element:a,show:!1};angular.forEach(["template","placement","keyboard","html","container","animation","duration","dismissable"],function(e){angular.isDefined(o[e])&&(i[e]=o[e])}),e.hasOwnProperty("title")||(e.title=""),angular.forEach(["title","content","type"],function(n){o[n]&&o.$observe(n,function(a){e[n]=t.trustAsHtml(a)})}),o.bsAlert&&e.$watch(o.bsAlert,function(t){angular.isObject(t)?angular.extend(e,t):e.content=t},!0);var r=n(i);a.on(o.trigger||"click",r.toggle),e.$on("$destroy",function(){r&&r.destroy(),i=null,r=null})}}}]),angular.module("mgcrea.ngStrap.button",[]).provider("$button",function(){var e=this.defaults={activeClass:"active",toggleEvent:"click"};this.$get=function(){return{defaults:e}}}).directive("bsCheckboxGroup",function(){return{restrict:"A",require:"ngModel",compile:function(e,t){e.attr("data-toggle","buttons"),e.removeAttr("ng-model");var n=e[0].querySelectorAll('input[type="checkbox"]');angular.forEach(n,function(e){var n=angular.element(e);n.attr("bs-checkbox",""),n.attr("ng-model",t.ngModel+"."+n.attr("value"))})}}}).directive("bsCheckbox",["$button","$$rAF",function(e,t){var n=e.defaults,a=/^(true|false|\d+)$/;return{restrict:"A",require:"ngModel",link:function(e,o,i,r){var s=n,l="INPUT"===o[0].nodeName,u=l?o.parent():o,c=angular.isDefined(i.trueValue)?i.trueValue:!0;a.test(i.trueValue)&&(c=e.$eval(i.trueValue));var d=angular.isDefined(i.falseValue)?i.falseValue:!1;a.test(i.falseValue)&&(d=e.$eval(i.falseValue));var f="boolean"!=typeof c||"boolean"!=typeof d;f&&(r.$parsers.push(function(e){return e?c:d}),r.$formatters.push(function(e){return angular.equals(e,c)}),e.$watch(i.ngModel,function(){r.$render()})),r.$render=function(){var e=angular.equals(r.$modelValue,c);t(function(){l&&(o[0].checked=e),u.toggleClass(s.activeClass,e)})},o.bind(s.toggleEvent,function(){e.$apply(function(){l||r.$setViewValue(!u.hasClass("active")),f||r.$render()})})}}}]).directive("bsRadioGroup",function(){return{restrict:"A",require:"ngModel",compile:function(e,t){e.attr("data-toggle","buttons"),e.removeAttr("ng-model");var n=e[0].querySelectorAll('input[type="radio"]');angular.forEach(n,function(e){angular.element(e).attr("bs-radio",""),angular.element(e).attr("ng-model",t.ngModel)})}}}).directive("bsRadio",["$button","$$rAF",function(e,t){var n=e.defaults,a=/^(true|false|\d+)$/;return{restrict:"A",require:"ngModel",link:function(e,o,i,r){var s,l=n,u="INPUT"===o[0].nodeName,c=u?o.parent():o;i.$observe("value",function(t){s=a.test(t)?e.$eval(t):t,r.$render()}),r.$render=function(){var e=angular.equals(r.$modelValue,s);t(function(){u&&(o[0].checked=e),c.toggleClass(l.activeClass,e)})},o.bind(l.toggleEvent,function(){e.$apply(function(){r.$setViewValue(s),r.$render()})})}}}]),angular.module("mgcrea.ngStrap.collapse",[]).provider("$collapse",function(){var e=this.defaults={animation:"am-collapse",disallowToggle:!1,activeClass:"in",startCollapsed:!1,allowMultiple:!1},t=this.controller=function(t,n,a){function o(e){for(var t=l.$targets.$active,n=0;nt;t++)angular.forEach(g.rows[t],u.$setDisabledEl)},u.select=function(e,t){angular.isDate(n.$dateValue)||(n.$dateValue=new Date(e)),!g.$mode||t?(n.$setViewValue(angular.copy(e)),n.$render(),p.autoclose&&!t&&l(function(){u.hide(!0)})):(angular.extend($,{year:e.getFullYear(),month:e.getMonth(),date:e.getDate()}),u.setMode(g.$mode-1),u.$build())},u.setMode=function(e){g.$mode=e,h=u.$views[g.$mode],u.$build()},u.$build=function(e){e===!0&&h.built||(e!==!1||h.built)&&h.build.call(h)},u.$updateSelected=function(){for(var e=0,t=g.rows.length;t>e;e++)angular.forEach(g.rows[e],o)},u.$isSelected=function(e){return h.isSelected(e)},u.$setDisabledEl=function(e){e.disabled=h.isDisabled(e.date)},u.$selectPane=function(e){var t=h.steps,n=new Date(Date.UTC($.year+(t.year||0)*e,$.month+(t.month||0)*e,1));angular.extend($,{year:n.getUTCFullYear(),month:n.getUTCMonth(),date:n.getUTCDate()}),u.$build()},u.$onMouseDown=function(e){if(e.preventDefault(),e.stopPropagation(),d){var t=angular.element(e.target);"button"!==t[0].nodeName.toLowerCase()&&(t=t.parent()),t.triggerHandler("click")}},u.$onKeyDown=function(e){if(/(38|37|39|40|13)/.test(e.keyCode)&&!e.shiftKey&&!e.altKey){if(e.preventDefault(),e.stopPropagation(),13===e.keyCode)return g.$mode?g.$apply(function(){u.setMode(g.$mode-1)}):u.hide(!0);h.onKeyDown(e),f.$digest()}};var v=u.init;u.init=function(){return c&&p.useNative?(t.prop("type","date"),void t.css("-webkit-appearance","textfield")):(d&&(t.prop("type","text"),t.attr("readonly","true"),t.on("click",i)),void v())};var y=u.destroy;u.destroy=function(){c&&p.useNative&&t.off("click",i),y()};var w=u.show;u.show=function(){w(),l(function(){u.$isShown&&(u.$element.on(d?"touchstart":"mousedown",u.$onMouseDown),p.keyboard&&t.on("keydown",u.$onKeyDown))},0,!1)};var b=u.hide;return u.hide=function(e){u.$isShown&&(u.$element.off(d?"touchstart":"mousedown",u.$onMouseDown),p.keyboard&&t.off("keydown",u.$onKeyDown),b(e))},u}var c=(angular.element(t.document.body),/(ip(a|o)d|iphone|android)/gi.test(t.navigator.userAgent)),d="createTouch"in t.document&&c;return e.lang||(e.lang=i.getDefaultLocale()),u.defaults=e,u}]}).directive("bsDatepicker",["$window","$parse","$q","$dateFormatter","$dateParser","$datepicker",function(e,t,n,a,o,i){var r=(i.defaults,/(ip(a|o)d|iphone|android)/gi.test(e.navigator.userAgent));return{restrict:"EAC",require:"ngModel",link:function(e,t,n,s){function l(e){return e&&e.length?e:null}function u(e){if(angular.isDate(e)){var t=isNaN(f.$options.minDate)||e.getTime()>=f.$options.minDate,n=isNaN(f.$options.maxDate)||e.getTime()<=f.$options.maxDate,a=t&&n;s.$setValidity("date",a),s.$setValidity("min",t),s.$setValidity("max",n),a&&(s.$dateValue=e)}}function c(){return!s.$dateValue||isNaN(s.$dateValue.getTime())?"":g(s.$dateValue,d.dateFormat)}var d={scope:e,controller:s};angular.forEach(["placement","container","delay","trigger","keyboard","html","animation","template","autoclose","dateType","dateFormat","timezone","modelDateFormat","dayFormat","strictFormat","startWeek","startDate","useNative","lang","startView","minView","iconLeft","iconRight","daysOfWeekDisabled","id"],function(e){angular.isDefined(n[e])&&(d[e]=n[e])}),n.bsShow&&e.$watch(n.bsShow,function(e){f&&angular.isDefined(e)&&(angular.isString(e)&&(e=!!e.match(/true|,?(datepicker),?/i)),e===!0?f.show():f.hide())});var f=i(t,s,d);d=f.$options,r&&d.useNative&&(d.dateFormat="yyyy-MM-dd");var p=d.lang,g=function(e,t){return a.formatDate(e,t,p)},m=o({format:d.dateFormat,lang:p,strict:d.strictFormat});angular.forEach(["minDate","maxDate"],function(e){angular.isDefined(n[e])&&n.$observe(e,function(t){f.$options[e]=m.getDateForAttribute(e,t),!isNaN(f.$options[e])&&f.$build(!1),u(s.$dateValue)})}),e.$watch(n.ngModel,function(){f.update(s.$dateValue)},!0),angular.isDefined(n.disabledDates)&&e.$watch(n.disabledDates,function(e,t){e=l(e),t=l(t),e&&f.updateDisabledDates(e)}),s.$parsers.unshift(function(e){var t;if(!e)return s.$setValidity("date",!0),null;var n=m.parse(e,s.$dateValue);return!n||isNaN(n.getTime())?void s.$setValidity("date",!1):(u(n),"string"===d.dateType?(t=m.timezoneOffsetAdjust(n,d.timezone,!0),g(t,d.modelDateFormat||d.dateFormat)):(t=m.timezoneOffsetAdjust(s.$dateValue,d.timezone,!0),"number"===d.dateType?t.getTime():"unix"===d.dateType?t.getTime()/1e3:"iso"===d.dateType?t.toISOString():new Date(t)))}),s.$formatters.push(function(e){var t;return t=angular.isUndefined(e)||null===e?0/0:angular.isDate(e)?e:"string"===d.dateType?m.parse(e,null,d.modelDateFormat):new Date("unix"===d.dateType?1e3*e:e),s.$dateValue=m.timezoneOffsetAdjust(t,d.timezone),c()}),s.$render=function(){t.val(c())},e.$on("$destroy",function(){f&&f.destroy(),d=null,f=null})}}}]).provider("datepickerViews",function(){function e(e,t){for(var n=[];e.length>0;)n.push(e.splice(0,t));return n}function t(e,t){return(e%t+t)%t}this.defaults={dayFormat:"dd",daySplit:7};this.$get=["$dateFormatter","$dateParser","$sce",function(n,a,o){return function(i){var r=i.$scope,s=i.$options,l=s.lang,u=function(e,t){return n.formatDate(e,t,l)},c=a({format:s.dateFormat,lang:l,strict:s.strictFormat}),d=n.weekdaysShort(l),f=d.slice(s.startWeek).concat(d.slice(0,s.startWeek)),p=o.trustAsHtml(''+f.join('')+""),g=i.$date||(s.startDate?c.getDateForAttribute("startDate",s.startDate):new Date),m={year:g.getFullYear(),month:g.getMonth(),date:g.getDate()},$=[{format:s.dayFormat,split:7,steps:{month:1},update:function(e,t){!this.built||t||e.getFullYear()!==m.year||e.getMonth()!==m.month?(angular.extend(m,{year:i.$date.getFullYear(),month:i.$date.getMonth(),date:i.$date.getDate()}),i.$build()):e.getDate()!==m.date&&(m.date=i.$date.getDate(),i.$updateSelected())},build:function(){var n=new Date(m.year,m.month,1),a=n.getTimezoneOffset(),o=new Date(+n-864e5*t(n.getDay()-s.startWeek,7)),l=o.getTimezoneOffset(),d=c.timezoneOffsetAdjust(new Date,s.timezone).toDateString();l!==a&&(o=new Date(+o+6e4*(l-a)));for(var f,g=[],$=0;42>$;$++)f=c.daylightSavingAdjust(new Date(o.getFullYear(),o.getMonth(),o.getDate()+$)),g.push({date:f,isToday:f.toDateString()===d,label:u(f,this.format),selected:i.$date&&this.isSelected(f),muted:f.getMonth()!==m.month,disabled:this.isDisabled(f)});r.title=u(n,s.monthTitleFormat),r.showLabels=!0,r.labels=p,r.rows=e(g,this.split),this.built=!0},isSelected:function(e){return i.$date&&e.getFullYear()===i.$date.getFullYear()&&e.getMonth()===i.$date.getMonth()&&e.getDate()===i.$date.getDate()},isDisabled:function(e){var t=e.getTime();if(ts.maxDate)return!0;if(-1!==s.daysOfWeekDisabled.indexOf(e.getDay()))return!0;if(s.disabledDateRanges)for(var n=0;n=s.disabledDateRanges[n].start&&t<=s.disabledDateRanges[n].end)return!0;return!1},onKeyDown:function(e){if(i.$date){var t,n=i.$date.getTime();37===e.keyCode?t=new Date(n-864e5):38===e.keyCode?t=new Date(n-6048e5):39===e.keyCode?t=new Date(n+864e5):40===e.keyCode&&(t=new Date(n+6048e5)),this.isDisabled(t)||i.select(t,!0)}}},{name:"month",format:s.monthFormat,split:4,steps:{year:1},update:function(e){this.built&&e.getFullYear()===m.year?e.getMonth()!==m.month&&(angular.extend(m,{month:i.$date.getMonth(),date:i.$date.getDate()}),i.$updateSelected()):(angular.extend(m,{year:i.$date.getFullYear(),month:i.$date.getMonth(),date:i.$date.getDate()}),i.$build())},build:function(){for(var t,n=(new Date(m.year,0,1),[]),a=0;12>a;a++)t=new Date(m.year,a,1),n.push({date:t,label:u(t,this.format),selected:i.$isSelected(t),disabled:this.isDisabled(t)});r.title=u(t,s.yearTitleFormat),r.showLabels=!1,r.rows=e(n,this.split),this.built=!0},isSelected:function(e){return i.$date&&e.getFullYear()===i.$date.getFullYear()&&e.getMonth()===i.$date.getMonth()},isDisabled:function(e){var t=+new Date(e.getFullYear(),e.getMonth()+1,0);return ts.maxDate},onKeyDown:function(e){if(i.$date){var t=i.$date.getMonth(),n=new Date(i.$date);37===e.keyCode?n.setMonth(t-1):38===e.keyCode?n.setMonth(t-4):39===e.keyCode?n.setMonth(t+1):40===e.keyCode&&n.setMonth(t+4),this.isDisabled(n)||i.select(n,!0)}}},{name:"year",format:s.yearFormat,split:4,steps:{year:12},update:function(e,t){!this.built||t||parseInt(e.getFullYear()/20,10)!==parseInt(m.year/20,10)?(angular.extend(m,{year:i.$date.getFullYear(),month:i.$date.getMonth(),date:i.$date.getDate()}),i.$build()):e.getFullYear()!==m.year&&(angular.extend(m,{year:i.$date.getFullYear(),month:i.$date.getMonth(),date:i.$date.getDate()}),i.$updateSelected())},build:function(){for(var t,n=m.year-m.year%(3*this.split),a=[],o=0;12>o;o++)t=new Date(n+o,0,1),a.push({date:t,label:u(t,this.format),selected:i.$isSelected(t),disabled:this.isDisabled(t)});r.title=a[0].label+"-"+a[a.length-1].label,r.showLabels=!1,r.rows=e(a,this.split),this.built=!0},isSelected:function(e){return i.$date&&e.getFullYear()===i.$date.getFullYear()},isDisabled:function(e){var t=+new Date(e.getFullYear()+1,0,0);return ts.maxDate},onKeyDown:function(e){if(i.$date){var t=i.$date.getFullYear(),n=new Date(i.$date);37===e.keyCode?n.setYear(t-1):38===e.keyCode?n.setYear(t-4):39===e.keyCode?n.setYear(t+1):40===e.keyCode&&n.setYear(t+4),this.isDisabled(n)||i.select(n,!0)}}}];return{views:s.minView?Array.prototype.slice.call($,s.minView):$,viewDate:m}}}]}),angular.module("mgcrea.ngStrap.dropdown",["mgcrea.ngStrap.tooltip"]).provider("$dropdown",function(){var e=this.defaults={animation:"am-fade",prefixClass:"dropdown",prefixEvent:"dropdown",placement:"bottom-left",template:"dropdown/dropdown.tpl.html",trigger:"click",container:!1,keyboard:!0,html:!1,delay:0};this.$get=["$window","$rootScope","$tooltip","$timeout",function(t,n,a,o){function i(t,i){function l(e){return e.target!==t[0]?e.target!==t[0]&&u.hide():void 0}{var u={},c=angular.extend({},e,i);u.$scope=c.scope&&c.scope.$new()||n.$new()}u=a(t,c);var d=t.parent();u.$onKeyDown=function(e){if(/(38|40)/.test(e.keyCode)){e.preventDefault(),e.stopPropagation();var t=angular.element(u.$element[0].querySelectorAll("li:not(.divider) a"));if(t.length){var n;angular.forEach(t,function(e,t){s&&s.call(e,":focus")&&(n=t)}),38===e.keyCode&&n>0?n--:40===e.keyCode&&no;o++)if(e[o].toLowerCase()===a)return o;return-1}e.prototype.setMilliseconds=function(e){this.milliseconds=e},e.prototype.setSeconds=function(e){this.seconds=e},e.prototype.setMinutes=function(e){this.minutes=e},e.prototype.setHours=function(e){this.hours=e},e.prototype.getHours=function(){return this.hours},e.prototype.setDate=function(e){this.day=e},e.prototype.setMonth=function(e){this.month=e},e.prototype.setFullYear=function(e){this.year=e},e.prototype.fromDate=function(e){return this.year=e.getFullYear(),this.month=e.getMonth(),this.day=e.getDate(),this.hours=e.getHours(),this.minutes=e.getMinutes(),this.seconds=e.getSeconds(),this.milliseconds=e.getMilliseconds(),this},e.prototype.toDate=function(){return new Date(this.year,this.month,this.day,this.hours,this.minutes,this.seconds,this.milliseconds)};var o=e.prototype,i=this.defaults={format:"shortDate",strict:!1};this.$get=["$locale","dateFilter",function(r,s){var l=function(l){function u(e){var t,n=Object.keys(h),a=[],o=[],i=e;for(t=0;t1){var r=i.search(n[t]);e=e.split(n[t]).join(""),h[n[t]]&&(a[r]=h[n[t]])}return angular.forEach(a,function(e){e&&o.push(e)}),o}function c(e){return e.replace(/\//g,"[\\/]").replace("/-/g","[-]").replace(/\./g,"[.]").replace(/\\s/g,"[\\s]")}function d(e){var t,n=Object.keys($),a=e;for(t=0;t12?e.getHours()+2:0),e):null},m.timezoneOffsetAdjust=function(e,t,n){return e?(t&&"UTC"===t&&(e=new Date(e.getTime()),e.setMinutes(e.getMinutes()+(n?-1:1)*e.getTimezoneOffset())),e):null},m.init(),m};return l}]}]),angular.module("mgcrea.ngStrap.helpers.debounce",[]).factory("debounce",["$timeout",function(e){return function(t,n,a){var o=null;return function(){var i=this,r=arguments,s=a&&!o;return o&&e.cancel(o),o=e(function(){o=null,a||t.apply(i,r)},n,!1),s&&t.apply(i,r),o}}}]).factory("throttle",["$timeout",function(e){return function(t,n,a){var o=null;return a||(a={}),function(){var i=this,r=arguments;o||(a.leading!==!1&&t.apply(i,r),o=e(function(){o=null,a.trailing!==!1&&t.apply(i,r)},n,!1))}}}]),angular.module("mgcrea.ngStrap.helpers.dimensions",[]).factory("dimensions",["$document","$window",function(){var t=(angular.element,{}),n=t.nodeName=function(e,t){return e.nodeName&&e.nodeName.toLowerCase()===t.toLowerCase()};t.css=function(t,n,a){var o;return o=t.currentStyle?t.currentStyle[n]:e.getComputedStyle?e.getComputedStyle(t)[n]:t.style[n],a===!0?parseFloat(o)||0:o},t.offset=function(t){var n=t.getBoundingClientRect(),a=t.ownerDocument;return{width:n.width||t.offsetWidth,height:n.height||t.offsetHeight,top:n.top+(e.pageYOffset||a.documentElement.scrollTop)-(a.documentElement.clientTop||0),left:n.left+(e.pageXOffset||a.documentElement.scrollLeft)-(a.documentElement.clientLeft||0)}},t.setOffset=function(e,n,a){var o,i,r,s,l,u,c,d=t.css(e,"position"),f=angular.element(e),p={};"static"===d&&(e.style.position="relative"),l=t.offset(e),r=t.css(e,"top"),u=t.css(e,"left"),c=("absolute"===d||"fixed"===d)&&(r+u).indexOf("auto")>-1,c?(o=t.position(e),s=o.top,i=o.left):(s=parseFloat(r)||0,i=parseFloat(u)||0),angular.isFunction(n)&&(n=n.call(e,a,l)),null!==n.top&&(p.top=n.top-l.top+s),null!==n.left&&(p.left=n.left-l.left+i),"using"in n?n.using.call(f,p):f.css({top:p.top+"px",left:p.left+"px"})},t.position=function(e){var o,i,r={top:0,left:0};return"fixed"===t.css(e,"position")?i=e.getBoundingClientRect():(o=a(e),i=t.offset(e),n(o,"html")||(r=t.offset(o)),r.top+=t.css(o,"borderTopWidth",!0),r.left+=t.css(o,"borderLeftWidth",!0)),{width:e.offsetWidth,height:e.offsetHeight,top:i.top-r.top-t.css(e,"marginTop",!0),left:i.left-r.left-t.css(e,"marginLeft",!0)}};var a=function(e){var a=e.ownerDocument,o=e.offsetParent||a;if(n(o,"#document"))return a.documentElement;for(;o&&!n(o,"html")&&"static"===t.css(o,"position");)o=o.offsetParent;return o||a.documentElement};return t.height=function(e,n){var a=e.offsetHeight;return n?a+=t.css(e,"marginTop",!0)+t.css(e,"marginBottom",!0):a-=t.css(e,"paddingTop",!0)+t.css(e,"paddingBottom",!0)+t.css(e,"borderTopWidth",!0)+t.css(e,"borderBottomWidth",!0),a +},t.width=function(e,n){var a=e.offsetWidth;return n?a+=t.css(e,"marginLeft",!0)+t.css(e,"marginRight",!0):a-=t.css(e,"paddingLeft",!0)+t.css(e,"paddingRight",!0)+t.css(e,"borderLeftWidth",!0)+t.css(e,"borderRightWidth",!0),a},t}]),angular.module("mgcrea.ngStrap.helpers.parseOptions",[]).provider("$parseOptions",function(){var e=this.defaults={regexp:/^\s*(.*?)(?:\s+as\s+(.*?))?(?:\s+group\s+by\s+(.*))?\s+for\s+(?:([\$\w][\$\w]*)|(?:\(\s*([\$\w][\$\w]*)\s*,\s*([\$\w][\$\w]*)\s*\)))\s+in\s+(.*?)(?:\s+track\s+by\s+(.*?))?$/};this.$get=["$parse","$q",function(t,n){function a(a,o){function i(e,t){return e.map(function(e,n){var a,o,i={};return i[c]=e,a=u(t,i),o=p(t,i),{label:a,value:o,index:n}})}var r={},s=angular.extend({},e,o);r.$values=[];var l,u,c,d,f,p,g;return r.init=function(){r.$match=l=a.match(s.regexp),u=t(l[2]||l[1]),c=l[4]||l[6],d=l[5],f=t(l[3]||""),p=t(l[2]?l[1]:c),g=t(l[7])},r.valuesFn=function(e,t){return n.when(g(e,t)).then(function(t){return r.$values=t?i(t,e):{},r.$values})},r.displayValue=function(e){var t={};return t[c]=e,u(t)},r.init(),r}return a}]}),angular.version.minor<3&&angular.version.dot<14&&angular.module("ng").factory("$$rAF",["$window","$timeout",function(e,t){var n=e.requestAnimationFrame||e.webkitRequestAnimationFrame||e.mozRequestAnimationFrame,a=e.cancelAnimationFrame||e.webkitCancelAnimationFrame||e.mozCancelAnimationFrame||e.webkitCancelRequestAnimationFrame,o=!!n,i=o?function(e){var t=n(e);return function(){a(t)}}:function(e){var n=t(e,16.66,!1);return function(){t.cancel(n)}};return i.supported=o,i}]),angular.module("mgcrea.ngStrap.modal",["mgcrea.ngStrap.helpers.dimensions"]).provider("$modal",function(){var e=this.defaults={animation:"am-fade",backdropAnimation:"am-fade",prefixClass:"modal",prefixEvent:"modal",placement:"top",template:"modal/modal.tpl.html",contentTemplate:!1,container:!1,element:null,backdrop:!0,keyboard:!0,html:!1,show:!0};this.$get=["$window","$rootScope","$compile","$q","$templateCache","$http","$animate","$timeout","$sce","dimensions",function(n,a,o,i,r,s,l,u,c){function d(t){function n(){w.$emit(d.prefixEvent+".show",u)}function i(){w.$emit(d.prefixEvent+".hide",u),v.removeClass(d.prefixClass+"-open"),d.animation&&v.removeClass(d.prefixClass+"-with-"+d.animation)}function r(e){e.target===e.currentTarget&&("static"===d.backdrop?u.focus():u.hide())}function s(e){e.preventDefault()}var u={},d=u.$options=angular.extend({},e,t);u.$promise=g(d.template);var w=u.$scope=d.scope&&d.scope.$new()||a.$new();d.element||d.container||(d.container="body"),u.$id=d.id||d.element&&d.element.attr("id")||"",m(["title","content"],function(e){d[e]&&(w[e]=c.trustAsHtml(d[e]))}),w.$hide=function(){w.$$postDigest(function(){u.hide()})},w.$show=function(){w.$$postDigest(function(){u.show()})},w.$toggle=function(){w.$$postDigest(function(){u.toggle()})},u.$isShown=w.$isShown=!1,d.contentTemplate&&(u.$promise=u.$promise.then(function(e){var n=angular.element(e);return g(d.contentTemplate).then(function(e){var a=p('[ng-bind="content"]',n[0]).removeAttr("ng-bind").html(e);return t.template||a.next().remove(),n[0].outerHTML})}));var b,D,k=angular.element('
    ');return k.css({position:"fixed",top:"0px",left:"0px",bottom:"0px",right:"0px","z-index":1038}),u.$promise.then(function(e){angular.isObject(e)&&(e=e.data),d.html&&(e=e.replace(y,'ng-bind-html="')),e=$.apply(e),b=o(e),u.init()}),u.init=function(){d.show&&w.$$postDigest(function(){u.show()})},u.destroy=function(){D&&(D.remove(),D=null),k&&(k.remove(),k=null),w.$destroy()},u.show=function(){if(!u.$isShown){var e,t;if(angular.isElement(d.container)?(e=d.container,t=d.container[0].lastChild?angular.element(d.container[0].lastChild):null):d.container?(e=p(d.container),t=e[0].lastChild?angular.element(e[0].lastChild):null):(e=null,t=d.element),D=u.$element=b(w,function(){}),!w.$emit(d.prefixEvent+".show.before",u).defaultPrevented){D.css({display:"block"}).addClass(d.placement),d.animation&&(d.backdrop&&k.addClass(d.backdropAnimation),D.addClass(d.animation)),d.backdrop&&l.enter(k,v,null);var a=l.enter(D,e,t,n);a&&a.then&&a.then(n),u.$isShown=w.$isShown=!0,f(w);var o=D[0];h(function(){o.focus()}),v.addClass(d.prefixClass+"-open"),d.animation&&v.addClass(d.prefixClass+"-with-"+d.animation),d.backdrop&&(D.on("click",r),k.on("click",r),k.on("wheel",s)),d.keyboard&&D.on("keyup",u.$onKeyUp)}}},u.hide=function(){if(u.$isShown&&!w.$emit(d.prefixEvent+".hide.before",u).defaultPrevented){var e=l.leave(D,i);e&&e.then&&e.then(i),d.backdrop&&l.leave(k),u.$isShown=w.$isShown=!1,f(w),d.backdrop&&(D.off("click",r),k.off("click",r),k.off("wheel",s)),d.keyboard&&D.off("keyup",u.$onKeyUp)}},u.toggle=function(){u.$isShown?u.hide():u.show()},u.focus=function(){D[0].focus()},u.$onKeyUp=function(e){27===e.which&&u.$isShown&&(u.hide(),e.stopPropagation())},u}function f(e){e.$$phase||e.$root&&e.$root.$$phase||e.$digest()}function p(e,n){return angular.element((n||t).querySelectorAll(e))}function g(e){return w[e]?w[e]:w[e]=s.get(e,{cache:r}).then(function(e){return e.data})}var m=angular.forEach,$=String.prototype.trim,h=n.requestAnimationFrame||n.setTimeout,v=angular.element(n.document.body),y=/ng-bind="/gi,w={};return d}]}).directive("bsModal",["$window","$sce","$modal",function(e,t,n){return{restrict:"EAC",scope:!0,link:function(e,a,o){var i={scope:e,element:a,show:!1};angular.forEach(["template","contentTemplate","placement","container","animation","id"],function(e){angular.isDefined(o[e])&&(i[e]=o[e])});var r=/^(false|0|)$/;angular.forEach(["keyboard","html"],function(e){angular.isDefined(o[e])&&(i[e]=!r.test(o[e]))}),angular.isDefined(o.backdrop)&&(i.backdrop=r.test(o.backdrop)?!1:o.backdrop),angular.forEach(["title","content"],function(n){o[n]&&o.$observe(n,function(a){e[n]=t.trustAsHtml(a)})}),o.bsModal&&e.$watch(o.bsModal,function(t){angular.isObject(t)?angular.extend(e,t):e.content=t},!0);var s=n(i);a.on(o.trigger||"click",s.toggle),e.$on("$destroy",function(){s&&s.destroy(),i=null,s=null})}}}]),angular.module("mgcrea.ngStrap.navbar",[]).provider("$navbar",function(){var e=this.defaults={activeClass:"active",routeAttr:"data-match-route",strict:!1};this.$get=function(){return{defaults:e}}}).directive("bsNavbar",["$window","$location","$navbar",function(e,t,n){var a=n.defaults;return{restrict:"A",link:function(e,n,o){var i=angular.copy(a);angular.forEach(Object.keys(a),function(e){angular.isDefined(o[e])&&(i[e]=o[e])}),e.$watch(function(){return t.path()},function(e){var t=n[0].querySelectorAll("li["+i.routeAttr+"]");angular.forEach(t,function(t){var n=angular.element(t),a=n.attr(i.routeAttr).replace("/","\\/");i.strict&&(a="^"+a+"$");var o=new RegExp(a,"i");o.test(e)?n.addClass(i.activeClass):n.removeClass(i.activeClass)})})}}}]),angular.module("mgcrea.ngStrap.popover",["mgcrea.ngStrap.tooltip"]).provider("$popover",function(){var e=this.defaults={animation:"am-fade",customClass:"",container:!1,target:!1,placement:"right",template:"popover/popover.tpl.html",contentTemplate:!1,trigger:"click",keyboard:!0,html:!1,title:"",content:"",delay:0,autoClose:!1};this.$get=["$tooltip",function(t){function n(n,a){var o=angular.extend({},e,a),i=t(n,o);return o.content&&(i.$scope.content=o.content),i}return n}]}).directive("bsPopover",["$window","$sce","$popover",function(e,t,n){var a=e.requestAnimationFrame||e.setTimeout;return{restrict:"EAC",scope:!0,link:function(e,o,i){var r={scope:e};angular.forEach(["template","contentTemplate","placement","container","target","delay","trigger","keyboard","html","animation","customClass","autoClose","id"],function(e){angular.isDefined(i[e])&&(r[e]=i[e])}),angular.forEach(["title","content"],function(n){i[n]&&i.$observe(n,function(o,i){e[n]=t.trustAsHtml(o),angular.isDefined(i)&&a(function(){s&&s.$applyPlacement()})})}),i.bsPopover&&e.$watch(i.bsPopover,function(t,n){angular.isObject(t)?angular.extend(e,t):e.content=t,angular.isDefined(n)&&a(function(){s&&s.$applyPlacement()})},!0),i.bsShow&&e.$watch(i.bsShow,function(e){s&&angular.isDefined(e)&&(angular.isString(e)&&(e=!!e.match(/true|,?(popover),?/i)),e===!0?s.show():s.hide())}),i.viewport&&e.$watch(i.viewport,function(e){s&&angular.isDefined(e)&&s.setViewport(e)});var s=n(o,r);e.$on("$destroy",function(){s&&s.destroy(),r=null,s=null})}}}]),angular.module("mgcrea.ngStrap.scrollspy",["mgcrea.ngStrap.helpers.debounce","mgcrea.ngStrap.helpers.dimensions"]).provider("$scrollspy",function(){var e=this.$$spies={},n=this.defaults={debounce:150,throttle:100,offset:100};this.$get=["$window","$document","$rootScope","dimensions","debounce","throttle",function(a,o,i,r,s,l){function u(e,t){return e[0].nodeName&&e[0].nodeName.toLowerCase()===t.toLowerCase()}function c(o){var c=angular.extend({},n,o);c.element||(c.element=p);var g=u(c.element,"body"),m=g?d:c.element,$=g?"window":c.id;if(e[$])return e[$].$$count++,e[$];var h,v,y,w,b,D,k,S,x={},T=x.$trackedElements=[],C=[];return x.init=function(){this.$$count=1,w=s(this.checkPosition,c.debounce),b=l(this.checkPosition,c.throttle),m.on("click",this.checkPositionWithEventLoop),d.on("resize",w),m.on("scroll",b),D=s(this.checkOffsets,c.debounce),h=i.$on("$viewContentLoaded",D),v=i.$on("$includeContentLoaded",D),D(),$&&(e[$]=x)},x.destroy=function(){this.$$count--,this.$$count>0||(m.off("click",this.checkPositionWithEventLoop),d.off("resize",w),m.off("scroll",b),h(),v(),$&&delete e[$])},x.checkPosition=function(){if(C.length){if(S=(g?a.pageYOffset:m.prop("scrollTop"))||0,k=Math.max(a.innerHeight,f.prop("clientHeight")),SC[e+1].offsetTop))return x.$activateElement(C[e])}},x.checkPositionWithEventLoop=function(){setTimeout(x.checkPosition,1)},x.$activateElement=function(e){if(y){var t=x.$getTrackedElement(y);t&&(t.source.removeClass("active"),u(t.source,"li")&&u(t.source.parent().parent(),"li")&&t.source.parent().parent().removeClass("active"))}y=e.target,e.source.addClass("active"),u(e.source,"li")&&u(e.source.parent().parent(),"li")&&e.source.parent().parent().addClass("active")},x.$getTrackedElement=function(e){return T.filter(function(t){return t.target===e})[0]},x.checkOffsets=function(){angular.forEach(T,function(e){var n=t.querySelector(e.target);e.offsetTop=n?r.offset(n).top:null,c.offset&&null!==e.offsetTop&&(e.offsetTop-=1*c.offset)}),C=T.filter(function(e){return null!==e.offsetTop}).sort(function(e,t){return e.offsetTop-t.offsetTop}),w()},x.trackElement=function(e,t){T.push({target:e,source:t})},x.untrackElement=function(e,t){for(var n,a=T.length;a--;)if(T[a].target===e&&T[a].source===t){n=a;break}T=T.splice(n,1)},x.activate=function(e){T[e].addClass("active")},x.init(),x}var d=angular.element(a),f=angular.element(o.prop("documentElement")),p=angular.element(a.document.body);return c}]}).directive("bsScrollspy",["$rootScope","debounce","dimensions","$scrollspy",function(e,t,n,a){return{restrict:"EAC",link:function(e,t,n){var o={scope:e};angular.forEach(["offset","target"],function(e){angular.isDefined(n[e])&&(o[e]=n[e])});var i=a(o);i.trackElement(o.target,t),e.$on("$destroy",function(){i&&(i.untrackElement(o.target,t),i.destroy()),o=null,i=null})}}}]).directive("bsScrollspyList",["$rootScope","debounce","dimensions","$scrollspy",function(){return{restrict:"A",compile:function(e){var t=e[0].querySelectorAll("li > a[href]");angular.forEach(t,function(e){var t=angular.element(e);t.parent().attr("bs-scrollspy","").attr("data-target",t.attr("href"))})}}}]),angular.module("mgcrea.ngStrap.select",["mgcrea.ngStrap.tooltip","mgcrea.ngStrap.helpers.parseOptions"]).provider("$select",function(){var e=this.defaults={animation:"am-fade",prefixClass:"select",prefixEvent:"$select",placement:"bottom-left",template:"select/select.tpl.html",trigger:"focus",container:!1,keyboard:!0,html:!1,delay:0,multiple:!1,allNoneButtons:!1,sort:!0,caretHtml:' ',placeholder:"Choose among the following...",allText:"All",noneText:"None",maxLength:3,maxLengthHtml:"selected",iconCheckmark:"glyphicon glyphicon-ok"};this.$get=["$window","$document","$rootScope","$tooltip","$timeout",function(t,n,a,o,i){function r(t,n,a){var r={},s=angular.extend({},e,a);s.sort=s.sort.toString().match(/true|1/i),r=o(t,s);var u=r.$scope;u.$matches=[],u.$activeIndex=-1,u.$isMultiple=s.multiple,u.$showAllNoneButtons=s.allNoneButtons&&s.multiple,u.$iconCheckmark=s.iconCheckmark,u.$allText=s.allText,u.$noneText=s.noneText,u.$activate=function(e){u.$$postDigest(function(){r.activate(e)})},u.$select=function(e){u.$$postDigest(function(){r.select(e)})},u.$isVisible=function(){return r.$isVisible()},u.$isActive=function(e){return r.$isActive(e)},u.$selectAll=function(){for(var e=0;e=u.$matches.length&&(u.$activeIndex=s.multiple?[]:0)},r.$isVisible=function(){return s.minLength&&n?u.$matches.length&&n.$viewValue.length>=s.minLength:u.$matches.length},r.$isActive=function(e){return s.multiple?-1!==u.$activeIndex.indexOf(e):u.$activeIndex===e},r.$getIndex=function(e){var t=u.$matches.length,n=t;if(t){for(n=t;n--&&u.$matches[n].value!==e;);if(!(0>n))return n}},r.$onMouseDown=function(e){if(e.preventDefault(),e.stopPropagation(),l){var t=angular.element(e.target);t.triggerHandler("click")}},r.$onKeyDown=function(e){return/(9|13|38|40)/.test(e.keyCode)?(e.preventDefault(),e.stopPropagation(),s.multiple&&9===e.keyCode?r.hide():s.multiple||13!==e.keyCode&&9!==e.keyCode?void(s.multiple||(38===e.keyCode&&u.$activeIndex>0?u.$activeIndex--:38===e.keyCode&&u.$activeIndex<0?u.$activeIndex=u.$matches.length-1:40===e.keyCode&&u.$activeIndex'),l.after(t)}var u=o(n.bsOptions),c=a(t,r,s),d=u.$match[7].replace(/\|.+/,"").trim();e.$watch(d,function(){u.valuesFn(e,r).then(function(e){c.update(e),r.$render()})},!0),e.$watch(n.ngModel,function(){c.$updateActiveIndex(),r.$render()},!0),r.$render=function(){var e,n;s.multiple&&angular.isArray(r.$modelValue)?(e=r.$modelValue.map(function(e){return n=c.$getIndex(e),angular.isDefined(n)?c.$scope.$matches[n].label:!1}).filter(angular.isDefined),e=e.length>(s.maxLength||i.maxLength)?e.length+" "+(s.maxLengthHtml||i.maxLengthHtml):e.join(", ")):(n=c.$getIndex(r.$modelValue),e=angular.isDefined(n)?c.$scope.$matches[n].label:!1),t.html((e?e:s.placeholder)+(s.caretHtml?s.caretHtml:i.caretHtml))},s.multiple&&(r.$isEmpty=function(e){return!e||0===e.length}),e.$on("$destroy",function(){c&&c.destroy(),s=null,c=null})}}}]),angular.module("mgcrea.ngStrap.tab",[]).provider("$tab",function(){var e=this.defaults={animation:"am-fade",template:"tab/tab.tpl.html",navClass:"nav-tabs",activeClass:"active"},t=this.controller=function(t,n,a){var o=this;o.$options=angular.copy(e),angular.forEach(["animation","navClass","activeClass"],function(e){angular.isDefined(a[e])&&(o.$options[e]=a[e])}),t.$navClass=o.$options.navClass,t.$activeClass=o.$options.activeClass,o.$panes=t.$panes=[],o.$activePaneChangeListeners=o.$viewChangeListeners=[],o.$push=function(e){o.$panes.push(e)},o.$remove=function(e){var t=o.$panes.indexOf(e),n=o.$panes.$active;o.$panes.splice(t,1),n>t?n--:t===n&&n===o.$panes.length&&n--,o.$setActive(n)},o.$panes.$active=0,o.$setActive=t.$setActive=function(e){o.$panes.$active=e,o.$activePaneChangeListeners.forEach(function(e){e()})}};this.$get=function(){var n={};return n.defaults=e,n.controller=t,n}}).directive("bsTabs",["$window","$animate","$tab","$parse",function(e,t,n,a){var o=n.defaults;return{require:["?ngModel","bsTabs"],transclude:!0,scope:!0,controller:["$scope","$element","$attrs",n.controller],templateUrl:function(e,t){return t.template||o.template},link:function(e,t,n,o){var i=o[0],r=o[1];if(i&&(console.warn("Usage of ngModel is deprecated, please use bsActivePane instead!"),r.$activePaneChangeListeners.push(function(){i.$setViewValue(r.$panes.$active)}),i.$formatters.push(function(e){return r.$setActive(1*e),e})),n.bsActivePane){var s=a(n.bsActivePane);r.$activePaneChangeListeners.push(function(){s.assign(e,r.$panes.$active)}),e.$watch(n.bsActivePane,function(e){r.$setActive(1*e)},!0)}}}}]).directive("bsPane",["$window","$animate","$sce",function(e,t,n){return{require:["^?ngModel","^bsTabs"],scope:!0,link:function(e,a,o,i){function r(){var n=s.$panes.indexOf(e),o=s.$panes.$active;t[n===o?"addClass":"removeClass"](a,s.$options.activeClass)}var s=(i[0],i[1]);a.addClass("tab-pane"),o.$observe("title",function(t){e.title=n.trustAsHtml(t)}),s.$options.animation&&a.addClass(s.$options.animation),o.$observe("disabled",function(t){e.disabled=e.$eval(t)}),s.$push(e),e.$on("$destroy",function(){s.$remove(e)}),s.$activePaneChangeListeners.push(function(){r()}),r()}}}]),angular.module("mgcrea.ngStrap.timepicker",["mgcrea.ngStrap.helpers.dateParser","mgcrea.ngStrap.helpers.dateFormatter","mgcrea.ngStrap.tooltip"]).provider("$timepicker",function(){var e=this.defaults={animation:"am-fade",prefixClass:"timepicker",placement:"bottom-left",template:"timepicker/timepicker.tpl.html",trigger:"focus",container:!1,keyboard:!0,html:!1,delay:0,useNative:!0,timeType:"date",timeFormat:"shortTime",timezone:null,modelTimeFormat:null,autoclose:!1,minTime:-1/0,maxTime:+1/0,length:5,hourStep:1,minuteStep:5,roundDisplay:!1,iconUp:"glyphicon glyphicon-chevron-up",iconDown:"glyphicon glyphicon-chevron-down",arrowBehavior:"pager"};this.$get=["$window","$document","$rootScope","$sce","$dateFormatter","$tooltip","$timeout",function(t,n,a,o,i,r,s){function l(t,n,a){function o(e){var t=6e4*g.minuteStep;return new Date(Math.floor(e.getTime()/t)*t)}function l(e,n){if(t[0].createTextRange){var a=t[0].createTextRange();a.collapse(!0),a.moveStart("character",e),a.moveEnd("character",n),a.select()}else t[0].setSelectionRange?t[0].setSelectionRange(e,n):angular.isUndefined(t[0].selectionStart)&&(t[0].selectionStart=e,t[0].selectionEnd=n)}function d(){t[0].focus()}var f=r(t,angular.extend({},e,a)),p=a.scope,g=f.$options,m=f.$scope,$=g.lang,h=function(e,t,n){return i.formatDate(e,t,$,n)},v=0,y=g.roundDisplay?o(new Date):new Date,w=n.$dateValue||y,b={hour:w.getHours(),meridian:w.getHours()<12,minute:w.getMinutes(),second:w.getSeconds(),millisecond:w.getMilliseconds()},D=i.getDatetimeFormat(g.timeFormat,$),k=i.hoursFormat(D),S=i.timeSeparator(D),x=i.minutesFormat(D),T=i.showAM(D);m.$iconUp=g.iconUp,m.$iconDown=g.iconDown,m.$select=function(e,t){f.select(e,t)},m.$moveIndex=function(e,t){f.$moveIndex(e,t)},m.$switchMeridian=function(e){f.switchMeridian(e)},f.update=function(e){angular.isDate(e)&&!isNaN(e.getTime())?(f.$date=e,angular.extend(b,{hour:e.getHours(),minute:e.getMinutes(),second:e.getSeconds(),millisecond:e.getMilliseconds()}),f.$build()):f.$isBuilt||f.$build()},f.select=function(e,t,a){(!n.$dateValue||isNaN(n.$dateValue.getTime()))&&(n.$dateValue=new Date(1970,0,1)),angular.isDate(e)||(e=new Date(e)),0===t?n.$dateValue.setHours(e.getHours()):1===t&&n.$dateValue.setMinutes(e.getMinutes()),n.$setViewValue(angular.copy(n.$dateValue)),n.$render(),g.autoclose&&!a&&s(function(){f.hide(!0)})},f.switchMeridian=function(e){if(n.$dateValue&&!isNaN(n.$dateValue.getTime())){var t=(e||n.$dateValue).getHours();n.$dateValue.setHours(12>t?t+12:t-12),n.$setViewValue(angular.copy(n.$dateValue)),n.$render()}},f.$build=function(){var e,t,n=m.midIndex=parseInt(g.length/2,10),a=[];for(e=0;e1*g.maxTime},m.$arrowAction=function(e,t){"picker"===g.arrowBehavior?f.$setTimeByStep(e,t):f.$moveIndex(e,t)},f.$setTimeByStep=function(e,t){{var n=new Date(f.$date),a=n.getHours(),o=(h(n,k).length,n.getMinutes());h(n,x).length}0===t?n.setHours(a-parseInt(g.hourStep,10)*e):n.setMinutes(o-parseInt(g.minuteStep,10)*e),f.select(n,t,!0)},f.$moveIndex=function(e,t){var n;0===t?(n=new Date(1970,0,1,b.hour+e*g.length,b.minute),angular.extend(b,{hour:n.getHours()})):1===t&&(n=new Date(1970,0,1,b.hour,b.minute+e*g.length*g.minuteStep),angular.extend(b,{minute:n.getMinutes()})),f.$build()},f.$onMouseDown=function(e){if("input"!==e.target.nodeName.toLowerCase()&&e.preventDefault(),e.stopPropagation(),c){var t=angular.element(e.target);"button"!==t[0].nodeName.toLowerCase()&&(t=t.parent()),t.triggerHandler("click")}},f.$onKeyDown=function(e){if(/(38|37|39|40|13)/.test(e.keyCode)&&!e.shiftKey&&!e.altKey){if(e.preventDefault(),e.stopPropagation(),13===e.keyCode)return f.hide(!0);var t=new Date(f.$date),n=t.getHours(),a=h(t,k).length,o=t.getMinutes(),i=h(t,x).length,r=/(37|39)/.test(e.keyCode),s=2+1*T;r&&(37===e.keyCode?v=1>v?s-1:v-1:39===e.keyCode&&(v=s-1>v?v+1:0));var u=[0,a];0===v?(38===e.keyCode?t.setHours(n-parseInt(g.hourStep,10)):40===e.keyCode&&t.setHours(n+parseInt(g.hourStep,10)),a=h(t,k).length,u=[0,a]):1===v?(38===e.keyCode?t.setMinutes(o-parseInt(g.minuteStep,10)):40===e.keyCode&&t.setMinutes(o+parseInt(g.minuteStep,10)),i=h(t,x).length,u=[a+1,a+1+i]):2===v&&(r||f.switchMeridian(),u=[a+1+i+1,a+1+i+3]),f.select(t,v,!0),l(u[0],u[1]),p.$digest()}};var C=f.init;f.init=function(){return u&&g.useNative?(t.prop("type","time"),void t.css("-webkit-appearance","textfield")):(c&&(t.prop("type","text"),t.attr("readonly","true"),t.on("click",d)),void C())};var M=f.destroy;f.destroy=function(){u&&g.useNative&&t.off("click",d),M()};var E=f.show;f.show=function(){E(),s(function(){f.$element.on(c?"touchstart":"mousedown",f.$onMouseDown),g.keyboard&&t.on("keydown",f.$onKeyDown)},0,!1)};var A=f.hide;return f.hide=function(e){f.$isShown&&(f.$element.off(c?"touchstart":"mousedown",f.$onMouseDown),g.keyboard&&t.off("keydown",f.$onKeyDown),A(e))},f}var u=(angular.element(t.document.body),/(ip(a|o)d|iphone|android)/gi.test(t.navigator.userAgent)),c="createTouch"in t.document&&u;return e.lang||(e.lang=i.getDefaultLocale()),l.defaults=e,l}]}).directive("bsTimepicker",["$window","$parse","$q","$dateFormatter","$dateParser","$timepicker",function(e,t,n,a,o,i){{var r=i.defaults,s=/(ip(a|o)d|iphone|android)/gi.test(e.navigator.userAgent);e.requestAnimationFrame||e.setTimeout}return{restrict:"EAC",require:"ngModel",link:function(e,t,n,l){function u(e){if(angular.isDate(e)){var t=isNaN(d.minTime)||new Date(e.getTime()).setFullYear(1970,0,1)>=d.minTime,n=isNaN(d.maxTime)||new Date(e.getTime()).setFullYear(1970,0,1)<=d.maxTime,a=t&&n;l.$setValidity("date",a),l.$setValidity("min",t),l.$setValidity("max",n),a&&(l.$dateValue=e)}}function c(){return!l.$dateValue||isNaN(l.$dateValue.getTime())?"":m(l.$dateValue,d.timeFormat)}var d={scope:e,controller:l};angular.forEach(["placement","container","delay","trigger","keyboard","html","animation","template","autoclose","timeType","timeFormat","timezone","modelTimeFormat","useNative","hourStep","minuteStep","length","arrowBehavior","iconUp","iconDown","id"],function(e){angular.isDefined(n[e])&&(d[e]=n[e])});var f=/^(false|0|)$/;angular.forEach(["roundDisplay"],function(e){angular.isDefined(n[e])&&(d[e]=!f.test(n[e]))}),n.bsShow&&e.$watch(n.bsShow,function(e){p&&angular.isDefined(e)&&(angular.isString(e)&&(e=!!e.match(/true|,?(timepicker),?/i)),e===!0?p.show():p.hide())}),s&&(d.useNative||r.useNative)&&(d.timeFormat="HH:mm");var p=i(t,l,d);d=p.$options;var g=d.lang,m=function(e,t,n){return a.formatDate(e,t,g,n)},$=o({format:d.timeFormat,lang:g});angular.forEach(["minTime","maxTime"],function(e){angular.isDefined(n[e])&&n.$observe(e,function(t){p.$options[e]=$.getTimeForAttribute(e,t),!isNaN(p.$options[e])&&p.$build(),u(l.$dateValue)})}),e.$watch(n.ngModel,function(){p.update(l.$dateValue)},!0),l.$parsers.unshift(function(e){var t;if(!e)return l.$setValidity("date",!0),null;var n=angular.isDate(e)?e:$.parse(e,l.$dateValue);return!n||isNaN(n.getTime())?void l.$setValidity("date",!1):(u(n),"string"===d.timeType?(t=$.timezoneOffsetAdjust(n,d.timezone,!0),m(t,d.modelTimeFormat||d.timeFormat)):(t=$.timezoneOffsetAdjust(l.$dateValue,d.timezone,!0),"number"===d.timeType?t.getTime():"unix"===d.timeType?t.getTime()/1e3:"iso"===d.timeType?t.toISOString():new Date(t)))}),l.$formatters.push(function(e){var t;return t=angular.isUndefined(e)||null===e?0/0:angular.isDate(e)?e:"string"===d.timeType?$.parse(e,null,d.modelTimeFormat):new Date("unix"===d.timeType?1e3*e:e),l.$dateValue=$.timezoneOffsetAdjust(t,d.timezone),c()}),l.$render=function(){t.val(c())},e.$on("$destroy",function(){p&&p.destroy(),d=null,p=null})}}}]),angular.module("mgcrea.ngStrap.typeahead",["mgcrea.ngStrap.tooltip","mgcrea.ngStrap.helpers.parseOptions"]).provider("$typeahead",function(){var e=this.defaults={animation:"am-fade",prefixClass:"typeahead",prefixEvent:"$typeahead",placement:"bottom-left",template:"typeahead/typeahead.tpl.html",trigger:"focus",container:!1,keyboard:!0,html:!1,delay:0,minLength:1,filter:"filter",limit:6,autoSelect:!1,comparator:""};this.$get=["$window","$rootScope","$tooltip","$timeout",function(t,n,a,o){function i(t,n,i){var r={},s=angular.extend({},e,i);r=a(t,s);var l=i.scope,u=r.$scope;u.$resetMatches=function(){u.$matches=[],u.$activeIndex=s.autoSelect?0:-1},u.$resetMatches(),u.$activate=function(e){u.$$postDigest(function(){r.activate(e)})},u.$select=function(e){u.$$postDigest(function(){r.select(e)})},u.$isVisible=function(){return r.$isVisible()},r.update=function(e){u.$matches=e,u.$activeIndex>=e.length&&(u.$activeIndex=s.autoSelect?0:-1),/^(bottom|bottom-left|bottom-right)$/.test(s.placement)||o(r.$applyPlacement)},r.activate=function(e){u.$activeIndex=e},r.select=function(e){var t=u.$matches[e].value;n.$setViewValue(t),n.$render(),u.$resetMatches(),l&&l.$digest(),u.$emit(s.prefixEvent+".select",t,e,r)},r.$isVisible=function(){return s.minLength&&n?u.$matches.length&&angular.isString(n.$viewValue)&&n.$viewValue.length>=s.minLength:!!u.$matches.length},r.$getIndex=function(e){var t=u.$matches.length,n=t;if(t){for(n=t;n--&&u.$matches[n].value!==e;);if(!(0>n))return n}},r.$onMouseDown=function(e){e.preventDefault(),e.stopPropagation()},r.$onKeyDown=function(e){/(38|40|13)/.test(e.keyCode)&&(r.$isVisible()&&(e.preventDefault(),e.stopPropagation()),13===e.keyCode&&u.$matches.length?r.select(u.$activeIndex):38===e.keyCode&&u.$activeIndex>0?u.$activeIndex--:40===e.keyCode&&u.$activeIndex0)return void s.$setViewValue(s.$viewValue.substring(0,s.$viewValue.length-1));e.length>c&&(e=e.slice(0,c));var n=g.$isVisible();n&&g.update(e),(1!==e.length||e[0].value!==t)&&(!n&&g.update(e),s.$render())})}),s.$formatters.push(function(e){var t=p.displayValue(e);return t===n?"":t}),s.$render=function(){if(s.$isEmpty(s.$viewValue))return t.val("");var e=g.$getIndex(s.$modelValue),n=angular.isDefined(e)?g.$scope.$matches[e].label:s.$viewValue;n=angular.isObject(n)?p.displayValue(n):n,t.val(n?n.toString().replace(/<(?:.|\n)*?>/gm,"").trim():"")},e.$on("$destroy",function(){g&&g.destroy(),l=null,g=null})}}}]),angular.module("mgcrea.ngStrap.tooltip",["mgcrea.ngStrap.helpers.dimensions"]).provider("$tooltip",function(){var e=this.defaults={animation:"am-fade",customClass:"",prefixClass:"tooltip",prefixEvent:"tooltip",container:!1,target:!1,placement:"top",template:"tooltip/tooltip.tpl.html",contentTemplate:!1,trigger:"hover focus",keyboard:!1,html:!1,show:!1,title:"",type:"",delay:0,autoClose:!1,bsEnabled:!0,viewport:{selector:"body",padding:0}};this.$get=["$window","$rootScope","$compile","$q","$templateCache","$http","$animate","$sce","dimensions","$$rAF","$timeout",function(n,a,o,i,r,s,l,u,c,d,f){function p(i,r){function s(){N.$emit(H.prefixEvent+".show",P)}function p(){if(N.$emit(H.prefixEvent+".hide",P),z===B){if(W&&"focus"===H.trigger)return i[0].blur();O()}}function b(){var e=H.trigger.split(" ");angular.forEach(e,function(e){"click"===e?i.on("click",P.toggle):"manual"!==e&&(i.on("hover"===e?"mouseenter":"focus",P.enter),i.on("hover"===e?"mouseleave":"blur",P.leave),"button"===I&&"hover"!==e&&i.on(v?"touchstart":"mousedown",P.$onFocusElementMouseDown)) +})}function D(){for(var e=H.trigger.split(" "),t=e.length;t--;){var n=e[t];"click"===n?i.off("click",P.toggle):"manual"!==n&&(i.off("hover"===n?"mouseenter":"focus",P.enter),i.off("hover"===n?"mouseleave":"blur",P.leave),"button"===I&&"hover"!==n&&i.off(v?"touchstart":"mousedown",P.$onFocusElementMouseDown))}}function k(){"focus"!==H.trigger?z.on("keyup",P.$onKeyUp):i.on("keyup",P.$onFocusKeyUp)}function S(){"focus"!==H.trigger?z.off("keyup",P.$onKeyUp):i.off("keyup",P.$onFocusKeyUp)}function x(){f(function(){z.on("click",C),w.on("click",P.hide),_=!0},0,!1)}function T(){_&&(z.off("click",C),w.off("click",P.hide),_=!1)}function C(e){e.stopPropagation()}function M(e){e=e||H.target||i;var a=e[0],o="BODY"===a.tagName,r=a.getBoundingClientRect(),s={};for(var l in r)s[l]=r[l];null===s.width&&(s=angular.extend({},s,{width:r.right-r.left,height:r.bottom-r.top}));var u=o?{top:0,left:0}:c.offset(a),d={scroll:o?t.documentElement.scrollTop||t.body.scrollTop:e.prop("scrollTop")||0},f=o?{width:t.documentElement.clientWidth,height:n.innerHeight}:null;return angular.extend({},s,d,f,u)}function E(e,t,n,a){var o,i=e.split("-");switch(i[0]){case"right":o={top:t.top+t.height/2-a/2,left:t.left+t.width};break;case"bottom":o={top:t.top+t.height,left:t.left+t.width/2-n/2};break;case"left":o={top:t.top+t.height/2-a/2,left:t.left-n};break;default:o={top:t.top-a,left:t.left+t.width/2-n/2}}if(!i[1])return o;if("top"===i[0]||"bottom"===i[0])switch(i[1]){case"left":o.left=t.left;break;case"right":o.left=t.left+t.width-n}else if("left"===i[0]||"right"===i[0])switch(i[1]){case"top":o.top=t.top-a;break;case"bottom":o.top=t.top+t.height}return o}function A(e,t){var n=z[0],a=n.offsetWidth,o=n.offsetHeight,i=parseInt(c.css(n,"margin-top"),10),r=parseInt(c.css(n,"margin-left"),10);isNaN(i)&&(i=0),isNaN(r)&&(r=0),e.top=e.top+i,e.left=e.left+r,c.setOffset(n,angular.extend({using:function(e){z.css({top:Math.round(e.top)+"px",left:Math.round(e.left)+"px"})}},e),0);var s=n.offsetWidth,l=n.offsetHeight;if("top"===t&&l!==o&&(e.top=e.top+o-l),!/top-left|top-right|bottom-left|bottom-right/.test(t)){var u=F(t,e,s,l);if(u.left?e.left+=u.left:e.top+=u.top,c.setOffset(n,e),/top|right|bottom|left/.test(t)){var d=/top|bottom/.test(t),f=d?2*u.left-a+s:2*u.top-o+l,p=d?"offsetWidth":"offsetHeight";V(f,n[p],d)}}}function F(e,t,n,a){var o={top:0,left:0},i=H.viewport&&m(H.viewport.selector||H.viewport);if(!i)return o;var r=H.viewport&&H.viewport.padding||0,s=M(i);if(/right|left/.test(e)){var l=t.top-r-s.scroll,u=t.top+r-s.scroll+a;ls.top+s.height&&(o.top=s.top+s.height-u)}else{var c=t.left-r,d=t.left+r+n;cs.width&&(o.left=s.left+s.width-d)}return o}function V(e,t,n){var a=m(".tooltip-arrow, .arrow",z[0]);a.css(n?"left":"top",50*(1-e/t)+"%").css(n?"top":"left","")}function O(){clearTimeout(R),P.$isShown&&null!==z&&(H.autoClose&&T(),H.keyboard&&S()),j&&(j.$destroy(),j=null),z&&(z.remove(),z=P.$element=null)}var P={},I=i[0].nodeName.toLowerCase(),H=P.$options=angular.extend({},e,r);P.$promise=$(H.template);var N=P.$scope=H.scope&&H.scope.$new()||a.$new();if(H.delay&&angular.isString(H.delay)){var L=H.delay.split(",").map(parseFloat);H.delay=L.length>1?{show:L[0],hide:L[1]}:L[0]}P.$id=H.id||i.attr("id")||"",H.title&&(N.title=u.trustAsHtml(H.title)),N.$setEnabled=function(e){N.$$postDigest(function(){P.setEnabled(e)})},N.$hide=function(){N.$$postDigest(function(){P.hide()})},N.$show=function(){N.$$postDigest(function(){P.show()})},N.$toggle=function(){N.$$postDigest(function(){P.toggle()})},P.$isShown=N.$isShown=!1;var R,Y;H.contentTemplate&&(P.$promise=P.$promise.then(function(e){var t=angular.element(e);return $(H.contentTemplate).then(function(e){var n=m('[ng-bind="content"]',t[0]);return n.length||(n=m('[ng-bind="title"]',t[0])),n.removeAttr("ng-bind").html(e),t[0].outerHTML})}));var q,z,K,U,j;P.$promise.then(function(e){angular.isObject(e)&&(e=e.data),H.html&&(e=e.replace(y,'ng-bind-html="')),e=h.apply(e),K=e,q=o(e),P.init()}),P.init=function(){H.delay&&angular.isNumber(H.delay)&&(H.delay={show:H.delay,hide:H.delay}),"self"===H.container?U=i:angular.isElement(H.container)?U=H.container:H.container&&(U=m(H.container)),b(),H.target&&(H.target=angular.isElement(H.target)?H.target:m(H.target)),H.show&&N.$$postDigest(function(){"focus"===H.trigger?i[0].focus():P.show()})},P.destroy=function(){D(),O(),N.$destroy()},P.enter=function(){return clearTimeout(R),Y="in",H.delay&&H.delay.show?void(R=setTimeout(function(){"in"===Y&&P.show()},H.delay.show)):P.show()},P.show=function(){if(H.bsEnabled&&!P.$isShown){N.$emit(H.prefixEvent+".show.before",P);var e,t;H.container?(e=U,t=U[0].lastChild?angular.element(U[0].lastChild):null):(e=null,t=i),z&&O(),j=P.$scope.$new(),z=P.$element=q(j,function(){}),z.css({top:"-9999px",left:"-9999px",display:"block",visibility:"hidden"}),H.animation&&z.addClass(H.animation),H.type&&z.addClass(H.prefixClass+"-"+H.type),H.customClass&&z.addClass(H.customClass),t?t.after(z):e.prepend(z),P.$isShown=N.$isShown=!0,g(N),P.$applyPlacement();var n=l.enter(z,e,t,s);n&&n.then&&n.then(s),g(N),d(function(){z&&z.css({visibility:"visible"})}),H.keyboard&&("focus"!==H.trigger&&P.focus(),k()),H.autoClose&&x()}},P.leave=function(){return clearTimeout(R),Y="out",H.delay&&H.delay.hide?void(R=setTimeout(function(){"out"===Y&&P.hide()},H.delay.hide)):P.hide()};var W,B;P.hide=function(e){if(P.$isShown){N.$emit(H.prefixEvent+".hide.before",P),W=e,B=z;var t=l.leave(z,p);t&&t.then&&t.then(p),P.$isShown=N.$isShown=!1,g(N),H.keyboard&&null!==z&&S(),H.autoClose&&null!==z&&T()}},P.toggle=function(){P.$isShown?P.leave():P.enter()},P.focus=function(){z[0].focus()},P.setEnabled=function(e){H.bsEnabled=e},P.setViewport=function(e){H.viewport=e},P.$applyPlacement=function(){if(z){var t=H.placement,n=/\s?auto?\s?/i,a=n.test(t);a&&(t=t.replace(n,"")||e.placement),z.addClass(H.placement);var o=M(),r=z.prop("offsetWidth"),s=z.prop("offsetHeight");if(a){var l=t,u=H.container?m(H.container):i.parent(),c=M(u);l.indexOf("bottom")>=0&&o.bottom+s>c.bottom?t=l.replace("bottom","top"):l.indexOf("top")>=0&&o.top-sc.width?t="right"===l?"left":t.replace("left","right"):("left"===l||"bottom-right"===l||"top-right"===l)&&o.left-r  
    ')}]),angular.module("mgcrea.ngStrap.aside").run(["$templateCache",function(a){a.put("aside/aside.tpl.html",'')}]),angular.module("mgcrea.ngStrap.datepicker").run(["$templateCache",function(a){a.put("datepicker/datepicker.tpl.html",'')}]),angular.module("mgcrea.ngStrap.dropdown").run(["$templateCache",function(a){a.put("dropdown/dropdown.tpl.html",'')}]),angular.module("mgcrea.ngStrap.modal").run(["$templateCache",function(a){a.put("modal/modal.tpl.html",'')}]),angular.module("mgcrea.ngStrap.popover").run(["$templateCache",function(a){a.put("popover/popover.tpl.html",'

    ')}]),angular.module("mgcrea.ngStrap.select").run(["$templateCache",function(a){a.put("select/select.tpl.html",'')}]),angular.module("mgcrea.ngStrap.tab").run(["$templateCache",function(a){a.put("tab/tab.tpl.html",'
    ')}]),angular.module("mgcrea.ngStrap.timepicker").run(["$templateCache",function(a){a.put("timepicker/timepicker.tpl.html",'')}]),angular.module("mgcrea.ngStrap.tooltip").run(["$templateCache",function(a){a.put("tooltip/tooltip.tpl.html",'
    ')}]),angular.module("mgcrea.ngStrap.typeahead").run(["$templateCache",function(a){a.put("typeahead/typeahead.tpl.html",'')}])}(window,document); -//# sourceMappingURL=angular-strap.tpl.min.map \ No newline at end of file +!function(){"use strict";angular.module("mgcrea.ngStrap.aside").run(["$templateCache",function(t){t.put("aside/aside.tpl.html",'')}]),angular.module("mgcrea.ngStrap.alert").run(["$templateCache",function(t){t.put("alert/alert.tpl.html",'
     
    ')}]),angular.module("mgcrea.ngStrap.datepicker").run(["$templateCache",function(t){t.put("datepicker/datepicker.tpl.html",'')}]),angular.module("mgcrea.ngStrap.dropdown").run(["$templateCache",function(t){t.put("dropdown/dropdown.tpl.html",'')}]),angular.module("mgcrea.ngStrap.modal").run(["$templateCache",function(t){t.put("modal/modal.tpl.html",'')}]),angular.module("mgcrea.ngStrap.popover").run(["$templateCache",function(t){t.put("popover/popover.tpl.html",'

    ')}]),angular.module("mgcrea.ngStrap.select").run(["$templateCache",function(t){t.put("select/select.tpl.html",'')}]),angular.module("mgcrea.ngStrap.tab").run(["$templateCache",function(t){t.put("tab/tab.tpl.html",'
    ')}]),angular.module("mgcrea.ngStrap.timepicker").run(["$templateCache",function(t){t.put("timepicker/timepicker.tpl.html",'')}]),angular.module("mgcrea.ngStrap.typeahead").run(["$templateCache",function(t){t.put("typeahead/typeahead.tpl.html",'')}]),angular.module("mgcrea.ngStrap.tooltip").run(["$templateCache",function(t){t.put("tooltip/tooltip.tpl.html",'
    ')}])}(window,document); \ No newline at end of file From ee840c730c1e20fcf54c5fceeaf9158d94e1cd87 Mon Sep 17 00:00:00 2001 From: Jimmy Zelinskie Date: Mon, 23 Mar 2015 20:24:08 -0400 Subject: [PATCH 13/96] status badges updated to use shields.io standard --- buildstatus/building.svg | 2 +- buildstatus/failed.svg | 2 +- buildstatus/none.svg | 2 +- buildstatus/ready.svg | 2 +- 4 files changed, 4 insertions(+), 4 deletions(-) diff --git a/buildstatus/building.svg b/buildstatus/building.svg index dc7aeae7b..8e26edf87 100644 --- a/buildstatus/building.svg +++ b/buildstatus/building.svg @@ -1 +1 @@ -Docker ImageDocker Imagebuildingbuilding \ No newline at end of file +containercontainerbuildingbuilding \ No newline at end of file diff --git a/buildstatus/failed.svg b/buildstatus/failed.svg index 069d9f4e4..cc74c2381 100644 --- a/buildstatus/failed.svg +++ b/buildstatus/failed.svg @@ -1 +1 @@ -Docker ImageDocker Imagebuild failedbuild failed \ No newline at end of file +containercontainerfailedfailed \ No newline at end of file diff --git a/buildstatus/none.svg b/buildstatus/none.svg index 3c31d29b1..0e4680acf 100644 --- a/buildstatus/none.svg +++ b/buildstatus/none.svg @@ -1 +1 @@ -Docker ImageDocker Imagenonenone \ No newline at end of file +containercontainernonenone \ No newline at end of file diff --git a/buildstatus/ready.svg b/buildstatus/ready.svg index 111262e3b..50e451a01 100644 --- a/buildstatus/ready.svg +++ b/buildstatus/ready.svg @@ -1 +1 @@ -Docker ImageDocker Imagereadyready \ No newline at end of file +containercontainerreadyready \ No newline at end of file From 2baa7fa14c7c15cc929b39979606b936fc19be0f Mon Sep 17 00:00:00 2001 From: Joseph Schorr Date: Tue, 24 Mar 2015 16:43:44 -0400 Subject: [PATCH 14/96] Fix bug in the view array service --- static/js/services/angular-view-array.js | 4 +++- 1 file changed, 3 insertions(+), 1 deletion(-) diff --git a/static/js/services/angular-view-array.js b/static/js/services/angular-view-array.js index 698ba2f61..2e0e6594a 100644 --- a/static/js/services/angular-view-array.js +++ b/static/js/services/angular-view-array.js @@ -29,7 +29,7 @@ angular.module('quay').factory('AngularViewArray', ['$interval', function($inter this.hasEntries = true; if (this.isVisible) { - this.setVisible(true); + this.startTimer_(); } }; @@ -64,6 +64,8 @@ angular.module('quay').factory('AngularViewArray', ['$interval', function($inter }; _ViewArray.prototype.startTimer_ = function() { + if (this.timerRef_) { return; } + var that = this; this.timerRef_ = $interval(function() { that.showAdditionalEntries_(); From 201943ed1c1339c67ca4cc905719e88cbaee4feb Mon Sep 17 00:00:00 2001 From: Jake Moshenko Date: Fri, 20 Mar 2015 23:21:20 -0400 Subject: [PATCH 15/96] Fix deadlocks with tags and garbage collection. --- data/database.py | 7 ++- data/model/legacy.py | 134 +++++++++++++++++++++---------------------- initdb.py | 2 +- test/fulldbtest.sh | 4 +- 4 files changed, 72 insertions(+), 75 deletions(-) diff --git a/data/database.py b/data/database.py index 2ddd2fada..e12c1e147 100644 --- a/data/database.py +++ b/data/database.py @@ -139,7 +139,7 @@ def uuid_generator(): return str(uuid.uuid4()) -_get_epoch_timestamp = lambda: int(time.time()) +get_epoch_timestamp = lambda: int(time.time()) def close_db_filter(_): @@ -483,7 +483,7 @@ class RepositoryTag(BaseModel): name = CharField() image = ForeignKeyField(Image) repository = ForeignKeyField(Repository) - lifetime_start_ts = IntegerField(default=_get_epoch_timestamp) + lifetime_start_ts = IntegerField(default=get_epoch_timestamp) lifetime_end_ts = IntegerField(null=True, index=True) hidden = BooleanField(default=False) @@ -492,6 +492,9 @@ class RepositoryTag(BaseModel): read_slaves = (read_slave,) indexes = ( (('repository', 'name'), False), + + # This unique index prevents deadlocks when concurrently moving and deleting tags + (('repository', 'name', 'lifetime_end_ts'), True), ) diff --git a/data/model/legacy.py b/data/model/legacy.py index 2d076c5cc..aa968408c 100644 --- a/data/model/legacy.py +++ b/data/model/legacy.py @@ -18,7 +18,7 @@ from data.database import (User, Repository, Image, AccessToken, Role, Repositor DerivedImageStorage, ImageStorageTransformation, random_string_generator, db, BUILD_PHASE, QuayUserField, ImageStorageSignature, QueueItem, ImageStorageSignatureKind, validate_database_url, db_for_update, - AccessTokenKind, Star) + AccessTokenKind, Star, get_epoch_timestamp) from peewee import JOIN_LEFT_OUTER, fn from util.validation import (validate_username, validate_email, validate_password, INVALID_PASSWORD_MESSAGE) @@ -1577,9 +1577,11 @@ def get_repository_images(namespace_name, repository_name): return _get_repository_images_base(namespace_name, repository_name, lambda q: q) -def _tag_alive(query): +def _tag_alive(query, now_ts=None): + if now_ts is None: + now_ts = get_epoch_timestamp() return query.where((RepositoryTag.lifetime_end_ts >> None) | - (RepositoryTag.lifetime_end_ts > int(time.time()))) + (RepositoryTag.lifetime_end_ts > now_ts)) def list_repository_tags(namespace_name, repository_name, include_hidden=False, @@ -1610,14 +1612,19 @@ def list_repository_tags(namespace_name, repository_name, include_hidden=False, def _garbage_collect_tags(namespace_name, repository_name): # We do this without using a join to prevent holding read locks on the repository table repo = _get_repository(namespace_name, repository_name) - now = int(time.time()) + expired_time = get_epoch_timestamp() - repo.namespace_user.removed_tag_expiration_s - (RepositoryTag - .delete() - .where(RepositoryTag.repository == repo, - ~(RepositoryTag.lifetime_end_ts >> None), - (RepositoryTag.lifetime_end_ts + repo.namespace_user.removed_tag_expiration_s) <= now) - .execute()) + tags_to_delete = list(RepositoryTag + .select(RepositoryTag.id) + .where(RepositoryTag.repository == repo, + ~(RepositoryTag.lifetime_end_ts >> None), + (RepositoryTag.lifetime_end_ts <= expired_time)) + .order_by(RepositoryTag.id)) + if len(tags_to_delete) > 0: + (RepositoryTag + .delete() + .where(RepositoryTag.id << tags_to_delete) + .execute()) def garbage_collect_repository(namespace_name, repository_name): @@ -1713,46 +1720,39 @@ def _garbage_collect_storage(storage_id_whitelist): logger.debug('Garbage collecting storages from candidates: %s', storage_id_whitelist) with config.app_config['DB_TRANSACTION_FACTORY'](db): # Track all of the data that should be removed from blob storage - placements_to_remove = orphaned_storage_query(ImageStoragePlacement - .select(ImageStoragePlacement, - ImageStorage, - ImageStorageLocation) - .join(ImageStorageLocation) - .switch(ImageStoragePlacement) - .join(ImageStorage), - storage_id_whitelist, - (ImageStorage, ImageStoragePlacement, - ImageStorageLocation)) + placements_to_remove = list(orphaned_storage_query(ImageStoragePlacement + .select(ImageStoragePlacement, + ImageStorage, + ImageStorageLocation) + .join(ImageStorageLocation) + .switch(ImageStoragePlacement) + .join(ImageStorage), + storage_id_whitelist, + (ImageStorage, ImageStoragePlacement, + ImageStorageLocation))) - paths_to_remove = placements_query_to_paths_set(placements_to_remove.clone()) + paths_to_remove = placements_query_to_paths_set(placements_to_remove) # Remove the placements for orphaned storages - placements_subquery = (placements_to_remove - .clone() - .select(ImageStoragePlacement.id) - .alias('ps')) - inner = (ImageStoragePlacement - .select(placements_subquery.c.id) - .from_(placements_subquery)) - placements_removed = (ImageStoragePlacement - .delete() - .where(ImageStoragePlacement.id << inner) - .execute()) - logger.debug('Removed %s image storage placements', placements_removed) + if len(placements_to_remove) > 0: + placement_ids_to_remove = [placement.id for placement in placements_to_remove] + placements_removed = (ImageStoragePlacement + .delete() + .where(ImageStoragePlacement.id << placement_ids_to_remove) + .execute()) + logger.debug('Removed %s image storage placements', placements_removed) # Remove all orphaned storages # The comma after ImageStorage.id is VERY important, it makes it a tuple, which is a sequence - orphaned_storages = orphaned_storage_query(ImageStorage.select(ImageStorage.id), - storage_id_whitelist, - (ImageStorage.id,)).alias('osq') - orphaned_storage_inner = (ImageStorage - .select(orphaned_storages.c.id) - .from_(orphaned_storages)) - storages_removed = (ImageStorage - .delete() - .where(ImageStorage.id << orphaned_storage_inner) - .execute()) - logger.debug('Removed %s image storage records', storages_removed) + orphaned_storages = list(orphaned_storage_query(ImageStorage.select(ImageStorage.id), + storage_id_whitelist, + (ImageStorage.id,)).alias('osq')) + if len(orphaned_storages) > 0: + storages_removed = (ImageStorage + .delete() + .where(ImageStorage.id << orphaned_storages) + .execute()) + logger.debug('Removed %s image storage records', storages_removed) # We are going to make the conscious decision to not delete image storage blobs inside # transactions. @@ -1803,40 +1803,34 @@ def get_parent_images(namespace_name, repository_name, image_obj): def create_or_update_tag(namespace_name, repository_name, tag_name, tag_docker_image_id): + try: + repo = _get_repository(namespace_name, repository_name) + except Repository.DoesNotExist: + raise DataModelException('Invalid repository %s/%s' % (namespace_name, repository_name)) + + now_ts = get_epoch_timestamp() with config.app_config['DB_TRANSACTION_FACTORY'](db): try: - repo = _get_repository(namespace_name, repository_name) - except Repository.DoesNotExist: - raise DataModelException('Invalid repository %s/%s' % (namespace_name, repository_name)) + tag = db_for_update(_tag_alive(RepositoryTag + .select() + .where(RepositoryTag.repository == repo, + RepositoryTag.name == tag_name), now_ts)).get() + tag.lifetime_end_ts = now_ts + tag.save() + except RepositoryTag.DoesNotExist: + pass try: image = Image.get(Image.docker_image_id == tag_docker_image_id, Image.repository == repo) except Image.DoesNotExist: raise DataModelException('Invalid image with id: %s' % tag_docker_image_id) - now_ts = int(time.time()) - - created = RepositoryTag.create(repository=repo, image=image, name=tag_name, - lifetime_start_ts=now_ts) - - try: - # When we move a tag, we really end the timeline of the old one and create a new one - query = _tag_alive(RepositoryTag - .select() - .where(RepositoryTag.repository == repo, RepositoryTag.name == tag_name, - RepositoryTag.id != created.id)) - tag = query.get() - tag.lifetime_end_ts = now_ts - tag.save() - except RepositoryTag.DoesNotExist: - # No tag that needs to be ended - pass - - return created - + return RepositoryTag.create(repository=repo, image=image, name=tag_name, + lifetime_start_ts=now_ts) def delete_tag(namespace_name, repository_name, tag_name): + now_ts = get_epoch_timestamp() with config.app_config['DB_TRANSACTION_FACTORY'](db): try: query = _tag_alive(RepositoryTag @@ -1845,21 +1839,21 @@ def delete_tag(namespace_name, repository_name, tag_name): .join(Namespace, on=(Repository.namespace_user == Namespace.id)) .where(Repository.name == repository_name, Namespace.username == namespace_name, - RepositoryTag.name == tag_name)) + RepositoryTag.name == tag_name), now_ts) found = db_for_update(query).get() except RepositoryTag.DoesNotExist: msg = ('Invalid repository tag \'%s\' on repository \'%s/%s\'' % (tag_name, namespace_name, repository_name)) raise DataModelException(msg) - found.lifetime_end_ts = int(time.time()) + found.lifetime_end_ts = now_ts found.save() def create_temporary_hidden_tag(repo, image, expiration_s): """ Create a tag with a defined timeline, that will not appear in the UI or CLI. Returns the name of the temporary tag. """ - now_ts = int(time.time()) + now_ts = get_epoch_timestamp() expire_ts = now_ts + expiration_s tag_name = str(uuid4()) RepositoryTag.create(repository=repo, image=image, name=tag_name, lifetime_start_ts=now_ts, diff --git a/initdb.py b/initdb.py index f35b77111..104e0fc19 100644 --- a/initdb.py +++ b/initdb.py @@ -103,7 +103,7 @@ def __create_subtree(repo, structure, creator_username, parent): new_image.docker_image_id) if tag_name[0] == '#': - tag.lifetime_end_ts = int(time.time()) - 1 + tag.lifetime_end_ts = get_epoch_timestamp() - 1 tag.save() for subtree in subtrees: diff --git a/test/fulldbtest.sh b/test/fulldbtest.sh index d3fa0caa7..975f3ddeb 100755 --- a/test/fulldbtest.sh +++ b/test/fulldbtest.sh @@ -14,7 +14,7 @@ up_mysql() { down_mysql() { docker kill mysql - docker rm mysql + docker rm -v mysql } up_postgres() { @@ -31,7 +31,7 @@ up_postgres() { down_postgres() { docker kill postgres - docker rm postgres + docker rm -v postgres } run_tests() { From 3d44416016326565f4323fdaaa76ffa12a20d7d6 Mon Sep 17 00:00:00 2001 From: Jake Moshenko Date: Mon, 23 Mar 2015 11:40:21 -0400 Subject: [PATCH 16/96] Add the migration for the unique index which helps prevent tag deadlocks. --- data/migrations/migration.sh | 8 +++--- ...dd_a_unique_index_to_prevent_deadlocks_.py | 26 +++++++++++++++++++ 2 files changed, 30 insertions(+), 4 deletions(-) create mode 100644 data/migrations/versions/2b4dc0818a5e_add_a_unique_index_to_prevent_deadlocks_.py diff --git a/data/migrations/migration.sh b/data/migrations/migration.sh index 17901e130..daae7e1f2 100755 --- a/data/migrations/migration.sh +++ b/data/migrations/migration.sh @@ -19,7 +19,7 @@ up_mysql() { down_mysql() { docker kill mysql - docker rm mysql + docker rm -v mysql } up_mariadb() { @@ -36,7 +36,7 @@ up_mariadb() { down_mariadb() { docker kill mariadb - docker rm mariadb + docker rm -v mariadb } up_percona() { @@ -53,7 +53,7 @@ up_percona() { down_percona() { docker kill percona - docker rm percona + docker rm -v percona } up_postgres() { @@ -70,7 +70,7 @@ up_postgres() { down_postgres() { docker kill postgres - docker rm postgres + docker rm -v postgres } gen_migrate() { diff --git a/data/migrations/versions/2b4dc0818a5e_add_a_unique_index_to_prevent_deadlocks_.py b/data/migrations/versions/2b4dc0818a5e_add_a_unique_index_to_prevent_deadlocks_.py new file mode 100644 index 000000000..8efe0c123 --- /dev/null +++ b/data/migrations/versions/2b4dc0818a5e_add_a_unique_index_to_prevent_deadlocks_.py @@ -0,0 +1,26 @@ +"""Add a unique index to prevent deadlocks with tags. + +Revision ID: 2b4dc0818a5e +Revises: 2b2529fd23ff +Create Date: 2015-03-20 23:37:10.558179 + +""" + +# revision identifiers, used by Alembic. +revision = '2b4dc0818a5e' +down_revision = '2b2529fd23ff' + +from alembic import op +import sqlalchemy as sa + + +def upgrade(tables): + ### commands auto generated by Alembic - please adjust! ### + op.create_index('repositorytag_repository_id_name_lifetime_end_ts', 'repositorytag', ['repository_id', 'name', 'lifetime_end_ts'], unique=True) + ### end Alembic commands ### + + +def downgrade(tables): + ### commands auto generated by Alembic - please adjust! ### + op.drop_index('repositorytag_repository_id_name_lifetime_end_ts', table_name='repositorytag') + ### end Alembic commands ### From 0d98776d5483d52c1c0bf36e643a19675c2e6ee1 Mon Sep 17 00:00:00 2001 From: Joseph Schorr Date: Tue, 24 Mar 2015 19:28:24 -0400 Subject: [PATCH 17/96] Fix NPE and other small issues with the new image view --- external_libraries.py | 2 +- static/js/graphing.js | 7 +++++-- static/js/pages/image-view.js | 2 +- static/partials/image-view.html | 13 +++++++++++-- 4 files changed, 18 insertions(+), 6 deletions(-) diff --git a/external_libraries.py b/external_libraries.py index 113295353..7004a2cca 100644 --- a/external_libraries.py +++ b/external_libraries.py @@ -21,7 +21,7 @@ EXTERNAL_CSS = [ 'netdna.bootstrapcdn.com/font-awesome/4.2.0/css/font-awesome.css', 'netdna.bootstrapcdn.com/bootstrap/3.3.2/css/bootstrap.min.css', 'fonts.googleapis.com/css?family=Source+Sans+Pro:400,700', - 'cdn.core-os.net/icons/core-icons.css' + 's3.amazonaws.com/cdn.core-os.net/icons/core-icons.css' ] EXTERNAL_FONTS = [ diff --git a/static/js/graphing.js b/static/js/graphing.js index 7ccfcf2df..5a5ee2018 100644 --- a/static/js/graphing.js +++ b/static/js/graphing.js @@ -1157,8 +1157,11 @@ FileTreeBase.prototype.populateAndDraw_ = function() { } this.root_ = this.nodeMap_['']; - this.root_.x0 = 0; - this.root_.y0 = 0; + if (this.root_) { + this.root_.x0 = 0; + this.root_.y0 = 0; + } + this.toggle_(this.root_); this.update_(this.root_); }; diff --git a/static/js/pages/image-view.js b/static/js/pages/image-view.js index 945fdc66e..347854d3d 100644 --- a/static/js/pages/image-view.js +++ b/static/js/pages/image-view.js @@ -73,7 +73,7 @@ }; $scope.initializeTree = function() { - if ($scope.tree) { return; } + if ($scope.tree || !$scope.combinedChanges.length) { return; } $scope.tree = new ImageFileChangeTree($scope.image, $scope.combinedChanges); $timeout(function() { diff --git a/static/partials/image-view.html b/static/partials/image-view.html index 421c685dd..615361ee3 100644 --- a/static/partials/image-view.html +++ b/static/partials/image-view.html @@ -40,8 +40,17 @@

    Image File Changes

    -
    +
    +
    No file changes
    +
    + There were no file system changes in this image layer. +
    +
    + +
    +
    +
    From a7a8571396d9d26ebedc5437ca29677a87060830 Mon Sep 17 00:00:00 2001 From: Joseph Schorr Date: Tue, 24 Mar 2015 19:48:43 -0400 Subject: [PATCH 18/96] Fix width of the image history tree --- static/js/graphing.js | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/static/js/graphing.js b/static/js/graphing.js index 5a5ee2018..7ea5203ec 100644 --- a/static/js/graphing.js +++ b/static/js/graphing.js @@ -95,7 +95,7 @@ function ImageHistoryTree(namespace, name, images, formatComment, formatTime, fo * Calculates the dimensions of the tree. */ ImageHistoryTree.prototype.calculateDimensions_ = function(container) { - var cw = Math.max(document.getElementById(container).clientWidth, this.maxWidth_ * DEPTH_WIDTH); + var cw = document.getElementById(container).clientWidth; var ch = this.maxHeight_ * (DEPTH_HEIGHT + 10); var margin = { top: 40, right: 20, bottom: 20, left: 80 }; From 2459b6b46728d148e8859cf46b2796bce155355f Mon Sep 17 00:00:00 2001 From: Joseph Schorr Date: Wed, 25 Mar 2015 15:31:05 -0400 Subject: [PATCH 19/96] Start on new org view --- endpoints/api/organization.py | 30 +-- static/css/pages/org-view.css | 9 + static/directives/repo-list-grid.html | 20 +- .../directives/repository-events-table.html | 2 +- static/directives/teams-manager.html | 30 +++ static/js/directives/ui/repo-list-grid.js | 4 +- static/js/directives/ui/teams-manager.js | 82 ++++++++ static/js/pages/org-admin.js | 2 +- static/js/pages/org-view.js | 88 +++++++- static/partials/old-org-view.html | 85 ++++++++ static/partials/org-view.html | 193 +++++++++++------- 11 files changed, 442 insertions(+), 103 deletions(-) create mode 100644 static/css/pages/org-view.css create mode 100644 static/directives/teams-manager.html create mode 100644 static/js/directives/ui/teams-manager.js create mode 100644 static/partials/old-org-view.html diff --git a/endpoints/api/organization.py b/endpoints/api/organization.py index 4302bd62f..115644789 100644 --- a/endpoints/api/organization.py +++ b/endpoints/api/organization.py @@ -24,16 +24,20 @@ logger = logging.getLogger(__name__) def org_view(o, teams): - admin_org = AdministerOrganizationPermission(o.username) - is_admin = admin_org.can() + is_admin = AdministerOrganizationPermission(o.username).can() + is_member = OrganizationMemberPermission(o.username).can() + view = { 'name': o.username, 'email': o.email if is_admin else '', 'avatar': avatar.compute_hash(o.email, name=o.username), - 'teams': {t.name : team_view(o.username, t) for t in teams}, - 'is_admin': is_admin + 'is_admin': is_admin, + 'is_member': is_member } + if teams is not None: + view['teams'] = {t.name : team_view(o.username, t) for t in teams} + if is_admin: view['invoice_email'] = o.invoice_email @@ -129,17 +133,17 @@ class Organization(ApiResource): @nickname('getOrganization') def get(self, orgname): """ Get the details for the specified organization """ - permission = OrganizationMemberPermission(orgname) - if permission.can(): - try: - org = model.get_organization(orgname) - except model.InvalidOrganizationException: - raise NotFound() + try: + org = model.get_organization(orgname) + except model.InvalidOrganizationException: + raise NotFound() + teams = None + if OrganizationMemberPermission(orgname).can(): teams = model.get_teams_within_org(org) - return org_view(org, teams) - raise Unauthorized() + return org_view(org, teams) + @require_scope(scopes.ORG_ADMIN) @nickname('changeOrganizationDetails') @@ -218,7 +222,7 @@ class OrgPrivateRepositories(ApiResource): @path_param('orgname', 'The name of the organization') class OrgnaizationMemberList(ApiResource): """ Resource for listing the members of an organization. """ - + @require_scope(scopes.ORG_ADMIN) @nickname('getOrganizationMembers') def get(self, orgname): diff --git a/static/css/pages/org-view.css b/static/css/pages/org-view.css new file mode 100644 index 000000000..f922343c1 --- /dev/null +++ b/static/css/pages/org-view.css @@ -0,0 +1,9 @@ +.org-view .organization-name { + vertical-align: middle; + margin-left: 6px; +} + +.org-view h3 { + margin-bottom: 20px; + margin-top: 0px; +} \ No newline at end of file diff --git a/static/directives/repo-list-grid.html b/static/directives/repo-list-grid.html index edad56285..88a58ad54 100644 --- a/static/directives/repo-list-grid.html +++ b/static/directives/repo-list-grid.html @@ -2,15 +2,17 @@
    -
    - - Starred -
    -
    - - {{ namespace.name }} - {{ namespace.name }} +
    +
    + + Starred +
    +
    + + {{ namespace.name }} + {{ namespace.name }} +
    diff --git a/static/directives/repository-events-table.html b/static/directives/repository-events-table.html index 26eb6e951..5c3f7b87f 100644 --- a/static/directives/repository-events-table.html +++ b/static/directives/repository-events-table.html @@ -14,7 +14,7 @@ error-message="'Could not load repository events'">
    -
    No notification have been setup for this repository.
    +
    No notifications have been setup for this repository.
    Click the "Create Notification" button above to add a new notification for a repository event.
    diff --git a/static/directives/teams-manager.html b/static/directives/teams-manager.html new file mode 100644 index 000000000..75f3544f9 --- /dev/null +++ b/static/directives/teams-manager.html @@ -0,0 +1,30 @@ +
    + + Create Team + + +
    +
    +
    +
    + + + {{ team.name }} + + + {{ team.name }} + +
    + +
    +
    + +
    + + +
    +
    +
    +
    +
    \ No newline at end of file diff --git a/static/js/directives/ui/repo-list-grid.js b/static/js/directives/ui/repo-list-grid.js index aa54bfda0..a01aec827 100644 --- a/static/js/directives/ui/repo-list-grid.js +++ b/static/js/directives/ui/repo-list-grid.js @@ -11,9 +11,9 @@ angular.module('quay').directive('repoListGrid', function () { scope: { repositoriesResource: '=repositoriesResource', starred: '=starred', - user: "=user", namespace: '=namespace', - starToggled: '&starToggled' + starToggled: '&starToggled', + hideTitle: '=hideTitle' }, controller: function($scope, $element, UserService) { $scope.isOrganization = function(namespace) { diff --git a/static/js/directives/ui/teams-manager.js b/static/js/directives/ui/teams-manager.js new file mode 100644 index 000000000..ceb9d00eb --- /dev/null +++ b/static/js/directives/ui/teams-manager.js @@ -0,0 +1,82 @@ +/** + * Element for managing the teams of an organization. + */ +angular.module('quay').directive('teamsManager', function () { + var directiveDefinitionObject = { + priority: 0, + templateUrl: '/static/directives/teams-manager.html', + replace: false, + transclude: false, + restrict: 'C', + scope: { + 'organization': '=organization' + }, + controller: function($scope, $element, ApiService, CreateService) { + $scope.TEAM_PATTERN = TEAM_PATTERN; + $scope.teamRoles = [ + { 'id': 'member', 'title': 'Member', 'kind': 'default' }, + { 'id': 'creator', 'title': 'Creator', 'kind': 'success' }, + { 'id': 'admin', 'title': 'Admin', 'kind': 'primary' } + ]; + + $scope.setRole = function(role, teamname) { + var previousRole = $scope.organization.teams[teamname].role; + $scope.organization.teams[teamname].role = role; + + var params = { + 'orgname': $scope.organization.name, + 'teamname': teamname + }; + + var data = $scope.organization.teams[teamname]; + + var errorHandler = ApiService.errorDisplay('Cannot update team', function(resp) { + $scope.organization.teams[teamname].role = previousRole; + }); + + ApiService.updateOrganizationTeam(data, params).then(function(resp) { + }, errorHandler); + }; + + $scope.createTeam = function(teamname) { + if (!teamname) { + return; + } + + if ($scope.organization.teams[teamname]) { + $('#team-' + teamname).removeClass('highlight'); + setTimeout(function() { + $('#team-' + teamname).addClass('highlight'); + }, 10); + return; + } + + var orgname = $scope.organization.name; + CreateService.createOrganizationTeam(ApiService, orgname, teamname, function(created) { + $scope.organization.teams[teamname] = created; + }); + }; + + $scope.askDeleteTeam = function(teamname) { + bootbox.confirm('Are you sure you want to delete team ' + teamname + '?', function(resp) { + if (resp) { + $scope.deleteTeam(teamname); + } + }); + }; + + $scope.deleteTeam = function(teamname) { + var params = { + 'orgname': $scope.organization.name, + 'teamname': teamname + }; + + ApiService.deleteOrganizationTeam(null, params).then(function() { + delete $scope.organization.teams[teamname]; + }, ApiService.errorDisplay('Cannot delete team')); + }; + } + }; + + return directiveDefinitionObject; +}); \ No newline at end of file diff --git a/static/js/pages/org-admin.js b/static/js/pages/org-admin.js index 194366f8a..b655e825b 100644 --- a/static/js/pages/org-admin.js +++ b/static/js/pages/org-admin.js @@ -1,6 +1,6 @@ (function() { /** - * Organization admin/settings page. + * DEPRECATED: Organization admin/settings page. */ angular.module('quayPages').config(['pages', function(pages) { pages.create('org-admin', 'org-admin.html', OrgAdminCtrl); diff --git a/static/js/pages/org-view.js b/static/js/pages/org-view.js index f1a3fa287..0f33e9d92 100644 --- a/static/js/pages/org-view.js +++ b/static/js/pages/org-view.js @@ -3,10 +3,94 @@ * Page that displays details about an organization, such as its teams. */ angular.module('quayPages').config(['pages', function(pages) { - pages.create('org-view', 'org-view.html', OrgViewCtrl); + pages.create('org-view', 'org-view.html', OrgViewCtrl, { + 'newLayout': true, + 'title': 'Organization {{ organization.name }}', + 'description': 'Organization {{ organization.name }}' + }, ['layout']) + + pages.create('org-view', 'old-org-view.html', OldOrgViewCtrl, { + }, ['old-layout']); }]); - function OrgViewCtrl($rootScope, $scope, ApiService, $routeParams, CreateService) { + function OrgViewCtrl($scope, $routeParams, $timeout, ApiService, UIService) { + var orgname = $routeParams.orgname; + + $scope.showLogsCounter = 0; + $scope.showApplicationsCounter = 0; + $scope.showInvoicesCounter = 0; + $scope.changingOrganization = false; + + $scope.$watch('organizationEmail', function(e) { + UIService.hidePopover('#changeEmailForm'); + }); + + var loadRepositories = function() { + var options = { + 'public': false, + 'private': true, + 'sort': true, + 'namespace': orgname, + }; + + $scope.repositoriesResource = ApiService.listReposAsResource().withOptions(options).get(function(resp) { + return resp.repositories; + }); + }; + + var loadOrganization = function() { + $scope.orgResource = ApiService.getOrganizationAsResource({'orgname': orgname}).get(function(org) { + $scope.organization = org; + $scope.organizationEmail = org.email; + $scope.isAdmin = org.is_admin; + $scope.isMember = org.is_member; + + // Load the repositories. + $timeout(function() { + loadRepositories(); + }, 10); + }); + }; + + // Load the organization. + loadOrganization(); + + $scope.showInvoices = function() { + $scope.showInvoicesCounter++; + }; + + $scope.showApplications = function() { + $scope.showApplicationsCounter++; + }; + + $scope.showLogs = function() { + $scope.showLogsCounter++; + }; + + $scope.changeEmail = function() { + UIService.hidePopover('#changeEmailForm'); + + $scope.changingOrganization = true; + var params = { + 'orgname': orgname + }; + + var data = { + 'email': $scope.organizationEmail + }; + + ApiService.changeOrganizationDetails(data, params).then(function(org) { + $scope.changingOrganization = false; + $scope.changeEmailForm.$setPristine(); + $scope.organization = org; + }, function(result) { + $scope.changingOrganization = false; + UIService.showFormError('#changeEmailForm', result); + }); + }; + } + + function OldOrgViewCtrl($rootScope, $scope, ApiService, $routeParams, CreateService) { var orgname = $routeParams.orgname; $scope.TEAM_PATTERN = TEAM_PATTERN; diff --git a/static/partials/old-org-view.html b/static/partials/old-org-view.html new file mode 100644 index 000000000..c5aae8994 --- /dev/null +++ b/static/partials/old-org-view.html @@ -0,0 +1,85 @@ +
    +
    +
    +
    + + + Create Team + + + Settings +
    +
    + + + +
    +
    +
    +
    + + + {{ team.name }} + + + {{ team.name }} + +
    + +
    +
    + +
    + + +
    +
    +
    +
    +
    + + + + + + + diff --git a/static/partials/org-view.html b/static/partials/org-view.html index c5aae8994..f28476c56 100644 --- a/static/partials/org-view.html +++ b/static/partials/org-view.html @@ -1,85 +1,128 @@ -
    -
    -
    -
    +
    +
    +
    + + + + {{ organization.name }} + +
    - - Create Team +
    +
    + + + + + + + + + + + + + + + + + + + + + + + + + + +
    - Settings -
    -
    +
    + +
    +

    Repositories

    +
    +
    +
    - + +
    +

    Teams

    +
    +
    -
    -
    -
    -
    - - - {{ team.name }} - - - {{ team.name }} - + +
    +

    Robot Accounts

    +
    +
    + + +
    +

    Default Permissions

    +
    +
    + + +
    +
    +
    + + +
    +

    Applications

    +
    +
    + + +
    +

    Plan Usage and Billing

    +
    +
    + + +
    +

    Billing Invoices

    +
    +
    + + +
    +

    Organization Settings

    + + +
    +
    Organization's e-mail address
    +
    +
    + + + +
    +
    -
    - -
    - - -
    -
    +
    -
    - - - - - - - +
    \ No newline at end of file From 6d0855f4fc74563a140e1711dbb910f9c4d505e8 Mon Sep 17 00:00:00 2001 From: Joseph Schorr Date: Wed, 25 Mar 2015 16:30:13 -0400 Subject: [PATCH 20/96] Small UI improvements to the teams tab --- static/css/directives/ui/teams-manager.css | 3 +++ static/directives/popup-input-button.html | 2 +- static/directives/teams-manager.html | 3 ++- static/partials/org-view.html | 1 - 4 files changed, 6 insertions(+), 3 deletions(-) create mode 100644 static/css/directives/ui/teams-manager.css diff --git a/static/css/directives/ui/teams-manager.css b/static/css/directives/ui/teams-manager.css new file mode 100644 index 000000000..24de3e908 --- /dev/null +++ b/static/css/directives/ui/teams-manager.css @@ -0,0 +1,3 @@ +.teams-manager .popup-input-button { + float: right; +} \ No newline at end of file diff --git a/static/directives/popup-input-button.html b/static/directives/popup-input-button.html index d6acadeb2..f76de6a43 100644 --- a/static/directives/popup-input-button.html +++ b/static/directives/popup-input-button.html @@ -1,4 +1,4 @@ -
    -
    \ No newline at end of file +
    diff --git a/static/directives/source-commit-link.html b/static/directives/source-commit-link.html index d8803b319..5bd3b187a 100644 --- a/static/directives/source-commit-link.html +++ b/static/directives/source-commit-link.html @@ -1,4 +1,4 @@ {{ commitSha.substring(0, 8) }} - \ No newline at end of file + diff --git a/static/directives/source-ref-link.html b/static/directives/source-ref-link.html index dcf142001..986216aa8 100644 --- a/static/directives/source-ref-link.html +++ b/static/directives/source-ref-link.html @@ -12,4 +12,4 @@ {{ getTitle(ref) }} - \ No newline at end of file + diff --git a/static/directives/tag-operations-dialog.html b/static/directives/tag-operations-dialog.html index d47d4651c..d79479d84 100644 --- a/static/directives/tag-operations-dialog.html +++ b/static/directives/tag-operations-dialog.html @@ -84,4 +84,4 @@ The following images and any other images not referenced by a tag will be deleted:
    -
    \ No newline at end of file +
    diff --git a/static/directives/teams-manager.html b/static/directives/teams-manager.html index 7cb47dba8..8191b350f 100644 --- a/static/directives/teams-manager.html +++ b/static/directives/teams-manager.html @@ -1,31 +1,62 @@
    +
    + submitted="createTeam(value)" ng-show="organization.is_admin"> Create New Team

    Teams

    +
    -
    -
    -
    -
    - - - {{ team.name }} - - - {{ team.name }} - -
    + -
    +
    +
    +
    +
    + + + {{ team.name }} + + + {{ team.name }} +
    -
    - - +
    + +
    +
    + + + + + + + + {{ members[team.name].members.length - 25 }} more team members. + (Empty Team)
    + +
    + + + + + Delete Team {{ team.name }} + + +
    -
    \ No newline at end of file +
    diff --git a/static/directives/triggered-build-description.html b/static/directives/triggered-build-description.html index 9fb907701..09f494417 100644 --- a/static/directives/triggered-build-description.html +++ b/static/directives/triggered-build-description.html @@ -62,4 +62,4 @@ -
    \ No newline at end of file +
    diff --git a/static/js/directives/ng-image-watch.js b/static/js/directives/ng-image-watch.js new file mode 100644 index 000000000..43aac4792 --- /dev/null +++ b/static/js/directives/ng-image-watch.js @@ -0,0 +1,17 @@ +/** + * Adds a ng-image-watch attribute, which is a callback invoked when the image is loaded or fails. + */ +angular.module('quay').directive('ngImageWatch', function () { + return { + restrict: 'A', + link: function postLink($scope, $element, $attr) { + $element.bind('error', function() { + $scope.$eval($attr.ngImageWatch)(false); + }); + + $element.bind('load', function() { + $scope.$eval($attr.ngImageWatch)(true); + }); + } + }; +}); \ No newline at end of file diff --git a/static/js/directives/quay-layout.js b/static/js/directives/quay-layout.js index 58db5b450..96053510a 100644 --- a/static/js/directives/quay-layout.js +++ b/static/js/directives/quay-layout.js @@ -120,6 +120,8 @@ angular.module('quay').directive('quayClasses', function(Features, Config) { /** * Adds a quay-include attribtue that adds a template solely if the expression evaluates to true. * Automatically adds the Features and Config services to the scope. + * + Usage: quay-include="{'Features.BILLING': 'partials/landing-normal.html', '!Features.BILLING': 'partials/landing-login.html'}" */ angular.module('quay').directive('quayInclude', function($compile, $templateCache, $http, Features, Config) { return { @@ -127,7 +129,7 @@ angular.module('quay').directive('quayInclude', function($compile, $templateCach restrict: 'A', link: function($scope, $element, $attr, ctrl) { var getTemplate = function(templateName) { - var templateUrl = '/static/partials/' + templateName; + var templateUrl = '/static/' + templateName; return $http.get(templateUrl, {cache: $templateCache}); }; diff --git a/static/js/directives/ui/anchor.js b/static/js/directives/ui/anchor.js new file mode 100644 index 000000000..ec10e082d --- /dev/null +++ b/static/js/directives/ui/anchor.js @@ -0,0 +1,19 @@ +/** + * An element which displays its contents wrapped in an tag, but only if the href is not null. + */ +angular.module('quay').directive('anchor', function () { + var directiveDefinitionObject = { + priority: 0, + templateUrl: '/static/directives/anchor.html', + replace: false, + transclude: true, + restrict: 'C', + scope: { + 'href': '@href', + 'isOnlyText': '=isOnlyText' + }, + controller: function($scope, $element) { + } + }; + return directiveDefinitionObject; +}); \ No newline at end of file diff --git a/static/js/directives/ui/avatar.js b/static/js/directives/ui/avatar.js index e2e9b339f..18dd9bb72 100644 --- a/static/js/directives/ui/avatar.js +++ b/static/js/directives/ui/avatar.js @@ -1,5 +1,5 @@ /** - * An element which displays an avatar for the given {email,name} or hash. + * An element which displays an avatar for the given avatar data. */ angular.module('quay').directive('avatar', function () { var directiveDefinitionObject = { @@ -9,25 +9,36 @@ angular.module('quay').directive('avatar', function () { transclude: true, restrict: 'C', scope: { - 'hash': '=hash', - 'email': '=email', - 'name': '=name', + 'data': '=data', 'size': '=size' }, - controller: function($scope, $element, AvatarService) { + controller: function($scope, $element, AvatarService, Config, UIService) { $scope.AvatarService = AvatarService; + $scope.Config = Config; + $scope.isLoading = true; + $scope.hasGravatar = false; + $scope.loadGravatar = false; - var refreshHash = function() { - if (!$scope.name && !$scope.email) { return; } - $scope._hash = AvatarService.computeHash($scope.email, $scope.name); + $scope.imageCallback = function(r) { + $scope.isLoading = false; + $scope.hasGravatar = r; }; - $scope.$watch('hash', function(hash) { - $scope._hash = hash; + $scope.$watch('size', function(size) { + size = size * 1 || 16; + $scope.fontSize = (size - 4) + 'px'; + $scope.lineHeight = size + 'px'; }); - $scope.$watch('name', refreshHash); - $scope.$watch('email', refreshHash); + $scope.$watch('data', function(data) { + if (!data) { return; } + + $scope.loadGravatar = Config.AVATAR_KIND == 'gravatar' && + (data.kind == 'user' || data.kind == 'org'); + + $scope.isLoading = $scope.loadGravatar; + $scope.hasGravatar = false; + }); } }; return directiveDefinitionObject; diff --git a/static/js/directives/ui/entity-reference.js b/static/js/directives/ui/entity-reference.js index 41b280304..403ddcd9a 100644 --- a/static/js/directives/ui/entity-reference.js +++ b/static/js/directives/ui/entity-reference.js @@ -39,6 +39,21 @@ angular.module('quay').directive('entityReference', function () { return '/organization/' + org['name'] + '/admin?tab=robots&showRobot=' + UtilService.textToSafeHtml(name); }; + $scope.getTitle = function(entity) { + if (!entity) { return ''; } + + switch (entity.kind) { + case 'org': + return 'Organization'; + + case 'team': + return 'Team'; + + case 'user': + return entity.is_robot ? 'Robot Account' : 'User'; + } + }; + $scope.getPrefix = function(name) { if (!name) { return ''; } var plus = name.indexOf('+'); diff --git a/static/js/directives/ui/entity-search.js b/static/js/directives/ui/entity-search.js index eb7313509..3362197bc 100644 --- a/static/js/directives/ui/entity-search.js +++ b/static/js/directives/ui/entity-search.js @@ -56,6 +56,8 @@ angular.module('quay').directive('entitySearch', function () { $scope.currentEntityInternal = $scope.currentEntity; + $scope.Config = Config; + var isSupported = function(kind, opt_array) { return $.inArray(kind, opt_array || $scope.allowedEntities || ['user', 'team', 'robot']) >= 0; }; @@ -102,7 +104,7 @@ angular.module('quay').directive('entitySearch', function () { } CreateService.createOrganizationTeam(ApiService, $scope.namespace, teamname, function(created) { - $scope.setEntity(created.name, 'team', false); + $scope.setEntity(created.name, 'team', false, created.avatar); $scope.teams[teamname] = created; }); }); @@ -121,17 +123,18 @@ angular.module('quay').directive('entitySearch', function () { } CreateService.createRobotAccount(ApiService, $scope.isOrganization, $scope.namespace, robotname, function(created) { - $scope.setEntity(created.name, 'user', true); + $scope.setEntity(created.name, 'user', true, created.avatar); $scope.robots.push(created); }); }); }; - $scope.setEntity = function(name, kind, is_robot) { + $scope.setEntity = function(name, kind, is_robot, avatar) { var entity = { 'name': name, 'kind': kind, - 'is_robot': is_robot + 'is_robot': is_robot, + 'avatar': avatar }; if ($scope.isOrganization) { diff --git a/static/js/directives/ui/repository-permissions-table.js b/static/js/directives/ui/repository-permissions-table.js index a0dfaddb2..95ce43f32 100644 --- a/static/js/directives/ui/repository-permissions-table.js +++ b/static/js/directives/ui/repository-permissions-table.js @@ -68,7 +68,8 @@ angular.module('quay').directive('repositoryPermissionsTable', function () { 'kind': kind, 'name': name, 'is_robot': permission.is_robot, - 'is_org_member': permission.is_org_member + 'is_org_member': permission.is_org_member, + 'avatar': permission.avatar }; }; diff --git a/static/js/directives/ui/teams-manager.js b/static/js/directives/ui/teams-manager.js index ceb9d00eb..52b434b47 100644 --- a/static/js/directives/ui/teams-manager.js +++ b/static/js/directives/ui/teams-manager.js @@ -19,6 +19,45 @@ angular.module('quay').directive('teamsManager', function () { { 'id': 'admin', 'title': 'Admin', 'kind': 'primary' } ]; + $scope.members = {}; + $scope.orderedTeams = []; + + var loadTeamMembers = function() { + if (!$scope.organization) { return; } + + for (var name in $scope.organization.teams) { + if (!$scope.organization.teams.hasOwnProperty(name)) { continue; } + loadMembersOfTeam(name); + } + }; + + var loadMembersOfTeam = function(name) { + var params = { + 'orgname': $scope.organization.name, + 'teamname': name + }; + + $scope.members[name] = {}; + + ApiService.getOrganizationTeamMembers(null, params).then(function(resp) { + $scope.members[name].members = resp.members; + }, function() { + delete $scope.members[name]; + }); + }; + + var loadOrderedTeams = function() { + if (!$scope.organization) { return; } + + $scope.orderedTeams = []; + $scope.organization.ordered_teams.map(function(name) { + $scope.orderedTeams.push($scope.organization.teams[name]); + }); + }; + + $scope.$watch('organization', loadOrderedTeams); + $scope.$watch('organization', loadTeamMembers); + $scope.setRole = function(role, teamname) { var previousRole = $scope.organization.teams[teamname].role; $scope.organization.teams[teamname].role = role; @@ -54,6 +93,10 @@ angular.module('quay').directive('teamsManager', function () { var orgname = $scope.organization.name; CreateService.createOrganizationTeam(ApiService, orgname, teamname, function(created) { $scope.organization.teams[teamname] = created; + $scope.members[teamname] = {}; + $scope.members[teamname].members = []; + $scope.organization.ordered_teams.push(teamname); + $scope.orderedTeams.push(created); }); }; @@ -72,6 +115,12 @@ angular.module('quay').directive('teamsManager', function () { }; ApiService.deleteOrganizationTeam(null, params).then(function() { + var index = $scope.organization.ordered_teams.indexOf(teamname); + if (index >= 0) { + $scope.organization.ordered_teams.splice(index, 1); + } + + loadOrderedTeams(); delete $scope.organization.teams[teamname]; }, ApiService.errorDisplay('Cannot delete team')); }; diff --git a/static/js/services/avatar-service.js b/static/js/services/avatar-service.js index 500475000..8aa3d436a 100644 --- a/static/js/services/avatar-service.js +++ b/static/js/services/avatar-service.js @@ -14,7 +14,9 @@ angular.module('quay').factory('AvatarService', ['Config', '$sanitize', 'md5', break; case 'gravatar': - return '//www.gravatar.com/avatar/' + hash + '?d=identicon&size=' + size; + // TODO(jschorr): Remove once the new layout is in place everywhere. + var default_kind = Config.isNewLayout() ? '404' : 'identicon'; + return '//www.gravatar.com/avatar/' + hash + '?d=' + default_kind + '&size=' + size; break; } }; diff --git a/static/js/services/features-config.js b/static/js/services/features-config.js index 2632aa0d3..7cc66b2dc 100644 --- a/static/js/services/features-config.js +++ b/static/js/services/features-config.js @@ -71,5 +71,10 @@ angular.module('quay').factory('Config', [function() { return value; }; + config.isNewLayout = function() { + // TODO(jschorr): Remove once new layout is in place for everyone. + return document.cookie.toString().indexOf('quay.exp-new-layout=true') >= 0; + }; + return config; }]); \ No newline at end of file diff --git a/static/partials/build-view.html b/static/partials/build-view.html index 1a2f00c77..43f48a7ce 100644 --- a/static/partials/build-view.html +++ b/static/partials/build-view.html @@ -61,4 +61,4 @@
    - \ No newline at end of file + diff --git a/static/partials/image-view.html b/static/partials/image-view.html index 615361ee3..181938afc 100644 --- a/static/partials/image-view.html +++ b/static/partials/image-view.html @@ -56,4 +56,4 @@ - \ No newline at end of file + diff --git a/static/partials/landing-login.html b/static/partials/landing-login.html index 5afb89af2..024762706 100644 --- a/static/partials/landing-login.html +++ b/static/partials/landing-login.html @@ -47,7 +47,7 @@
    - +
    Welcome {{ user.username }}!
    Browse all repositories Create a new repository diff --git a/static/partials/landing-normal.html b/static/partials/landing-normal.html index f8b969cd8..7865fdfac 100644 --- a/static/partials/landing-normal.html +++ b/static/partials/landing-normal.html @@ -46,7 +46,7 @@
    - +
    Welcome {{ user.username }}!
    Browse all repositories Create a new repository diff --git a/static/partials/landing.html b/static/partials/landing.html index 719d1a9cb..7813e1ab8 100644 --- a/static/partials/landing.html +++ b/static/partials/landing.html @@ -1,3 +1,3 @@ -
    +
    diff --git a/static/partials/manage-application.html b/static/partials/manage-application.html index ea7fc0860..d4101c5a1 100644 --- a/static/partials/manage-application.html +++ b/static/partials/manage-application.html @@ -10,7 +10,7 @@

    {{ application.name || '(Untitled)' }}

    - + {{ organization.name }}

    @@ -100,7 +100,7 @@
    Note: The generated token will act on behalf of user - + {{ user.username }}
    diff --git a/static/partials/org-view.html b/static/partials/org-view.html index 7069ec9e4..6fb5995b4 100644 --- a/static/partials/org-view.html +++ b/static/partials/org-view.html @@ -5,7 +5,7 @@
    - + {{ organization.name }}
    @@ -124,4 +124,4 @@
    - \ No newline at end of file + diff --git a/static/partials/organizations.html b/static/partials/organizations.html index b556ed300..7bcc4f05c 100644 --- a/static/partials/organizations.html +++ b/static/partials/organizations.html @@ -23,7 +23,7 @@

    Organizations

    diff --git a/static/partials/repo-list.html b/static/partials/repo-list.html index d5ef953ec..87e54ba4b 100644 --- a/static/partials/repo-list.html +++ b/static/partials/repo-list.html @@ -34,11 +34,11 @@