##// END OF EJS Templates
hovercacrds: added new tooltips and hovercards to expose certain information for objects shown in UI
marcink -
r4026:ed756817 default
parent child Browse files
Show More

The requested changes are too big and content was truncated. Show full diff

@@ -0,0 +1,38 b''
1 # -*- coding: utf-8 -*-
2
3 # Copyright (C) 2018-2019 RhodeCode GmbH
4 #
5 # This program is free software: you can redistribute it and/or modify
6 # it under the terms of the GNU Affero General Public License, version 3
7 # (only), as published by the Free Software Foundation.
8 #
9 # This program is distributed in the hope that it will be useful,
10 # but WITHOUT ANY WARRANTY; without even the implied warranty of
11 # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
12 # GNU General Public License for more details.
13 #
14 # You should have received a copy of the GNU Affero General Public License
15 # along with this program. If not, see <http://www.gnu.org/licenses/>.
16 #
17 # This program is dual-licensed. If you wish to learn more about the
18 # RhodeCode Enterprise Edition, including its added features, Support services,
19 # and proprietary license terms, please see https://rhodecode.com/licenses/
20 from rhodecode.config import routing_links
21
22
23 def includeme(config):
24
25 config.add_route(
26 name='hovercard_user',
27 pattern='/_hovercard/user/{user_id}')
28
29 config.add_route(
30 name='hovercard_user_group',
31 pattern='/_hovercard/user_group/{user_group_id}')
32
33 config.add_route(
34 name='hovercard_commit',
35 pattern='/_hovercard/commit/{repo_name}/{user_id}')
36
37 # Scan module for configuration decorators.
38 config.scan('.views', ignore='.tests')
@@ -0,0 +1,71 b''
1 # -*- coding: utf-8 -*-
2
3 # Copyright (C) 2016-2019 RhodeCode GmbH
4 #
5 # This program is free software: you can redistribute it and/or modify
6 # it under the terms of the GNU Affero General Public License, version 3
7 # (only), as published by the Free Software Foundation.
8 #
9 # This program is distributed in the hope that it will be useful,
10 # but WITHOUT ANY WARRANTY; without even the implied warranty of
11 # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
12 # GNU General Public License for more details.
13 #
14 # You should have received a copy of the GNU Affero General Public License
15 # along with this program. If not, see <http://www.gnu.org/licenses/>.
16 #
17 # This program is dual-licensed. If you wish to learn more about the
18 # RhodeCode Enterprise Edition, including its added features, Support services,
19 # and proprietary license terms, please see https://rhodecode.com/licenses/
20
21 import re
22 import logging
23 import collections
24
25 from pyramid.view import view_config
26
27 from rhodecode.apps._base import BaseAppView
28 from rhodecode.lib import helpers as h
29 from rhodecode.lib.auth import (
30 LoginRequired, NotAnonymous, HasRepoGroupPermissionAnyDecorator, CSRFRequired)
31 from rhodecode.lib.codeblocks import filenode_as_lines_tokens
32 from rhodecode.lib.index import searcher_from_config
33 from rhodecode.lib.utils2 import safe_unicode, str2bool, safe_int
34 from rhodecode.lib.ext_json import json
35 from rhodecode.lib.vcs.nodes import FileNode
36 from rhodecode.model.db import (
37 func, true, or_, case, in_filter_generator, Repository, RepoGroup, User, UserGroup)
38 from rhodecode.model.repo import RepoModel
39 from rhodecode.model.repo_group import RepoGroupModel
40 from rhodecode.model.scm import RepoGroupList, RepoList
41 from rhodecode.model.user import UserModel
42 from rhodecode.model.user_group import UserGroupModel
43
44 log = logging.getLogger(__name__)
45
46
47 class HoverCardsView(BaseAppView):
48
49 def load_default_context(self):
50 c = self._get_local_tmpl_context()
51 return c
52
53 @LoginRequired()
54 @view_config(
55 route_name='hovercard_user', request_method='GET', xhr=True,
56 renderer='rhodecode:templates/hovercards/hovercard_user.mako')
57 def hovercard_user(self):
58 c = self.load_default_context()
59 user_id = self.request.matchdict['user_id']
60 c.user = User.get_or_404(user_id)
61 return self._get_template_context(c)
62
63 @LoginRequired()
64 @view_config(
65 route_name='hovercard_user_group', request_method='GET', xhr=True,
66 renderer='rhodecode:templates/hovercards/hovercard_user_group.mako')
67 def hovercard_user_group(self):
68 c = self.load_default_context()
69 user_group_id = self.request.matchdict['user_group_id']
70 c.user_group = UserGroup.get_or_404(user_group_id)
71 return self._get_template_context(c)
@@ -0,0 +1,460 b''
1 /* This is the core CSS of Tooltipster */
2
3 /* GENERAL STRUCTURE RULES (do not edit this section) */
4
5 .tooltipster-base {
6 /* this ensures that a constrained height set by functionPosition,
7 if greater that the natural height of the tooltip, will be enforced
8 in browsers that support display:flex */
9 display: flex;
10 pointer-events: none;
11 /* this may be overriden in JS for fixed position origins */
12 position: absolute;
13 }
14
15 .tooltipster-box {
16 /* see .tooltipster-base. flex-shrink 1 is only necessary for IE10-
17 and flex-basis auto for IE11- (at least) */
18 flex: 1 1 auto;
19 }
20
21 .tooltipster-content {
22 /* prevents an overflow if the user adds padding to the div */
23 box-sizing: border-box;
24 /* these make sure we'll be able to detect any overflow */
25 max-height: 100%;
26 max-width: 100%;
27 overflow: auto;
28 }
29
30 .tooltipster-ruler {
31 /* these let us test the size of the tooltip without overflowing the window */
32 bottom: 0;
33 left: 0;
34 overflow: hidden;
35 position: fixed;
36 right: 0;
37 top: 0;
38 visibility: hidden;
39 }
40
41 /* ANIMATIONS */
42
43 /* Open/close animations */
44
45 /* fade */
46
47 .tooltipster-fade {
48 opacity: 0;
49 -webkit-transition-property: opacity;
50 -moz-transition-property: opacity;
51 -o-transition-property: opacity;
52 -ms-transition-property: opacity;
53 transition-property: opacity;
54 }
55 .tooltipster-fade.tooltipster-show {
56 opacity: 1;
57 }
58
59 /* grow */
60
61 .tooltipster-grow {
62 -webkit-transform: scale(0,0);
63 -moz-transform: scale(0,0);
64 -o-transform: scale(0,0);
65 -ms-transform: scale(0,0);
66 transform: scale(0,0);
67 -webkit-transition-property: -webkit-transform;
68 -moz-transition-property: -moz-transform;
69 -o-transition-property: -o-transform;
70 -ms-transition-property: -ms-transform;
71 transition-property: transform;
72 -webkit-backface-visibility: hidden;
73 }
74 .tooltipster-grow.tooltipster-show {
75 -webkit-transform: scale(1,1);
76 -moz-transform: scale(1,1);
77 -o-transform: scale(1,1);
78 -ms-transform: scale(1,1);
79 transform: scale(1,1);
80 -webkit-transition-timing-function: cubic-bezier(0.175, 0.885, 0.320, 1);
81 -webkit-transition-timing-function: cubic-bezier(0.175, 0.885, 0.320, 1.15);
82 -moz-transition-timing-function: cubic-bezier(0.175, 0.885, 0.320, 1.15);
83 -ms-transition-timing-function: cubic-bezier(0.175, 0.885, 0.320, 1.15);
84 -o-transition-timing-function: cubic-bezier(0.175, 0.885, 0.320, 1.15);
85 transition-timing-function: cubic-bezier(0.175, 0.885, 0.320, 1.15);
86 }
87
88 /* swing */
89
90 .tooltipster-swing {
91 opacity: 0;
92 -webkit-transform: rotateZ(4deg);
93 -moz-transform: rotateZ(4deg);
94 -o-transform: rotateZ(4deg);
95 -ms-transform: rotateZ(4deg);
96 transform: rotateZ(4deg);
97 -webkit-transition-property: -webkit-transform, opacity;
98 -moz-transition-property: -moz-transform;
99 -o-transition-property: -o-transform;
100 -ms-transition-property: -ms-transform;
101 transition-property: transform;
102 }
103 .tooltipster-swing.tooltipster-show {
104 opacity: 1;
105 -webkit-transform: rotateZ(0deg);
106 -moz-transform: rotateZ(0deg);
107 -o-transform: rotateZ(0deg);
108 -ms-transform: rotateZ(0deg);
109 transform: rotateZ(0deg);
110 -webkit-transition-timing-function: cubic-bezier(0.230, 0.635, 0.495, 1);
111 -webkit-transition-timing-function: cubic-bezier(0.230, 0.635, 0.495, 2.4);
112 -moz-transition-timing-function: cubic-bezier(0.230, 0.635, 0.495, 2.4);
113 -ms-transition-timing-function: cubic-bezier(0.230, 0.635, 0.495, 2.4);
114 -o-transition-timing-function: cubic-bezier(0.230, 0.635, 0.495, 2.4);
115 transition-timing-function: cubic-bezier(0.230, 0.635, 0.495, 2.4);
116 }
117
118 /* fall */
119
120 .tooltipster-fall {
121 -webkit-transition-property: top;
122 -moz-transition-property: top;
123 -o-transition-property: top;
124 -ms-transition-property: top;
125 transition-property: top;
126 -webkit-transition-timing-function: cubic-bezier(0.175, 0.885, 0.320, 1);
127 -webkit-transition-timing-function: cubic-bezier(0.175, 0.885, 0.320, 1.15);
128 -moz-transition-timing-function: cubic-bezier(0.175, 0.885, 0.320, 1.15);
129 -ms-transition-timing-function: cubic-bezier(0.175, 0.885, 0.320, 1.15);
130 -o-transition-timing-function: cubic-bezier(0.175, 0.885, 0.320, 1.15);
131 transition-timing-function: cubic-bezier(0.175, 0.885, 0.320, 1.15);
132 }
133 .tooltipster-fall.tooltipster-initial {
134 top: 0 !important;
135 }
136 .tooltipster-fall.tooltipster-show {
137 }
138 .tooltipster-fall.tooltipster-dying {
139 -webkit-transition-property: all;
140 -moz-transition-property: all;
141 -o-transition-property: all;
142 -ms-transition-property: all;
143 transition-property: all;
144 top: 0 !important;
145 opacity: 0;
146 }
147
148 /* slide */
149
150 .tooltipster-slide {
151 -webkit-transition-property: left;
152 -moz-transition-property: left;
153 -o-transition-property: left;
154 -ms-transition-property: left;
155 transition-property: left;
156 -webkit-transition-timing-function: cubic-bezier(0.175, 0.885, 0.320, 1);
157 -webkit-transition-timing-function: cubic-bezier(0.175, 0.885, 0.320, 1.15);
158 -moz-transition-timing-function: cubic-bezier(0.175, 0.885, 0.320, 1.15);
159 -ms-transition-timing-function: cubic-bezier(0.175, 0.885, 0.320, 1.15);
160 -o-transition-timing-function: cubic-bezier(0.175, 0.885, 0.320, 1.15);
161 transition-timing-function: cubic-bezier(0.175, 0.885, 0.320, 1.15);
162 }
163 .tooltipster-slide.tooltipster-initial {
164 left: -40px !important;
165 }
166 .tooltipster-slide.tooltipster-show {
167 }
168 .tooltipster-slide.tooltipster-dying {
169 -webkit-transition-property: all;
170 -moz-transition-property: all;
171 -o-transition-property: all;
172 -ms-transition-property: all;
173 transition-property: all;
174 left: 0 !important;
175 opacity: 0;
176 }
177
178 /* Update animations */
179
180 /* We use animations rather than transitions here because
181 transition durations may be specified in the style tag due to
182 animationDuration, and we try to avoid collisions and the use
183 of !important */
184
185 /* fade */
186
187 @keyframes tooltipster-fading {
188 0% {
189 opacity: 0;
190 }
191 100% {
192 opacity: 1;
193 }
194 }
195
196 .tooltipster-update-fade {
197 animation: tooltipster-fading 400ms;
198 }
199
200 /* rotate */
201
202 @keyframes tooltipster-rotating {
203 25% {
204 transform: rotate(-2deg);
205 }
206 75% {
207 transform: rotate(2deg);
208 }
209 100% {
210 transform: rotate(0);
211 }
212 }
213
214 .tooltipster-update-rotate {
215 animation: tooltipster-rotating 600ms;
216 }
217
218 /* scale */
219
220 @keyframes tooltipster-scaling {
221 50% {
222 transform: scale(1.1);
223 }
224 100% {
225 transform: scale(1);
226 }
227 }
228
229 .tooltipster-update-scale {
230 animation: tooltipster-scaling 600ms;
231 }
232
233 /**
234 * DEFAULT STYLE OF THE SIDETIP PLUGIN
235 *
236 * All styles are "namespaced" with .tooltipster-sidetip to prevent
237 * conflicts between plugins.
238 */
239
240 /* .tooltipster-box */
241
242 .tooltipster-sidetip .tooltipster-box {
243 background: #565656;
244 border: 2px solid black;
245 border-radius: 4px;
246 }
247
248 .tooltipster-sidetip.tooltipster-bottom .tooltipster-box {
249 margin-top: 8px;
250 }
251
252 .tooltipster-sidetip.tooltipster-left .tooltipster-box {
253 margin-right: 8px;
254 }
255
256 .tooltipster-sidetip.tooltipster-right .tooltipster-box {
257 margin-left: 8px;
258 }
259
260 .tooltipster-sidetip.tooltipster-top .tooltipster-box {
261 margin-bottom: 8px;
262 }
263
264 /* .tooltipster-content */
265
266 .tooltipster-sidetip .tooltipster-content {
267 color: white;
268 line-height: 18px;
269 padding: 6px 14px;
270 }
271
272 /* .tooltipster-arrow : will keep only the zone of .tooltipster-arrow-uncropped that
273 corresponds to the arrow we want to display */
274
275 .tooltipster-sidetip .tooltipster-arrow {
276 overflow: hidden;
277 position: absolute;
278 }
279
280 .tooltipster-sidetip.tooltipster-bottom .tooltipster-arrow {
281 height: 10px;
282 /* half the width, for centering */
283 margin-left: -10px;
284 top: 0;
285 width: 20px;
286 }
287
288 .tooltipster-sidetip.tooltipster-left .tooltipster-arrow {
289 height: 20px;
290 margin-top: -10px;
291 right: 0;
292 /* top 0 to keep the arrow from overflowing .tooltipster-base when it has not
293 been positioned yet */
294 top: 0;
295 width: 10px;
296 }
297
298 .tooltipster-sidetip.tooltipster-right .tooltipster-arrow {
299 height: 20px;
300 margin-top: -10px;
301 left: 0;
302 /* same as .tooltipster-left .tooltipster-arrow */
303 top: 0;
304 width: 10px;
305 }
306
307 .tooltipster-sidetip.tooltipster-top .tooltipster-arrow {
308 bottom: 0;
309 height: 10px;
310 margin-left: -10px;
311 width: 20px;
312 }
313
314 /* common rules between .tooltipster-arrow-background and .tooltipster-arrow-border */
315
316 .tooltipster-sidetip .tooltipster-arrow-background, .tooltipster-sidetip .tooltipster-arrow-border {
317 height: 0;
318 position: absolute;
319 width: 0;
320 }
321
322 /* .tooltipster-arrow-background */
323
324 .tooltipster-sidetip .tooltipster-arrow-background {
325 border: 10px solid transparent;
326 }
327
328 .tooltipster-sidetip.tooltipster-bottom .tooltipster-arrow-background {
329 border-bottom-color: #565656;
330 left: 0;
331 top: 3px;
332 }
333
334 .tooltipster-sidetip.tooltipster-left .tooltipster-arrow-background {
335 border-left-color: #565656;
336 left: -3px;
337 top: 0;
338 }
339
340 .tooltipster-sidetip.tooltipster-right .tooltipster-arrow-background {
341 border-right-color: #565656;
342 left: 3px;
343 top: 0;
344 }
345
346 .tooltipster-sidetip.tooltipster-top .tooltipster-arrow-background {
347 border-top-color: #565656;
348 left: 0;
349 top: -3px;
350 }
351
352 /* .tooltipster-arrow-border */
353
354 .tooltipster-sidetip .tooltipster-arrow-border {
355 border: 10px solid transparent;
356 left: 0;
357 top: 0;
358 }
359
360 .tooltipster-sidetip.tooltipster-bottom .tooltipster-arrow-border {
361 border-bottom-color: black;
362 }
363
364 .tooltipster-sidetip.tooltipster-left .tooltipster-arrow-border {
365 border-left-color: black;
366 }
367
368 .tooltipster-sidetip.tooltipster-right .tooltipster-arrow-border {
369 border-right-color: black;
370 }
371
372 .tooltipster-sidetip.tooltipster-top .tooltipster-arrow-border {
373 border-top-color: black;
374 }
375
376 /* tooltipster-arrow-uncropped */
377
378 .tooltipster-sidetip .tooltipster-arrow-uncropped {
379 position: relative;
380 }
381
382 .tooltipster-sidetip.tooltipster-bottom .tooltipster-arrow-uncropped {
383 top: -10px;
384 }
385
386 .tooltipster-sidetip.tooltipster-right .tooltipster-arrow-uncropped {
387 left: -10px;
388 }
389
390 .tooltipster-sidetip.tooltipster-shadow .tooltipster-box {
391 border: none;
392 border-radius: 5px;
393 background: #fff;
394 box-shadow: 0 0 5px 3px rgba(0, 0, 0, .1)
395 }
396
397 .tooltipster-sidetip.tooltipster-shadow.tooltipster-bottom .tooltipster-box {
398 margin-top: 6px
399 }
400
401 .tooltipster-sidetip.tooltipster-shadow.tooltipster-left .tooltipster-box {
402 margin-right: 6px
403 }
404
405 .tooltipster-sidetip.tooltipster-shadow.tooltipster-right .tooltipster-box {
406 margin-left: 6px
407 }
408
409 .tooltipster-sidetip.tooltipster-shadow.tooltipster-top .tooltipster-box {
410 margin-bottom: 6px
411 }
412
413 .tooltipster-sidetip.tooltipster-shadow .tooltipster-content {
414 color: #8d8d8d
415 }
416
417 .tooltipster-sidetip.tooltipster-shadow .tooltipster-arrow {
418 height: 6px;
419 margin-left: -6px;
420 width: 12px
421 }
422
423 .tooltipster-sidetip.tooltipster-shadow.tooltipster-left .tooltipster-arrow, .tooltipster-sidetip.tooltipster-shadow.tooltipster-right .tooltipster-arrow {
424 height: 12px;
425 margin-left: 0;
426 margin-top: -6px;
427 width: 6px
428 }
429
430 .tooltipster-sidetip.tooltipster-shadow .tooltipster-arrow-background {
431 display: none
432 }
433
434 .tooltipster-sidetip.tooltipster-shadow .tooltipster-arrow-border {
435 border: 6px solid transparent
436 }
437
438 .tooltipster-sidetip.tooltipster-shadow.tooltipster-bottom .tooltipster-arrow-border {
439 border-bottom-color: #fff
440 }
441
442 .tooltipster-sidetip.tooltipster-shadow.tooltipster-left .tooltipster-arrow-border {
443 border-left-color: #fff
444 }
445
446 .tooltipster-sidetip.tooltipster-shadow.tooltipster-right .tooltipster-arrow-border {
447 border-right-color: #fff
448 }
449
450 .tooltipster-sidetip.tooltipster-shadow.tooltipster-top .tooltipster-arrow-border {
451 border-top-color: #fff
452 }
453
454 .tooltipster-sidetip.tooltipster-shadow.tooltipster-bottom .tooltipster-arrow-uncropped {
455 top: -6px
456 }
457
458 .tooltipster-sidetip.tooltipster-shadow.tooltipster-right .tooltipster-arrow-uncropped {
459 left: -6px
460 } No newline at end of file
1 NO CONTENT: new file 100644
The requested commit or file is too big and content was truncated. Show full diff
1 NO CONTENT: new file 100644
The requested commit or file is too big and content was truncated. Show full diff
@@ -1,754 +1,755 b''
1 1 # -*- coding: utf-8 -*-
2 2
3 3 # Copyright (C) 2010-2019 RhodeCode GmbH
4 4 #
5 5 # This program is free software: you can redistribute it and/or modify
6 6 # it under the terms of the GNU Affero General Public License, version 3
7 7 # (only), as published by the Free Software Foundation.
8 8 #
9 9 # This program is distributed in the hope that it will be useful,
10 10 # but WITHOUT ANY WARRANTY; without even the implied warranty of
11 11 # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
12 12 # GNU General Public License for more details.
13 13 #
14 14 # You should have received a copy of the GNU Affero General Public License
15 15 # along with this program. If not, see <http://www.gnu.org/licenses/>.
16 16 #
17 17 # This program is dual-licensed. If you wish to learn more about the
18 18 # RhodeCode Enterprise Edition, including its added features, Support services,
19 19 # and proprietary license terms, please see https://rhodecode.com/licenses/
20 20
21 21 import os
22 22 import sys
23 23 import logging
24 24 import collections
25 25 import tempfile
26 26 import time
27 27
28 28 from paste.gzipper import make_gzip_middleware
29 29 import pyramid.events
30 30 from pyramid.wsgi import wsgiapp
31 31 from pyramid.authorization import ACLAuthorizationPolicy
32 32 from pyramid.config import Configurator
33 33 from pyramid.settings import asbool, aslist
34 34 from pyramid.httpexceptions import (
35 35 HTTPException, HTTPError, HTTPInternalServerError, HTTPFound, HTTPNotFound)
36 36 from pyramid.renderers import render_to_response
37 37
38 38 from rhodecode.model import meta
39 39 from rhodecode.config import patches
40 40 from rhodecode.config import utils as config_utils
41 41 from rhodecode.config.environment import load_pyramid_environment
42 42
43 43 import rhodecode.events
44 44 from rhodecode.lib.middleware.vcs import VCSMiddleware
45 45 from rhodecode.lib.request import Request
46 46 from rhodecode.lib.vcs import VCSCommunicationError
47 47 from rhodecode.lib.exceptions import VCSServerUnavailable
48 48 from rhodecode.lib.middleware.appenlight import wrap_in_appenlight_if_enabled
49 49 from rhodecode.lib.middleware.https_fixup import HttpsFixup
50 50 from rhodecode.lib.celerylib.loader import configure_celery
51 51 from rhodecode.lib.plugins.utils import register_rhodecode_plugin
52 52 from rhodecode.lib.utils2 import aslist as rhodecode_aslist, AttributeDict
53 53 from rhodecode.lib.exc_tracking import store_exception
54 54 from rhodecode.subscribers import (
55 55 scan_repositories_if_enabled, write_js_routes_if_enabled,
56 56 write_metadata_if_needed, inject_app_settings)
57 57
58 58
59 59 log = logging.getLogger(__name__)
60 60
61 61
62 62 def is_http_error(response):
63 63 # error which should have traceback
64 64 return response.status_code > 499
65 65
66 66
67 67 def should_load_all():
68 68 """
69 69 Returns if all application components should be loaded. In some cases it's
70 70 desired to skip apps loading for faster shell script execution
71 71 """
72 72 ssh_cmd = os.environ.get('RC_CMD_SSH_WRAPPER')
73 73 if ssh_cmd:
74 74 return False
75 75
76 76 return True
77 77
78 78
79 79 def make_pyramid_app(global_config, **settings):
80 80 """
81 81 Constructs the WSGI application based on Pyramid.
82 82
83 83 Specials:
84 84
85 85 * The application can also be integrated like a plugin via the call to
86 86 `includeme`. This is accompanied with the other utility functions which
87 87 are called. Changing this should be done with great care to not break
88 88 cases when these fragments are assembled from another place.
89 89
90 90 """
91 91
92 92 # Allows to use format style "{ENV_NAME}" placeholders in the configuration. It
93 93 # will be replaced by the value of the environment variable "NAME" in this case.
94 94 start_time = time.time()
95 95
96 96 debug = asbool(global_config.get('debug'))
97 97 if debug:
98 98 enable_debug()
99 99
100 100 environ = {'ENV_{}'.format(key): value for key, value in os.environ.items()}
101 101
102 102 global_config = _substitute_values(global_config, environ)
103 103 settings = _substitute_values(settings, environ)
104 104
105 105 sanitize_settings_and_apply_defaults(global_config, settings)
106 106
107 107 config = Configurator(settings=settings)
108 108
109 109 # Apply compatibility patches
110 110 patches.inspect_getargspec()
111 111
112 112 load_pyramid_environment(global_config, settings)
113 113
114 114 # Static file view comes first
115 115 includeme_first(config)
116 116
117 117 includeme(config)
118 118
119 119 pyramid_app = config.make_wsgi_app()
120 120 pyramid_app = wrap_app_in_wsgi_middlewares(pyramid_app, config)
121 121 pyramid_app.config = config
122 122
123 123 config.configure_celery(global_config['__file__'])
124 124 # creating the app uses a connection - return it after we are done
125 125 meta.Session.remove()
126 126 total_time = time.time() - start_time
127 127 log.info('Pyramid app `%s` created and configured in %.2fs',
128 128 pyramid_app.func_name, total_time)
129 129
130 130 return pyramid_app
131 131
132 132
133 133 def not_found_view(request):
134 134 """
135 135 This creates the view which should be registered as not-found-view to
136 136 pyramid.
137 137 """
138 138
139 139 if not getattr(request, 'vcs_call', None):
140 140 # handle like regular case with our error_handler
141 141 return error_handler(HTTPNotFound(), request)
142 142
143 143 # handle not found view as a vcs call
144 144 settings = request.registry.settings
145 145 ae_client = getattr(request, 'ae_client', None)
146 146 vcs_app = VCSMiddleware(
147 147 HTTPNotFound(), request.registry, settings,
148 148 appenlight_client=ae_client)
149 149
150 150 return wsgiapp(vcs_app)(None, request)
151 151
152 152
153 153 def error_handler(exception, request):
154 154 import rhodecode
155 155 from rhodecode.lib import helpers
156 156
157 157 rhodecode_title = rhodecode.CONFIG.get('rhodecode_title') or 'RhodeCode'
158 158
159 159 base_response = HTTPInternalServerError()
160 160 # prefer original exception for the response since it may have headers set
161 161 if isinstance(exception, HTTPException):
162 162 base_response = exception
163 163 elif isinstance(exception, VCSCommunicationError):
164 164 base_response = VCSServerUnavailable()
165 165
166 166 if is_http_error(base_response):
167 167 log.exception(
168 168 'error occurred handling this request for path: %s', request.path)
169 169
170 170 error_explanation = base_response.explanation or str(base_response)
171 171 if base_response.status_code == 404:
172 172 error_explanation += " Optionally you don't have permission to access this page."
173 173 c = AttributeDict()
174 174 c.error_message = base_response.status
175 175 c.error_explanation = error_explanation
176 176 c.visual = AttributeDict()
177 177
178 178 c.visual.rhodecode_support_url = (
179 179 request.registry.settings.get('rhodecode_support_url') or
180 180 request.route_url('rhodecode_support')
181 181 )
182 182 c.redirect_time = 0
183 183 c.rhodecode_name = rhodecode_title
184 184 if not c.rhodecode_name:
185 185 c.rhodecode_name = 'Rhodecode'
186 186
187 187 c.causes = []
188 188 if is_http_error(base_response):
189 189 c.causes.append('Server is overloaded.')
190 190 c.causes.append('Server database connection is lost.')
191 191 c.causes.append('Server expected unhandled error.')
192 192
193 193 if hasattr(base_response, 'causes'):
194 194 c.causes = base_response.causes
195 195
196 196 c.messages = helpers.flash.pop_messages(request=request)
197 197
198 198 exc_info = sys.exc_info()
199 199 c.exception_id = id(exc_info)
200 200 c.show_exception_id = isinstance(base_response, VCSServerUnavailable) \
201 201 or base_response.status_code > 499
202 202 c.exception_id_url = request.route_url(
203 203 'admin_settings_exception_tracker_show', exception_id=c.exception_id)
204 204
205 205 if c.show_exception_id:
206 206 store_exception(c.exception_id, exc_info)
207 207
208 208 response = render_to_response(
209 209 '/errors/error_document.mako', {'c': c, 'h': helpers}, request=request,
210 210 response=base_response)
211 211
212 212 return response
213 213
214 214
215 215 def includeme_first(config):
216 216 # redirect automatic browser favicon.ico requests to correct place
217 217 def favicon_redirect(context, request):
218 218 return HTTPFound(
219 219 request.static_path('rhodecode:public/images/favicon.ico'))
220 220
221 221 config.add_view(favicon_redirect, route_name='favicon')
222 222 config.add_route('favicon', '/favicon.ico')
223 223
224 224 def robots_redirect(context, request):
225 225 return HTTPFound(
226 226 request.static_path('rhodecode:public/robots.txt'))
227 227
228 228 config.add_view(robots_redirect, route_name='robots')
229 229 config.add_route('robots', '/robots.txt')
230 230
231 231 config.add_static_view(
232 232 '_static/deform', 'deform:static')
233 233 config.add_static_view(
234 234 '_static/rhodecode', path='rhodecode:public', cache_max_age=3600 * 24)
235 235
236 236
237 237 def includeme(config):
238 238 log.debug('Initializing main includeme from %s', os.path.basename(__file__))
239 239 settings = config.registry.settings
240 240 config.set_request_factory(Request)
241 241
242 242 # plugin information
243 243 config.registry.rhodecode_plugins = collections.OrderedDict()
244 244
245 245 config.add_directive(
246 246 'register_rhodecode_plugin', register_rhodecode_plugin)
247 247
248 248 config.add_directive('configure_celery', configure_celery)
249 249
250 250 if asbool(settings.get('appenlight', 'false')):
251 251 config.include('appenlight_client.ext.pyramid_tween')
252 252
253 253 load_all = should_load_all()
254 254
255 255 # Includes which are required. The application would fail without them.
256 256 config.include('pyramid_mako')
257 257 config.include('rhodecode.lib.rc_beaker')
258 258 config.include('rhodecode.lib.rc_cache')
259 259
260 260 config.include('rhodecode.apps._base.navigation')
261 261 config.include('rhodecode.apps._base.subscribers')
262 262 config.include('rhodecode.tweens')
263 263 config.include('rhodecode.authentication')
264 264
265 265 if load_all:
266 266 config.include('rhodecode.integrations')
267 267
268 268 if load_all:
269 269 from rhodecode.authentication import discover_legacy_plugins
270 270 # load CE authentication plugins
271 271 config.include('rhodecode.authentication.plugins.auth_crowd')
272 272 config.include('rhodecode.authentication.plugins.auth_headers')
273 273 config.include('rhodecode.authentication.plugins.auth_jasig_cas')
274 274 config.include('rhodecode.authentication.plugins.auth_ldap')
275 275 config.include('rhodecode.authentication.plugins.auth_pam')
276 276 config.include('rhodecode.authentication.plugins.auth_rhodecode')
277 277 config.include('rhodecode.authentication.plugins.auth_token')
278 278
279 279 # Auto discover authentication plugins and include their configuration.
280 280 discover_legacy_plugins(config)
281 281
282 282 # apps
283 283 if load_all:
284 284 config.include('rhodecode.apps._base')
285 config.include('rhodecode.apps.hovercards')
285 286 config.include('rhodecode.apps.ops')
286 287 config.include('rhodecode.apps.admin')
287 288 config.include('rhodecode.apps.channelstream')
288 289 config.include('rhodecode.apps.file_store')
289 290 config.include('rhodecode.apps.login')
290 291 config.include('rhodecode.apps.home')
291 292 config.include('rhodecode.apps.journal')
292 293 config.include('rhodecode.apps.repository')
293 294 config.include('rhodecode.apps.repo_group')
294 295 config.include('rhodecode.apps.user_group')
295 296 config.include('rhodecode.apps.search')
296 297 config.include('rhodecode.apps.user_profile')
297 298 config.include('rhodecode.apps.user_group_profile')
298 299 config.include('rhodecode.apps.my_account')
299 300 config.include('rhodecode.apps.svn_support')
300 301 config.include('rhodecode.apps.ssh_support')
301 302 config.include('rhodecode.apps.gist')
302 303 config.include('rhodecode.apps.debug_style')
303 304 config.include('rhodecode.api')
304 305
305 306 config.add_route('rhodecode_support', 'https://rhodecode.com/help/', static=True)
306 307 config.add_translation_dirs('rhodecode:i18n/')
307 308 settings['default_locale_name'] = settings.get('lang', 'en')
308 309
309 310 # Add subscribers.
310 311 if load_all:
311 312 config.add_subscriber(inject_app_settings,
312 313 pyramid.events.ApplicationCreated)
313 314 config.add_subscriber(scan_repositories_if_enabled,
314 315 pyramid.events.ApplicationCreated)
315 316 config.add_subscriber(write_metadata_if_needed,
316 317 pyramid.events.ApplicationCreated)
317 318 config.add_subscriber(write_js_routes_if_enabled,
318 319 pyramid.events.ApplicationCreated)
319 320
320 321 # request custom methods
321 322 config.add_request_method(
322 323 'rhodecode.lib.partial_renderer.get_partial_renderer',
323 324 'get_partial_renderer')
324 325
325 326 config.add_request_method(
326 327 'rhodecode.lib.request_counter.get_request_counter',
327 328 'request_count')
328 329
329 330 # Set the authorization policy.
330 331 authz_policy = ACLAuthorizationPolicy()
331 332 config.set_authorization_policy(authz_policy)
332 333
333 334 # Set the default renderer for HTML templates to mako.
334 335 config.add_mako_renderer('.html')
335 336
336 337 config.add_renderer(
337 338 name='json_ext',
338 339 factory='rhodecode.lib.ext_json_renderer.pyramid_ext_json')
339 340
340 341 # include RhodeCode plugins
341 342 includes = aslist(settings.get('rhodecode.includes', []))
342 343 for inc in includes:
343 344 config.include(inc)
344 345
345 346 # custom not found view, if our pyramid app doesn't know how to handle
346 347 # the request pass it to potential VCS handling ap
347 348 config.add_notfound_view(not_found_view)
348 349 if not settings.get('debugtoolbar.enabled', False):
349 350 # disabled debugtoolbar handle all exceptions via the error_handlers
350 351 config.add_view(error_handler, context=Exception)
351 352
352 353 # all errors including 403/404/50X
353 354 config.add_view(error_handler, context=HTTPError)
354 355
355 356
356 357 def wrap_app_in_wsgi_middlewares(pyramid_app, config):
357 358 """
358 359 Apply outer WSGI middlewares around the application.
359 360 """
360 361 registry = config.registry
361 362 settings = registry.settings
362 363
363 364 # enable https redirects based on HTTP_X_URL_SCHEME set by proxy
364 365 pyramid_app = HttpsFixup(pyramid_app, settings)
365 366
366 367 pyramid_app, _ae_client = wrap_in_appenlight_if_enabled(
367 368 pyramid_app, settings)
368 369 registry.ae_client = _ae_client
369 370
370 371 if settings['gzip_responses']:
371 372 pyramid_app = make_gzip_middleware(
372 373 pyramid_app, settings, compress_level=1)
373 374
374 375 # this should be the outer most middleware in the wsgi stack since
375 376 # middleware like Routes make database calls
376 377 def pyramid_app_with_cleanup(environ, start_response):
377 378 try:
378 379 return pyramid_app(environ, start_response)
379 380 finally:
380 381 # Dispose current database session and rollback uncommitted
381 382 # transactions.
382 383 meta.Session.remove()
383 384
384 385 # In a single threaded mode server, on non sqlite db we should have
385 386 # '0 Current Checked out connections' at the end of a request,
386 387 # if not, then something, somewhere is leaving a connection open
387 388 pool = meta.Base.metadata.bind.engine.pool
388 389 log.debug('sa pool status: %s', pool.status())
389 390 log.debug('Request processing finalized')
390 391
391 392 return pyramid_app_with_cleanup
392 393
393 394
394 395 def sanitize_settings_and_apply_defaults(global_config, settings):
395 396 """
396 397 Applies settings defaults and does all type conversion.
397 398
398 399 We would move all settings parsing and preparation into this place, so that
399 400 we have only one place left which deals with this part. The remaining parts
400 401 of the application would start to rely fully on well prepared settings.
401 402
402 403 This piece would later be split up per topic to avoid a big fat monster
403 404 function.
404 405 """
405 406
406 407 settings.setdefault('rhodecode.edition', 'Community Edition')
407 408
408 409 if 'mako.default_filters' not in settings:
409 410 # set custom default filters if we don't have it defined
410 411 settings['mako.imports'] = 'from rhodecode.lib.base import h_filter'
411 412 settings['mako.default_filters'] = 'h_filter'
412 413
413 414 if 'mako.directories' not in settings:
414 415 mako_directories = settings.setdefault('mako.directories', [
415 416 # Base templates of the original application
416 417 'rhodecode:templates',
417 418 ])
418 419 log.debug(
419 420 "Using the following Mako template directories: %s",
420 421 mako_directories)
421 422
422 423 # NOTE(marcink): fix redis requirement for schema of connection since 3.X
423 424 if 'beaker.session.type' in settings and settings['beaker.session.type'] == 'ext:redis':
424 425 raw_url = settings['beaker.session.url']
425 426 if not raw_url.startswith(('redis://', 'rediss://', 'unix://')):
426 427 settings['beaker.session.url'] = 'redis://' + raw_url
427 428
428 429 # Default includes, possible to change as a user
429 430 pyramid_includes = settings.setdefault('pyramid.includes', [
430 431 'rhodecode.lib.middleware.request_wrapper',
431 432 ])
432 433 log.debug(
433 434 "Using the following pyramid.includes: %s",
434 435 pyramid_includes)
435 436
436 437 # TODO: johbo: Re-think this, usually the call to config.include
437 438 # should allow to pass in a prefix.
438 439 settings.setdefault('rhodecode.api.url', '/_admin/api')
439 440 settings.setdefault('__file__', global_config.get('__file__'))
440 441
441 442 # Sanitize generic settings.
442 443 _list_setting(settings, 'default_encoding', 'UTF-8')
443 444 _bool_setting(settings, 'is_test', 'false')
444 445 _bool_setting(settings, 'gzip_responses', 'false')
445 446
446 447 # Call split out functions that sanitize settings for each topic.
447 448 _sanitize_appenlight_settings(settings)
448 449 _sanitize_vcs_settings(settings)
449 450 _sanitize_cache_settings(settings)
450 451
451 452 # configure instance id
452 453 config_utils.set_instance_id(settings)
453 454
454 455 return settings
455 456
456 457
457 458 def enable_debug():
458 459 """
459 460 Helper to enable debug on running instance
460 461 :return:
461 462 """
462 463 import tempfile
463 464 import textwrap
464 465 import logging.config
465 466
466 467 ini_template = textwrap.dedent("""
467 468 #####################################
468 469 ### DEBUG LOGGING CONFIGURATION ####
469 470 #####################################
470 471 [loggers]
471 472 keys = root, sqlalchemy, beaker, celery, rhodecode, ssh_wrapper
472 473
473 474 [handlers]
474 475 keys = console, console_sql
475 476
476 477 [formatters]
477 478 keys = generic, color_formatter, color_formatter_sql
478 479
479 480 #############
480 481 ## LOGGERS ##
481 482 #############
482 483 [logger_root]
483 484 level = NOTSET
484 485 handlers = console
485 486
486 487 [logger_sqlalchemy]
487 488 level = INFO
488 489 handlers = console_sql
489 490 qualname = sqlalchemy.engine
490 491 propagate = 0
491 492
492 493 [logger_beaker]
493 494 level = DEBUG
494 495 handlers =
495 496 qualname = beaker.container
496 497 propagate = 1
497 498
498 499 [logger_rhodecode]
499 500 level = DEBUG
500 501 handlers =
501 502 qualname = rhodecode
502 503 propagate = 1
503 504
504 505 [logger_ssh_wrapper]
505 506 level = DEBUG
506 507 handlers =
507 508 qualname = ssh_wrapper
508 509 propagate = 1
509 510
510 511 [logger_celery]
511 512 level = DEBUG
512 513 handlers =
513 514 qualname = celery
514 515
515 516
516 517 ##############
517 518 ## HANDLERS ##
518 519 ##############
519 520
520 521 [handler_console]
521 522 class = StreamHandler
522 523 args = (sys.stderr, )
523 524 level = DEBUG
524 525 formatter = color_formatter
525 526
526 527 [handler_console_sql]
527 528 # "level = DEBUG" logs SQL queries and results.
528 529 # "level = INFO" logs SQL queries.
529 530 # "level = WARN" logs neither. (Recommended for production systems.)
530 531 class = StreamHandler
531 532 args = (sys.stderr, )
532 533 level = WARN
533 534 formatter = color_formatter_sql
534 535
535 536 ################
536 537 ## FORMATTERS ##
537 538 ################
538 539
539 540 [formatter_generic]
540 541 class = rhodecode.lib.logging_formatter.ExceptionAwareFormatter
541 542 format = %(asctime)s.%(msecs)03d [%(process)d] %(levelname)-5.5s [%(name)s] %(message)s | %(req_id)s
542 543 datefmt = %Y-%m-%d %H:%M:%S
543 544
544 545 [formatter_color_formatter]
545 546 class = rhodecode.lib.logging_formatter.ColorRequestTrackingFormatter
546 547 format = %(asctime)s.%(msecs)03d [%(process)d] %(levelname)-5.5s [%(name)s] %(message)s | %(req_id)s
547 548 datefmt = %Y-%m-%d %H:%M:%S
548 549
549 550 [formatter_color_formatter_sql]
550 551 class = rhodecode.lib.logging_formatter.ColorFormatterSql
551 552 format = %(asctime)s.%(msecs)03d [%(process)d] %(levelname)-5.5s [%(name)s] %(message)s
552 553 datefmt = %Y-%m-%d %H:%M:%S
553 554 """)
554 555
555 556 with tempfile.NamedTemporaryFile(prefix='rc_debug_logging_', suffix='.ini',
556 557 delete=False) as f:
557 558 log.info('Saved Temporary DEBUG config at %s', f.name)
558 559 f.write(ini_template)
559 560
560 561 logging.config.fileConfig(f.name)
561 562 log.debug('DEBUG MODE ON')
562 563 os.remove(f.name)
563 564
564 565
565 566 def _sanitize_appenlight_settings(settings):
566 567 _bool_setting(settings, 'appenlight', 'false')
567 568
568 569
569 570 def _sanitize_vcs_settings(settings):
570 571 """
571 572 Applies settings defaults and does type conversion for all VCS related
572 573 settings.
573 574 """
574 575 _string_setting(settings, 'vcs.svn.compatible_version', '')
575 576 _string_setting(settings, 'vcs.hooks.protocol', 'http')
576 577 _string_setting(settings, 'vcs.hooks.host', '127.0.0.1')
577 578 _string_setting(settings, 'vcs.scm_app_implementation', 'http')
578 579 _string_setting(settings, 'vcs.server', '')
579 580 _string_setting(settings, 'vcs.server.log_level', 'debug')
580 581 _string_setting(settings, 'vcs.server.protocol', 'http')
581 582 _bool_setting(settings, 'startup.import_repos', 'false')
582 583 _bool_setting(settings, 'vcs.hooks.direct_calls', 'false')
583 584 _bool_setting(settings, 'vcs.server.enable', 'true')
584 585 _bool_setting(settings, 'vcs.start_server', 'false')
585 586 _list_setting(settings, 'vcs.backends', 'hg, git, svn')
586 587 _int_setting(settings, 'vcs.connection_timeout', 3600)
587 588
588 589 # Support legacy values of vcs.scm_app_implementation. Legacy
589 590 # configurations may use 'rhodecode.lib.middleware.utils.scm_app_http', or
590 591 # disabled since 4.13 'vcsserver.scm_app' which is now mapped to 'http'.
591 592 scm_app_impl = settings['vcs.scm_app_implementation']
592 593 if scm_app_impl in ['rhodecode.lib.middleware.utils.scm_app_http', 'vcsserver.scm_app']:
593 594 settings['vcs.scm_app_implementation'] = 'http'
594 595
595 596
596 597 def _sanitize_cache_settings(settings):
597 598 temp_store = tempfile.gettempdir()
598 599 default_cache_dir = os.path.join(temp_store, 'rc_cache')
599 600
600 601 # save default, cache dir, and use it for all backends later.
601 602 default_cache_dir = _string_setting(
602 603 settings,
603 604 'cache_dir',
604 605 default_cache_dir, lower=False, default_when_empty=True)
605 606
606 607 # ensure we have our dir created
607 608 if not os.path.isdir(default_cache_dir):
608 609 os.makedirs(default_cache_dir, mode=0o755)
609 610
610 611 # exception store cache
611 612 _string_setting(
612 613 settings,
613 614 'exception_tracker.store_path',
614 615 temp_store, lower=False, default_when_empty=True)
615 616
616 617 # cache_perms
617 618 _string_setting(
618 619 settings,
619 620 'rc_cache.cache_perms.backend',
620 621 'dogpile.cache.rc.file_namespace', lower=False)
621 622 _int_setting(
622 623 settings,
623 624 'rc_cache.cache_perms.expiration_time',
624 625 60)
625 626 _string_setting(
626 627 settings,
627 628 'rc_cache.cache_perms.arguments.filename',
628 629 os.path.join(default_cache_dir, 'rc_cache_1'), lower=False)
629 630
630 631 # cache_repo
631 632 _string_setting(
632 633 settings,
633 634 'rc_cache.cache_repo.backend',
634 635 'dogpile.cache.rc.file_namespace', lower=False)
635 636 _int_setting(
636 637 settings,
637 638 'rc_cache.cache_repo.expiration_time',
638 639 60)
639 640 _string_setting(
640 641 settings,
641 642 'rc_cache.cache_repo.arguments.filename',
642 643 os.path.join(default_cache_dir, 'rc_cache_2'), lower=False)
643 644
644 645 # cache_license
645 646 _string_setting(
646 647 settings,
647 648 'rc_cache.cache_license.backend',
648 649 'dogpile.cache.rc.file_namespace', lower=False)
649 650 _int_setting(
650 651 settings,
651 652 'rc_cache.cache_license.expiration_time',
652 653 5*60)
653 654 _string_setting(
654 655 settings,
655 656 'rc_cache.cache_license.arguments.filename',
656 657 os.path.join(default_cache_dir, 'rc_cache_3'), lower=False)
657 658
658 659 # cache_repo_longterm memory, 96H
659 660 _string_setting(
660 661 settings,
661 662 'rc_cache.cache_repo_longterm.backend',
662 663 'dogpile.cache.rc.memory_lru', lower=False)
663 664 _int_setting(
664 665 settings,
665 666 'rc_cache.cache_repo_longterm.expiration_time',
666 667 345600)
667 668 _int_setting(
668 669 settings,
669 670 'rc_cache.cache_repo_longterm.max_size',
670 671 10000)
671 672
672 673 # sql_cache_short
673 674 _string_setting(
674 675 settings,
675 676 'rc_cache.sql_cache_short.backend',
676 677 'dogpile.cache.rc.memory_lru', lower=False)
677 678 _int_setting(
678 679 settings,
679 680 'rc_cache.sql_cache_short.expiration_time',
680 681 30)
681 682 _int_setting(
682 683 settings,
683 684 'rc_cache.sql_cache_short.max_size',
684 685 10000)
685 686
686 687
687 688 def _int_setting(settings, name, default):
688 689 settings[name] = int(settings.get(name, default))
689 690 return settings[name]
690 691
691 692
692 693 def _bool_setting(settings, name, default):
693 694 input_val = settings.get(name, default)
694 695 if isinstance(input_val, unicode):
695 696 input_val = input_val.encode('utf8')
696 697 settings[name] = asbool(input_val)
697 698 return settings[name]
698 699
699 700
700 701 def _list_setting(settings, name, default):
701 702 raw_value = settings.get(name, default)
702 703
703 704 old_separator = ','
704 705 if old_separator in raw_value:
705 706 # If we get a comma separated list, pass it to our own function.
706 707 settings[name] = rhodecode_aslist(raw_value, sep=old_separator)
707 708 else:
708 709 # Otherwise we assume it uses pyramids space/newline separation.
709 710 settings[name] = aslist(raw_value)
710 711 return settings[name]
711 712
712 713
713 714 def _string_setting(settings, name, default, lower=True, default_when_empty=False):
714 715 value = settings.get(name, default)
715 716
716 717 if default_when_empty and not value:
717 718 # use default value when value is empty
718 719 value = default
719 720
720 721 if lower:
721 722 value = value.lower()
722 723 settings[name] = value
723 724 return settings[name]
724 725
725 726
726 727 def _substitute_values(mapping, substitutions):
727 728 result = {}
728 729
729 730 try:
730 731 for key, value in mapping.items():
731 732 # initialize without substitution first
732 733 result[key] = value
733 734
734 735 # Note: Cannot use regular replacements, since they would clash
735 736 # with the implementation of ConfigParser. Using "format" instead.
736 737 try:
737 738 result[key] = value.format(**substitutions)
738 739 except KeyError as e:
739 740 env_var = '{}'.format(e.args[0])
740 741
741 742 msg = 'Failed to substitute: `{key}={{{var}}}` with environment entry. ' \
742 743 'Make sure your environment has {var} set, or remove this ' \
743 744 'variable from config file'.format(key=key, var=env_var)
744 745
745 746 if env_var.startswith('ENV_'):
746 747 raise ValueError(msg)
747 748 else:
748 749 log.warning(msg)
749 750
750 751 except ValueError as e:
751 752 log.warning('Failed to substitute ENV variable: %s', e)
752 753 result = mapping
753 754
754 755 return result
@@ -1,2089 +1,2089 b''
1 1 # -*- coding: utf-8 -*-
2 2
3 3 # Copyright (C) 2010-2019 RhodeCode GmbH
4 4 #
5 5 # This program is free software: you can redistribute it and/or modify
6 6 # it under the terms of the GNU Affero General Public License, version 3
7 7 # (only), as published by the Free Software Foundation.
8 8 #
9 9 # This program is distributed in the hope that it will be useful,
10 10 # but WITHOUT ANY WARRANTY; without even the implied warranty of
11 11 # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
12 12 # GNU General Public License for more details.
13 13 #
14 14 # You should have received a copy of the GNU Affero General Public License
15 15 # along with this program. If not, see <http://www.gnu.org/licenses/>.
16 16 #
17 17 # This program is dual-licensed. If you wish to learn more about the
18 18 # RhodeCode Enterprise Edition, including its added features, Support services,
19 19 # and proprietary license terms, please see https://rhodecode.com/licenses/
20 20
21 21 """
22 22 Helper functions
23 23
24 24 Consists of functions to typically be used within templates, but also
25 25 available to Controllers. This module is available to both as 'h'.
26 26 """
27 27
28 28 import os
29 29 import random
30 30 import hashlib
31 31 import StringIO
32 32 import textwrap
33 33 import urllib
34 34 import math
35 35 import logging
36 36 import re
37 37 import time
38 38 import string
39 39 import hashlib
40 40 from collections import OrderedDict
41 41
42 42 import pygments
43 43 import itertools
44 44 import fnmatch
45 45 import bleach
46 46
47 47 from pyramid import compat
48 48 from datetime import datetime
49 49 from functools import partial
50 50 from pygments.formatters.html import HtmlFormatter
51 51 from pygments.lexers import (
52 52 get_lexer_by_name, get_lexer_for_filename, get_lexer_for_mimetype)
53 53
54 54 from pyramid.threadlocal import get_current_request
55 55
56 56 from webhelpers.html import literal, HTML, escape
57 57 from webhelpers.html.tools import *
58 58 from webhelpers.html.builder import make_tag
59 59 from webhelpers.html.tags import auto_discovery_link, checkbox, css_classes, \
60 60 end_form, file, form as wh_form, hidden, image, javascript_link, link_to, \
61 61 link_to_if, link_to_unless, ol, required_legend, select, stylesheet_link, \
62 62 submit, text, password, textarea, title, ul, xml_declaration, radio
63 63 from webhelpers.html.tools import auto_link, button_to, highlight, \
64 64 js_obfuscate, mail_to, strip_links, strip_tags, tag_re
65 65 from webhelpers.text import chop_at, collapse, convert_accented_entities, \
66 66 convert_misc_entities, lchop, plural, rchop, remove_formatting, \
67 67 replace_whitespace, urlify, truncate, wrap_paragraphs
68 68 from webhelpers.date import time_ago_in_words
69 69 from webhelpers.paginate import Page as _Page
70 70 from webhelpers.html.tags import _set_input_attrs, _set_id_attr, \
71 71 convert_boolean_attrs, NotGiven, _make_safe_id_component
72 72 from webhelpers2.number import format_byte_size
73 73
74 74 from rhodecode.lib.action_parser import action_parser
75 75 from rhodecode.lib.ext_json import json
76 76 from rhodecode.lib.utils import repo_name_slug, get_custom_lexer
77 77 from rhodecode.lib.utils2 import str2bool, safe_unicode, safe_str, \
78 78 get_commit_safe, datetime_to_time, time_to_datetime, time_to_utcdatetime, \
79 79 AttributeDict, safe_int, md5, md5_safe
80 80 from rhodecode.lib.markup_renderer import MarkupRenderer, relative_links
81 81 from rhodecode.lib.vcs.exceptions import CommitDoesNotExistError
82 82 from rhodecode.lib.vcs.backends.base import BaseChangeset, EmptyCommit
83 83 from rhodecode.lib.index.search_utils import get_matching_line_offsets
84 84 from rhodecode.config.conf import DATE_FORMAT, DATETIME_FORMAT
85 85 from rhodecode.model.changeset_status import ChangesetStatusModel
86 86 from rhodecode.model.db import Permission, User, Repository
87 87 from rhodecode.model.repo_group import RepoGroupModel
88 88 from rhodecode.model.settings import IssueTrackerSettingsModel
89 89
90 90
91 91 log = logging.getLogger(__name__)
92 92
93 93
94 94 DEFAULT_USER = User.DEFAULT_USER
95 95 DEFAULT_USER_EMAIL = User.DEFAULT_USER_EMAIL
96 96
97 97
98 98 def asset(path, ver=None, **kwargs):
99 99 """
100 100 Helper to generate a static asset file path for rhodecode assets
101 101
102 102 eg. h.asset('images/image.png', ver='3923')
103 103
104 104 :param path: path of asset
105 105 :param ver: optional version query param to append as ?ver=
106 106 """
107 107 request = get_current_request()
108 108 query = {}
109 109 query.update(kwargs)
110 110 if ver:
111 111 query = {'ver': ver}
112 112 return request.static_path(
113 113 'rhodecode:public/{}'.format(path), _query=query)
114 114
115 115
116 116 default_html_escape_table = {
117 117 ord('&'): u'&amp;',
118 118 ord('<'): u'&lt;',
119 119 ord('>'): u'&gt;',
120 120 ord('"'): u'&quot;',
121 121 ord("'"): u'&#39;',
122 122 }
123 123
124 124
125 125 def html_escape(text, html_escape_table=default_html_escape_table):
126 126 """Produce entities within text."""
127 127 return text.translate(html_escape_table)
128 128
129 129
130 130 def chop_at_smart(s, sub, inclusive=False, suffix_if_chopped=None):
131 131 """
132 132 Truncate string ``s`` at the first occurrence of ``sub``.
133 133
134 134 If ``inclusive`` is true, truncate just after ``sub`` rather than at it.
135 135 """
136 136 suffix_if_chopped = suffix_if_chopped or ''
137 137 pos = s.find(sub)
138 138 if pos == -1:
139 139 return s
140 140
141 141 if inclusive:
142 142 pos += len(sub)
143 143
144 144 chopped = s[:pos]
145 145 left = s[pos:].strip()
146 146
147 147 if left and suffix_if_chopped:
148 148 chopped += suffix_if_chopped
149 149
150 150 return chopped
151 151
152 152
153 153 def shorter(text, size=20, prefix=False):
154 154 postfix = '...'
155 155 if len(text) > size:
156 156 if prefix:
157 157 # shorten in front
158 158 return postfix + text[-(size - len(postfix)):]
159 159 else:
160 160 return text[:size - len(postfix)] + postfix
161 161 return text
162 162
163 163
164 164 def _reset(name, value=None, id=NotGiven, type="reset", **attrs):
165 165 """
166 166 Reset button
167 167 """
168 168 _set_input_attrs(attrs, type, name, value)
169 169 _set_id_attr(attrs, id, name)
170 170 convert_boolean_attrs(attrs, ["disabled"])
171 171 return HTML.input(**attrs)
172 172
173 173 reset = _reset
174 174 safeid = _make_safe_id_component
175 175
176 176
177 177 def branding(name, length=40):
178 178 return truncate(name, length, indicator="")
179 179
180 180
181 181 def FID(raw_id, path):
182 182 """
183 183 Creates a unique ID for filenode based on it's hash of path and commit
184 184 it's safe to use in urls
185 185
186 186 :param raw_id:
187 187 :param path:
188 188 """
189 189
190 190 return 'c-%s-%s' % (short_id(raw_id), md5_safe(path)[:12])
191 191
192 192
193 193 class _GetError(object):
194 194 """Get error from form_errors, and represent it as span wrapped error
195 195 message
196 196
197 197 :param field_name: field to fetch errors for
198 198 :param form_errors: form errors dict
199 199 """
200 200
201 201 def __call__(self, field_name, form_errors):
202 202 tmpl = """<span class="error_msg">%s</span>"""
203 203 if form_errors and field_name in form_errors:
204 204 return literal(tmpl % form_errors.get(field_name))
205 205
206 206
207 207 get_error = _GetError()
208 208
209 209
210 210 class _ToolTip(object):
211 211
212 212 def __call__(self, tooltip_title, trim_at=50):
213 213 """
214 214 Special function just to wrap our text into nice formatted
215 215 autowrapped text
216 216
217 217 :param tooltip_title:
218 218 """
219 219 tooltip_title = escape(tooltip_title)
220 220 tooltip_title = tooltip_title.replace('<', '&lt;').replace('>', '&gt;')
221 221 return tooltip_title
222 222
223 223
224 224 tooltip = _ToolTip()
225 225
226 226 files_icon = u'<i class="file-breadcrumb-copy tooltip icon-clipboard clipboard-action" data-clipboard-text="{}" title="Copy the full path"></i>'
227 227
228 228
229 229 def files_breadcrumbs(repo_name, commit_id, file_path, at_ref=None, limit_items=False, linkify_last_item=False):
230 230 if isinstance(file_path, str):
231 231 file_path = safe_unicode(file_path)
232 232
233 233 route_qry = {'at': at_ref} if at_ref else None
234 234
235 235 # first segment is a `..` link to repo files
236 236 root_name = literal(u'<i class="icon-home"></i>')
237 237 url_segments = [
238 238 link_to(
239 239 root_name,
240 240 route_path(
241 241 'repo_files',
242 242 repo_name=repo_name,
243 243 commit_id=commit_id,
244 244 f_path='',
245 245 _query=route_qry),
246 246 )]
247 247
248 248 path_segments = file_path.split('/')
249 249 last_cnt = len(path_segments) - 1
250 250 for cnt, segment in enumerate(path_segments):
251 251 if not segment:
252 252 continue
253 253 segment_html = escape(segment)
254 254
255 255 last_item = cnt == last_cnt
256 256
257 257 if last_item and linkify_last_item is False:
258 258 # plain version
259 259 url_segments.append(segment_html)
260 260 else:
261 261 url_segments.append(
262 262 link_to(
263 263 segment_html,
264 264 route_path(
265 265 'repo_files',
266 266 repo_name=repo_name,
267 267 commit_id=commit_id,
268 268 f_path='/'.join(path_segments[:cnt + 1]),
269 269 _query=route_qry),
270 270 ))
271 271
272 272 limited_url_segments = url_segments[:1] + ['...'] + url_segments[-5:]
273 273 if limit_items and len(limited_url_segments) < len(url_segments):
274 274 url_segments = limited_url_segments
275 275
276 276 full_path = file_path
277 277 icon = files_icon.format(escape(full_path))
278 278 if file_path == '':
279 279 return root_name
280 280 else:
281 281 return literal(' / '.join(url_segments) + icon)
282 282
283 283
284 284 def files_url_data(request):
285 285 matchdict = request.matchdict
286 286
287 287 if 'f_path' not in matchdict:
288 288 matchdict['f_path'] = ''
289 289
290 290 if 'commit_id' not in matchdict:
291 291 matchdict['commit_id'] = 'tip'
292 292
293 293 return json.dumps(matchdict)
294 294
295 295
296 296 def code_highlight(code, lexer, formatter, use_hl_filter=False):
297 297 """
298 298 Lex ``code`` with ``lexer`` and format it with the formatter ``formatter``.
299 299
300 300 If ``outfile`` is given and a valid file object (an object
301 301 with a ``write`` method), the result will be written to it, otherwise
302 302 it is returned as a string.
303 303 """
304 304 if use_hl_filter:
305 305 # add HL filter
306 306 from rhodecode.lib.index import search_utils
307 307 lexer.add_filter(search_utils.ElasticSearchHLFilter())
308 308 return pygments.format(pygments.lex(code, lexer), formatter)
309 309
310 310
311 311 class CodeHtmlFormatter(HtmlFormatter):
312 312 """
313 313 My code Html Formatter for source codes
314 314 """
315 315
316 316 def wrap(self, source, outfile):
317 317 return self._wrap_div(self._wrap_pre(self._wrap_code(source)))
318 318
319 319 def _wrap_code(self, source):
320 320 for cnt, it in enumerate(source):
321 321 i, t = it
322 322 t = '<div id="L%s">%s</div>' % (cnt + 1, t)
323 323 yield i, t
324 324
325 325 def _wrap_tablelinenos(self, inner):
326 326 dummyoutfile = StringIO.StringIO()
327 327 lncount = 0
328 328 for t, line in inner:
329 329 if t:
330 330 lncount += 1
331 331 dummyoutfile.write(line)
332 332
333 333 fl = self.linenostart
334 334 mw = len(str(lncount + fl - 1))
335 335 sp = self.linenospecial
336 336 st = self.linenostep
337 337 la = self.lineanchors
338 338 aln = self.anchorlinenos
339 339 nocls = self.noclasses
340 340 if sp:
341 341 lines = []
342 342
343 343 for i in range(fl, fl + lncount):
344 344 if i % st == 0:
345 345 if i % sp == 0:
346 346 if aln:
347 347 lines.append('<a href="#%s%d" class="special">%*d</a>' %
348 348 (la, i, mw, i))
349 349 else:
350 350 lines.append('<span class="special">%*d</span>' % (mw, i))
351 351 else:
352 352 if aln:
353 353 lines.append('<a href="#%s%d">%*d</a>' % (la, i, mw, i))
354 354 else:
355 355 lines.append('%*d' % (mw, i))
356 356 else:
357 357 lines.append('')
358 358 ls = '\n'.join(lines)
359 359 else:
360 360 lines = []
361 361 for i in range(fl, fl + lncount):
362 362 if i % st == 0:
363 363 if aln:
364 364 lines.append('<a href="#%s%d">%*d</a>' % (la, i, mw, i))
365 365 else:
366 366 lines.append('%*d' % (mw, i))
367 367 else:
368 368 lines.append('')
369 369 ls = '\n'.join(lines)
370 370
371 371 # in case you wonder about the seemingly redundant <div> here: since the
372 372 # content in the other cell also is wrapped in a div, some browsers in
373 373 # some configurations seem to mess up the formatting...
374 374 if nocls:
375 375 yield 0, ('<table class="%stable">' % self.cssclass +
376 376 '<tr><td><div class="linenodiv" '
377 377 'style="background-color: #f0f0f0; padding-right: 10px">'
378 378 '<pre style="line-height: 125%">' +
379 379 ls + '</pre></div></td><td id="hlcode" class="code">')
380 380 else:
381 381 yield 0, ('<table class="%stable">' % self.cssclass +
382 382 '<tr><td class="linenos"><div class="linenodiv"><pre>' +
383 383 ls + '</pre></div></td><td id="hlcode" class="code">')
384 384 yield 0, dummyoutfile.getvalue()
385 385 yield 0, '</td></tr></table>'
386 386
387 387
388 388 class SearchContentCodeHtmlFormatter(CodeHtmlFormatter):
389 389 def __init__(self, **kw):
390 390 # only show these line numbers if set
391 391 self.only_lines = kw.pop('only_line_numbers', [])
392 392 self.query_terms = kw.pop('query_terms', [])
393 393 self.max_lines = kw.pop('max_lines', 5)
394 394 self.line_context = kw.pop('line_context', 3)
395 395 self.url = kw.pop('url', None)
396 396
397 397 super(CodeHtmlFormatter, self).__init__(**kw)
398 398
399 399 def _wrap_code(self, source):
400 400 for cnt, it in enumerate(source):
401 401 i, t = it
402 402 t = '<pre>%s</pre>' % t
403 403 yield i, t
404 404
405 405 def _wrap_tablelinenos(self, inner):
406 406 yield 0, '<table class="code-highlight %stable">' % self.cssclass
407 407
408 408 last_shown_line_number = 0
409 409 current_line_number = 1
410 410
411 411 for t, line in inner:
412 412 if not t:
413 413 yield t, line
414 414 continue
415 415
416 416 if current_line_number in self.only_lines:
417 417 if last_shown_line_number + 1 != current_line_number:
418 418 yield 0, '<tr>'
419 419 yield 0, '<td class="line">...</td>'
420 420 yield 0, '<td id="hlcode" class="code"></td>'
421 421 yield 0, '</tr>'
422 422
423 423 yield 0, '<tr>'
424 424 if self.url:
425 425 yield 0, '<td class="line"><a href="%s#L%i">%i</a></td>' % (
426 426 self.url, current_line_number, current_line_number)
427 427 else:
428 428 yield 0, '<td class="line"><a href="">%i</a></td>' % (
429 429 current_line_number)
430 430 yield 0, '<td id="hlcode" class="code">' + line + '</td>'
431 431 yield 0, '</tr>'
432 432
433 433 last_shown_line_number = current_line_number
434 434
435 435 current_line_number += 1
436 436
437 437 yield 0, '</table>'
438 438
439 439
440 440 def hsv_to_rgb(h, s, v):
441 441 """ Convert hsv color values to rgb """
442 442
443 443 if s == 0.0:
444 444 return v, v, v
445 445 i = int(h * 6.0) # XXX assume int() truncates!
446 446 f = (h * 6.0) - i
447 447 p = v * (1.0 - s)
448 448 q = v * (1.0 - s * f)
449 449 t = v * (1.0 - s * (1.0 - f))
450 450 i = i % 6
451 451 if i == 0:
452 452 return v, t, p
453 453 if i == 1:
454 454 return q, v, p
455 455 if i == 2:
456 456 return p, v, t
457 457 if i == 3:
458 458 return p, q, v
459 459 if i == 4:
460 460 return t, p, v
461 461 if i == 5:
462 462 return v, p, q
463 463
464 464
465 465 def unique_color_generator(n=10000, saturation=0.10, lightness=0.95):
466 466 """
467 467 Generator for getting n of evenly distributed colors using
468 468 hsv color and golden ratio. It always return same order of colors
469 469
470 470 :param n: number of colors to generate
471 471 :param saturation: saturation of returned colors
472 472 :param lightness: lightness of returned colors
473 473 :returns: RGB tuple
474 474 """
475 475
476 476 golden_ratio = 0.618033988749895
477 477 h = 0.22717784590367374
478 478
479 479 for _ in xrange(n):
480 480 h += golden_ratio
481 481 h %= 1
482 482 HSV_tuple = [h, saturation, lightness]
483 483 RGB_tuple = hsv_to_rgb(*HSV_tuple)
484 484 yield map(lambda x: str(int(x * 256)), RGB_tuple)
485 485
486 486
487 487 def color_hasher(n=10000, saturation=0.10, lightness=0.95):
488 488 """
489 489 Returns a function which when called with an argument returns a unique
490 490 color for that argument, eg.
491 491
492 492 :param n: number of colors to generate
493 493 :param saturation: saturation of returned colors
494 494 :param lightness: lightness of returned colors
495 495 :returns: css RGB string
496 496
497 497 >>> color_hash = color_hasher()
498 498 >>> color_hash('hello')
499 499 'rgb(34, 12, 59)'
500 500 >>> color_hash('hello')
501 501 'rgb(34, 12, 59)'
502 502 >>> color_hash('other')
503 503 'rgb(90, 224, 159)'
504 504 """
505 505
506 506 color_dict = {}
507 507 cgenerator = unique_color_generator(
508 508 saturation=saturation, lightness=lightness)
509 509
510 510 def get_color_string(thing):
511 511 if thing in color_dict:
512 512 col = color_dict[thing]
513 513 else:
514 514 col = color_dict[thing] = cgenerator.next()
515 515 return "rgb(%s)" % (', '.join(col))
516 516
517 517 return get_color_string
518 518
519 519
520 520 def get_lexer_safe(mimetype=None, filepath=None):
521 521 """
522 522 Tries to return a relevant pygments lexer using mimetype/filepath name,
523 523 defaulting to plain text if none could be found
524 524 """
525 525 lexer = None
526 526 try:
527 527 if mimetype:
528 528 lexer = get_lexer_for_mimetype(mimetype)
529 529 if not lexer:
530 530 lexer = get_lexer_for_filename(filepath)
531 531 except pygments.util.ClassNotFound:
532 532 pass
533 533
534 534 if not lexer:
535 535 lexer = get_lexer_by_name('text')
536 536
537 537 return lexer
538 538
539 539
540 540 def get_lexer_for_filenode(filenode):
541 541 lexer = get_custom_lexer(filenode.extension) or filenode.lexer
542 542 return lexer
543 543
544 544
545 545 def pygmentize(filenode, **kwargs):
546 546 """
547 547 pygmentize function using pygments
548 548
549 549 :param filenode:
550 550 """
551 551 lexer = get_lexer_for_filenode(filenode)
552 552 return literal(code_highlight(filenode.content, lexer,
553 553 CodeHtmlFormatter(**kwargs)))
554 554
555 555
556 556 def is_following_repo(repo_name, user_id):
557 557 from rhodecode.model.scm import ScmModel
558 558 return ScmModel().is_following_repo(repo_name, user_id)
559 559
560 560
561 561 class _Message(object):
562 562 """A message returned by ``Flash.pop_messages()``.
563 563
564 564 Converting the message to a string returns the message text. Instances
565 565 also have the following attributes:
566 566
567 567 * ``message``: the message text.
568 568 * ``category``: the category specified when the message was created.
569 569 """
570 570
571 571 def __init__(self, category, message):
572 572 self.category = category
573 573 self.message = message
574 574
575 575 def __str__(self):
576 576 return self.message
577 577
578 578 __unicode__ = __str__
579 579
580 580 def __html__(self):
581 581 return escape(safe_unicode(self.message))
582 582
583 583
584 584 class Flash(object):
585 585 # List of allowed categories. If None, allow any category.
586 586 categories = ["warning", "notice", "error", "success"]
587 587
588 588 # Default category if none is specified.
589 589 default_category = "notice"
590 590
591 591 def __init__(self, session_key="flash", categories=None,
592 592 default_category=None):
593 593 """
594 594 Instantiate a ``Flash`` object.
595 595
596 596 ``session_key`` is the key to save the messages under in the user's
597 597 session.
598 598
599 599 ``categories`` is an optional list which overrides the default list
600 600 of categories.
601 601
602 602 ``default_category`` overrides the default category used for messages
603 603 when none is specified.
604 604 """
605 605 self.session_key = session_key
606 606 if categories is not None:
607 607 self.categories = categories
608 608 if default_category is not None:
609 609 self.default_category = default_category
610 610 if self.categories and self.default_category not in self.categories:
611 611 raise ValueError(
612 612 "unrecognized default category %r" % (self.default_category,))
613 613
614 614 def pop_messages(self, session=None, request=None):
615 615 """
616 616 Return all accumulated messages and delete them from the session.
617 617
618 618 The return value is a list of ``Message`` objects.
619 619 """
620 620 messages = []
621 621
622 622 if not session:
623 623 if not request:
624 624 request = get_current_request()
625 625 session = request.session
626 626
627 627 # Pop the 'old' pylons flash messages. They are tuples of the form
628 628 # (category, message)
629 629 for cat, msg in session.pop(self.session_key, []):
630 630 messages.append(_Message(cat, msg))
631 631
632 632 # Pop the 'new' pyramid flash messages for each category as list
633 633 # of strings.
634 634 for cat in self.categories:
635 635 for msg in session.pop_flash(queue=cat):
636 636 messages.append(_Message(cat, msg))
637 637 # Map messages from the default queue to the 'notice' category.
638 638 for msg in session.pop_flash():
639 639 messages.append(_Message('notice', msg))
640 640
641 641 session.save()
642 642 return messages
643 643
644 644 def json_alerts(self, session=None, request=None):
645 645 payloads = []
646 646 messages = flash.pop_messages(session=session, request=request)
647 647 if messages:
648 648 for message in messages:
649 649 subdata = {}
650 650 if hasattr(message.message, 'rsplit'):
651 651 flash_data = message.message.rsplit('|DELIM|', 1)
652 652 org_message = flash_data[0]
653 653 if len(flash_data) > 1:
654 654 subdata = json.loads(flash_data[1])
655 655 else:
656 656 org_message = message.message
657 657 payloads.append({
658 658 'message': {
659 659 'message': u'{}'.format(org_message),
660 660 'level': message.category,
661 661 'force': True,
662 662 'subdata': subdata
663 663 }
664 664 })
665 665 return json.dumps(payloads)
666 666
667 667 def __call__(self, message, category=None, ignore_duplicate=True,
668 668 session=None, request=None):
669 669
670 670 if not session:
671 671 if not request:
672 672 request = get_current_request()
673 673 session = request.session
674 674
675 675 session.flash(
676 676 message, queue=category, allow_duplicate=not ignore_duplicate)
677 677
678 678
679 679 flash = Flash()
680 680
681 681 #==============================================================================
682 682 # SCM FILTERS available via h.
683 683 #==============================================================================
684 684 from rhodecode.lib.vcs.utils import author_name, author_email
685 685 from rhodecode.lib.utils2 import credentials_filter, age, age_from_seconds
686 686 from rhodecode.model.db import User, ChangesetStatus
687 687
688 688 capitalize = lambda x: x.capitalize()
689 689 email = author_email
690 690 short_id = lambda x: x[:12]
691 691 hide_credentials = lambda x: ''.join(credentials_filter(x))
692 692
693 693
694 694 import pytz
695 695 import tzlocal
696 696 local_timezone = tzlocal.get_localzone()
697 697
698 698
699 699 def age_component(datetime_iso, value=None, time_is_local=False):
700 700 title = value or format_date(datetime_iso)
701 701 tzinfo = '+00:00'
702 702
703 703 # detect if we have a timezone info, otherwise, add it
704 704 if time_is_local and isinstance(datetime_iso, datetime) and not datetime_iso.tzinfo:
705 705 force_timezone = os.environ.get('RC_TIMEZONE', '')
706 706 if force_timezone:
707 707 force_timezone = pytz.timezone(force_timezone)
708 708 timezone = force_timezone or local_timezone
709 709 offset = timezone.localize(datetime_iso).strftime('%z')
710 710 tzinfo = '{}:{}'.format(offset[:-2], offset[-2:])
711 711
712 712 return literal(
713 713 '<time class="timeago tooltip" '
714 714 'title="{1}{2}" datetime="{0}{2}">{1}</time>'.format(
715 715 datetime_iso, title, tzinfo))
716 716
717 717
718 718 def _shorten_commit_id(commit_id, commit_len=None):
719 719 if commit_len is None:
720 720 request = get_current_request()
721 721 commit_len = request.call_context.visual.show_sha_length
722 722 return commit_id[:commit_len]
723 723
724 724
725 725 def show_id(commit, show_idx=None, commit_len=None):
726 726 """
727 727 Configurable function that shows ID
728 728 by default it's r123:fffeeefffeee
729 729
730 730 :param commit: commit instance
731 731 """
732 732 if show_idx is None:
733 733 request = get_current_request()
734 734 show_idx = request.call_context.visual.show_revision_number
735 735
736 736 raw_id = _shorten_commit_id(commit.raw_id, commit_len=commit_len)
737 737 if show_idx:
738 738 return 'r%s:%s' % (commit.idx, raw_id)
739 739 else:
740 740 return '%s' % (raw_id, )
741 741
742 742
743 743 def format_date(date):
744 744 """
745 745 use a standardized formatting for dates used in RhodeCode
746 746
747 747 :param date: date/datetime object
748 748 :return: formatted date
749 749 """
750 750
751 751 if date:
752 752 _fmt = "%a, %d %b %Y %H:%M:%S"
753 753 return safe_unicode(date.strftime(_fmt))
754 754
755 755 return u""
756 756
757 757
758 758 class _RepoChecker(object):
759 759
760 760 def __init__(self, backend_alias):
761 761 self._backend_alias = backend_alias
762 762
763 763 def __call__(self, repository):
764 764 if hasattr(repository, 'alias'):
765 765 _type = repository.alias
766 766 elif hasattr(repository, 'repo_type'):
767 767 _type = repository.repo_type
768 768 else:
769 769 _type = repository
770 770 return _type == self._backend_alias
771 771
772 772
773 773 is_git = _RepoChecker('git')
774 774 is_hg = _RepoChecker('hg')
775 775 is_svn = _RepoChecker('svn')
776 776
777 777
778 778 def get_repo_type_by_name(repo_name):
779 779 repo = Repository.get_by_repo_name(repo_name)
780 780 if repo:
781 781 return repo.repo_type
782 782
783 783
784 784 def is_svn_without_proxy(repository):
785 785 if is_svn(repository):
786 786 from rhodecode.model.settings import VcsSettingsModel
787 787 conf = VcsSettingsModel().get_ui_settings_as_config_obj()
788 788 return not str2bool(conf.get('vcs_svn_proxy', 'http_requests_enabled'))
789 789 return False
790 790
791 791
792 792 def discover_user(author):
793 793 """
794 794 Tries to discover RhodeCode User based on the autho string. Author string
795 795 is typically `FirstName LastName <email@address.com>`
796 796 """
797 797
798 798 # if author is already an instance use it for extraction
799 799 if isinstance(author, User):
800 800 return author
801 801
802 802 # Valid email in the attribute passed, see if they're in the system
803 803 _email = author_email(author)
804 804 if _email != '':
805 805 user = User.get_by_email(_email, case_insensitive=True, cache=True)
806 806 if user is not None:
807 807 return user
808 808
809 809 # Maybe it's a username, we try to extract it and fetch by username ?
810 810 _author = author_name(author)
811 811 user = User.get_by_username(_author, case_insensitive=True, cache=True)
812 812 if user is not None:
813 813 return user
814 814
815 815 return None
816 816
817 817
818 818 def email_or_none(author):
819 819 # extract email from the commit string
820 820 _email = author_email(author)
821 821
822 822 # If we have an email, use it, otherwise
823 823 # see if it contains a username we can get an email from
824 824 if _email != '':
825 825 return _email
826 826 else:
827 827 user = User.get_by_username(
828 828 author_name(author), case_insensitive=True, cache=True)
829 829
830 830 if user is not None:
831 831 return user.email
832 832
833 833 # No valid email, not a valid user in the system, none!
834 834 return None
835 835
836 836
837 837 def link_to_user(author, length=0, **kwargs):
838 838 user = discover_user(author)
839 839 # user can be None, but if we have it already it means we can re-use it
840 840 # in the person() function, so we save 1 intensive-query
841 841 if user:
842 842 author = user
843 843
844 844 display_person = person(author, 'username_or_name_or_email')
845 845 if length:
846 846 display_person = shorter(display_person, length)
847 847
848 848 if user:
849 849 return link_to(
850 850 escape(display_person),
851 851 route_path('user_profile', username=user.username),
852 852 **kwargs)
853 853 else:
854 854 return escape(display_person)
855 855
856 856
857 857 def link_to_group(users_group_name, **kwargs):
858 858 return link_to(
859 859 escape(users_group_name),
860 860 route_path('user_group_profile', user_group_name=users_group_name),
861 861 **kwargs)
862 862
863 863
864 864 def person(author, show_attr="username_and_name"):
865 865 user = discover_user(author)
866 866 if user:
867 867 return getattr(user, show_attr)
868 868 else:
869 869 _author = author_name(author)
870 870 _email = email(author)
871 871 return _author or _email
872 872
873 873
874 874 def author_string(email):
875 875 if email:
876 876 user = User.get_by_email(email, case_insensitive=True, cache=True)
877 877 if user:
878 878 if user.first_name or user.last_name:
879 879 return '%s %s &lt;%s&gt;' % (
880 880 user.first_name, user.last_name, email)
881 881 else:
882 882 return email
883 883 else:
884 884 return email
885 885 else:
886 886 return None
887 887
888 888
889 889 def person_by_id(id_, show_attr="username_and_name"):
890 890 # attr to return from fetched user
891 891 person_getter = lambda usr: getattr(usr, show_attr)
892 892
893 893 #maybe it's an ID ?
894 894 if str(id_).isdigit() or isinstance(id_, int):
895 895 id_ = int(id_)
896 896 user = User.get(id_)
897 897 if user is not None:
898 898 return person_getter(user)
899 899 return id_
900 900
901 901
902 def gravatar_with_user(request, author, show_disabled=False):
903 _render = request.get_partial_renderer(
904 'rhodecode:templates/base/base.mako')
905 return _render('gravatar_with_user', author, show_disabled=show_disabled)
902 def gravatar_with_user(request, author, show_disabled=False, tooltip=False):
903 _render = request.get_partial_renderer('rhodecode:templates/base/base.mako')
904 return _render('gravatar_with_user', author, show_disabled=show_disabled, tooltip=tooltip)
906 905
907 906
908 907 tags_paterns = OrderedDict((
909 908 ('lang', (re.compile(r'\[(lang|language)\ \=\&gt;\ *([a-zA-Z\-\/\#\+\.]*)\]'),
910 909 '<div class="metatag" tag="lang">\\2</div>')),
911 910
912 911 ('see', (re.compile(r'\[see\ \=\&gt;\ *([a-zA-Z0-9\/\=\?\&amp;\ \:\/\.\-]*)\]'),
913 912 '<div class="metatag" tag="see">see: \\1 </div>')),
914 913
915 914 ('url', (re.compile(r'\[url\ \=\&gt;\ \[([a-zA-Z0-9\ \.\-\_]+)\]\((http://|https://|/)(.*?)\)\]'),
916 915 '<div class="metatag" tag="url"> <a href="\\2\\3">\\1</a> </div>')),
917 916
918 917 ('license', (re.compile(r'\[license\ \=\&gt;\ *([a-zA-Z0-9\/\=\?\&amp;\ \:\/\.\-]*)\]'),
919 918 '<div class="metatag" tag="license"><a href="http:\/\/www.opensource.org/licenses/\\1">\\1</a></div>')),
920 919
921 920 ('ref', (re.compile(r'\[(requires|recommends|conflicts|base)\ \=\&gt;\ *([a-zA-Z0-9\-\/]*)\]'),
922 921 '<div class="metatag" tag="ref \\1">\\1: <a href="/\\2">\\2</a></div>')),
923 922
924 923 ('state', (re.compile(r'\[(stable|featured|stale|dead|dev|deprecated)\]'),
925 924 '<div class="metatag" tag="state \\1">\\1</div>')),
926 925
927 926 # label in grey
928 927 ('label', (re.compile(r'\[([a-z]+)\]'),
929 928 '<div class="metatag" tag="label">\\1</div>')),
930 929
931 930 # generic catch all in grey
932 931 ('generic', (re.compile(r'\[([a-zA-Z0-9\.\-\_]+)\]'),
933 932 '<div class="metatag" tag="generic">\\1</div>')),
934 933 ))
935 934
936 935
937 936 def extract_metatags(value):
938 937 """
939 938 Extract supported meta-tags from given text value
940 939 """
941 940 tags = []
942 941 if not value:
943 942 return tags, ''
944 943
945 944 for key, val in tags_paterns.items():
946 945 pat, replace_html = val
947 946 tags.extend([(key, x.group()) for x in pat.finditer(value)])
948 947 value = pat.sub('', value)
949 948
950 949 return tags, value
951 950
952 951
953 952 def style_metatag(tag_type, value):
954 953 """
955 954 converts tags from value into html equivalent
956 955 """
957 956 if not value:
958 957 return ''
959 958
960 959 html_value = value
961 960 tag_data = tags_paterns.get(tag_type)
962 961 if tag_data:
963 962 pat, replace_html = tag_data
964 963 # convert to plain `unicode` instead of a markup tag to be used in
965 964 # regex expressions. safe_unicode doesn't work here
966 965 html_value = pat.sub(replace_html, unicode(value))
967 966
968 967 return html_value
969 968
970 969
971 970 def bool2icon(value, show_at_false=True):
972 971 """
973 972 Returns boolean value of a given value, represented as html element with
974 973 classes that will represent icons
975 974
976 975 :param value: given value to convert to html node
977 976 """
978 977
979 978 if value: # does bool conversion
980 979 return HTML.tag('i', class_="icon-true", title='True')
981 980 else: # not true as bool
982 981 if show_at_false:
983 982 return HTML.tag('i', class_="icon-false", title='False')
984 983 return HTML.tag('i')
985 984
986 985 #==============================================================================
987 986 # PERMS
988 987 #==============================================================================
989 988 from rhodecode.lib.auth import HasPermissionAny, HasPermissionAll, \
990 989 HasRepoPermissionAny, HasRepoPermissionAll, HasRepoGroupPermissionAll, \
991 990 HasRepoGroupPermissionAny, HasRepoPermissionAnyApi, get_csrf_token, \
992 991 csrf_token_key
993 992
994 993
995 994 #==============================================================================
996 995 # GRAVATAR URL
997 996 #==============================================================================
998 997 class InitialsGravatar(object):
999 998 def __init__(self, email_address, first_name, last_name, size=30,
1000 999 background=None, text_color='#fff'):
1001 1000 self.size = size
1002 1001 self.first_name = first_name
1003 1002 self.last_name = last_name
1004 1003 self.email_address = email_address
1005 1004 self.background = background or self.str2color(email_address)
1006 1005 self.text_color = text_color
1007 1006
1008 1007 def get_color_bank(self):
1009 1008 """
1010 1009 returns a predefined list of colors that gravatars can use.
1011 1010 Those are randomized distinct colors that guarantee readability and
1012 1011 uniqueness.
1013 1012
1014 1013 generated with: http://phrogz.net/css/distinct-colors.html
1015 1014 """
1016 1015 return [
1017 1016 '#bf3030', '#a67f53', '#00ff00', '#5989b3', '#392040', '#d90000',
1018 1017 '#402910', '#204020', '#79baf2', '#a700b3', '#bf6060', '#7f5320',
1019 1018 '#008000', '#003059', '#ee00ff', '#ff0000', '#8c4b00', '#007300',
1020 1019 '#005fb3', '#de73e6', '#ff4040', '#ffaa00', '#3df255', '#203140',
1021 1020 '#47004d', '#591616', '#664400', '#59b365', '#0d2133', '#83008c',
1022 1021 '#592d2d', '#bf9f60', '#73e682', '#1d3f73', '#73006b', '#402020',
1023 1022 '#b2862d', '#397341', '#597db3', '#e600d6', '#a60000', '#736039',
1024 1023 '#00b318', '#79aaf2', '#330d30', '#ff8080', '#403010', '#16591f',
1025 1024 '#002459', '#8c4688', '#e50000', '#ffbf40', '#00732e', '#102340',
1026 1025 '#bf60ac', '#8c4646', '#cc8800', '#00a642', '#1d3473', '#b32d98',
1027 1026 '#660e00', '#ffd580', '#80ffb2', '#7391e6', '#733967', '#d97b6c',
1028 1027 '#8c5e00', '#59b389', '#3967e6', '#590047', '#73281d', '#665200',
1029 1028 '#00e67a', '#2d50b3', '#8c2377', '#734139', '#b2982d', '#16593a',
1030 1029 '#001859', '#ff00aa', '#a65e53', '#ffcc00', '#0d3321', '#2d3959',
1031 1030 '#731d56', '#401610', '#4c3d00', '#468c6c', '#002ca6', '#d936a3',
1032 1031 '#d94c36', '#403920', '#36d9a3', '#0d1733', '#592d4a', '#993626',
1033 1032 '#cca300', '#00734d', '#46598c', '#8c005e', '#7f1100', '#8c7000',
1034 1033 '#00a66f', '#7382e6', '#b32d74', '#d9896c', '#ffe680', '#1d7362',
1035 1034 '#364cd9', '#73003d', '#d93a00', '#998a4d', '#59b3a1', '#5965b3',
1036 1035 '#e5007a', '#73341d', '#665f00', '#00b38f', '#0018b3', '#59163a',
1037 1036 '#b2502d', '#bfb960', '#00ffcc', '#23318c', '#a6537f', '#734939',
1038 1037 '#b2a700', '#104036', '#3d3df2', '#402031', '#e56739', '#736f39',
1039 1038 '#79f2ea', '#000059', '#401029', '#4c1400', '#ffee00', '#005953',
1040 1039 '#101040', '#990052', '#402820', '#403d10', '#00ffee', '#0000d9',
1041 1040 '#ff80c4', '#a66953', '#eeff00', '#00ccbe', '#8080ff', '#e673a1',
1042 1041 '#a62c00', '#474d00', '#1a3331', '#46468c', '#733950', '#662900',
1043 1042 '#858c23', '#238c85', '#0f0073', '#b20047', '#d9986c', '#becc00',
1044 1043 '#396f73', '#281d73', '#ff0066', '#ff6600', '#dee673', '#59adb3',
1045 1044 '#6559b3', '#590024', '#b2622d', '#98b32d', '#36ced9', '#332d59',
1046 1045 '#40001a', '#733f1d', '#526600', '#005359', '#242040', '#bf6079',
1047 1046 '#735039', '#cef23d', '#007780', '#5630bf', '#66001b', '#b24700',
1048 1047 '#acbf60', '#1d6273', '#25008c', '#731d34', '#a67453', '#50592d',
1049 1048 '#00ccff', '#6600ff', '#ff0044', '#4c1f00', '#8a994d', '#79daf2',
1050 1049 '#a173e6', '#d93662', '#402310', '#aaff00', '#2d98b3', '#8c40ff',
1051 1050 '#592d39', '#ff8c40', '#354020', '#103640', '#1a0040', '#331a20',
1052 1051 '#331400', '#334d00', '#1d5673', '#583973', '#7f0022', '#4c3626',
1053 1052 '#88cc00', '#36a3d9', '#3d0073', '#d9364c', '#33241a', '#698c23',
1054 1053 '#5995b3', '#300059', '#e57382', '#7f3300', '#366600', '#00aaff',
1055 1054 '#3a1659', '#733941', '#663600', '#74b32d', '#003c59', '#7f53a6',
1056 1055 '#73000f', '#ff8800', '#baf279', '#79caf2', '#291040', '#a6293a',
1057 1056 '#b2742d', '#587339', '#0077b3', '#632699', '#400009', '#d9a66c',
1058 1057 '#294010', '#2d4a59', '#aa00ff', '#4c131b', '#b25f00', '#5ce600',
1059 1058 '#267399', '#a336d9', '#990014', '#664e33', '#86bf60', '#0088ff',
1060 1059 '#7700b3', '#593a16', '#073300', '#1d4b73', '#ac60bf', '#e59539',
1061 1060 '#4f8c46', '#368dd9', '#5c0073'
1062 1061 ]
1063 1062
1064 1063 def rgb_to_hex_color(self, rgb_tuple):
1065 1064 """
1066 1065 Converts an rgb_tuple passed to an hex color.
1067 1066
1068 1067 :param rgb_tuple: tuple with 3 ints represents rgb color space
1069 1068 """
1070 1069 return '#' + ("".join(map(chr, rgb_tuple)).encode('hex'))
1071 1070
1072 1071 def email_to_int_list(self, email_str):
1073 1072 """
1074 1073 Get every byte of the hex digest value of email and turn it to integer.
1075 1074 It's going to be always between 0-255
1076 1075 """
1077 1076 digest = md5_safe(email_str.lower())
1078 1077 return [int(digest[i * 2:i * 2 + 2], 16) for i in range(16)]
1079 1078
1080 1079 def pick_color_bank_index(self, email_str, color_bank):
1081 1080 return self.email_to_int_list(email_str)[0] % len(color_bank)
1082 1081
1083 1082 def str2color(self, email_str):
1084 1083 """
1085 1084 Tries to map in a stable algorithm an email to color
1086 1085
1087 1086 :param email_str:
1088 1087 """
1089 1088 color_bank = self.get_color_bank()
1090 1089 # pick position (module it's length so we always find it in the
1091 1090 # bank even if it's smaller than 256 values
1092 1091 pos = self.pick_color_bank_index(email_str, color_bank)
1093 1092 return color_bank[pos]
1094 1093
1095 1094 def normalize_email(self, email_address):
1096 1095 import unicodedata
1097 1096 # default host used to fill in the fake/missing email
1098 1097 default_host = u'localhost'
1099 1098
1100 1099 if not email_address:
1101 1100 email_address = u'%s@%s' % (User.DEFAULT_USER, default_host)
1102 1101
1103 1102 email_address = safe_unicode(email_address)
1104 1103
1105 1104 if u'@' not in email_address:
1106 1105 email_address = u'%s@%s' % (email_address, default_host)
1107 1106
1108 1107 if email_address.endswith(u'@'):
1109 1108 email_address = u'%s%s' % (email_address, default_host)
1110 1109
1111 1110 email_address = unicodedata.normalize('NFKD', email_address)\
1112 1111 .encode('ascii', 'ignore')
1113 1112 return email_address
1114 1113
1115 1114 def get_initials(self):
1116 1115 """
1117 1116 Returns 2 letter initials calculated based on the input.
1118 1117 The algorithm picks first given email address, and takes first letter
1119 1118 of part before @, and then the first letter of server name. In case
1120 1119 the part before @ is in a format of `somestring.somestring2` it replaces
1121 1120 the server letter with first letter of somestring2
1122 1121
1123 1122 In case function was initialized with both first and lastname, this
1124 1123 overrides the extraction from email by first letter of the first and
1125 1124 last name. We add special logic to that functionality, In case Full name
1126 1125 is compound, like Guido Von Rossum, we use last part of the last name
1127 1126 (Von Rossum) picking `R`.
1128 1127
1129 1128 Function also normalizes the non-ascii characters to they ascii
1130 1129 representation, eg Ą => A
1131 1130 """
1132 1131 import unicodedata
1133 1132 # replace non-ascii to ascii
1134 1133 first_name = unicodedata.normalize(
1135 1134 'NFKD', safe_unicode(self.first_name)).encode('ascii', 'ignore')
1136 1135 last_name = unicodedata.normalize(
1137 1136 'NFKD', safe_unicode(self.last_name)).encode('ascii', 'ignore')
1138 1137
1139 1138 # do NFKD encoding, and also make sure email has proper format
1140 1139 email_address = self.normalize_email(self.email_address)
1141 1140
1142 1141 # first push the email initials
1143 1142 prefix, server = email_address.split('@', 1)
1144 1143
1145 1144 # check if prefix is maybe a 'first_name.last_name' syntax
1146 1145 _dot_split = prefix.rsplit('.', 1)
1147 1146 if len(_dot_split) == 2 and _dot_split[1]:
1148 1147 initials = [_dot_split[0][0], _dot_split[1][0]]
1149 1148 else:
1150 1149 initials = [prefix[0], server[0]]
1151 1150
1152 1151 # then try to replace either first_name or last_name
1153 1152 fn_letter = (first_name or " ")[0].strip()
1154 1153 ln_letter = (last_name.split(' ', 1)[-1] or " ")[0].strip()
1155 1154
1156 1155 if fn_letter:
1157 1156 initials[0] = fn_letter
1158 1157
1159 1158 if ln_letter:
1160 1159 initials[1] = ln_letter
1161 1160
1162 1161 return ''.join(initials).upper()
1163 1162
1164 1163 def get_img_data_by_type(self, font_family, img_type):
1165 1164 default_user = """
1166 1165 <svg xmlns="http://www.w3.org/2000/svg"
1167 1166 version="1.1" x="0px" y="0px" width="{size}" height="{size}"
1168 1167 viewBox="-15 -10 439.165 429.164"
1169 1168
1170 1169 xml:space="preserve"
1171 1170 style="background:{background};" >
1172 1171
1173 1172 <path d="M204.583,216.671c50.664,0,91.74-48.075,
1174 1173 91.74-107.378c0-82.237-41.074-107.377-91.74-107.377
1175 1174 c-50.668,0-91.74,25.14-91.74,107.377C112.844,
1176 1175 168.596,153.916,216.671,
1177 1176 204.583,216.671z" fill="{text_color}"/>
1178 1177 <path d="M407.164,374.717L360.88,
1179 1178 270.454c-2.117-4.771-5.836-8.728-10.465-11.138l-71.83-37.392
1180 1179 c-1.584-0.823-3.502-0.663-4.926,0.415c-20.316,
1181 1180 15.366-44.203,23.488-69.076,23.488c-24.877,
1182 1181 0-48.762-8.122-69.078-23.488
1183 1182 c-1.428-1.078-3.346-1.238-4.93-0.415L58.75,
1184 1183 259.316c-4.631,2.41-8.346,6.365-10.465,11.138L2.001,374.717
1185 1184 c-3.191,7.188-2.537,15.412,1.75,22.005c4.285,
1186 1185 6.592,11.537,10.526,19.4,10.526h362.861c7.863,0,15.117-3.936,
1187 1186 19.402-10.527 C409.699,390.129,
1188 1187 410.355,381.902,407.164,374.717z" fill="{text_color}"/>
1189 1188 </svg>""".format(
1190 1189 size=self.size,
1191 1190 background='#979797', # @grey4
1192 1191 text_color=self.text_color,
1193 1192 font_family=font_family)
1194 1193
1195 1194 return {
1196 1195 "default_user": default_user
1197 1196 }[img_type]
1198 1197
1199 1198 def get_img_data(self, svg_type=None):
1200 1199 """
1201 1200 generates the svg metadata for image
1202 1201 """
1203 1202 fonts = [
1204 1203 '-apple-system',
1205 1204 'BlinkMacSystemFont',
1206 1205 'Segoe UI',
1207 1206 'Roboto',
1208 1207 'Oxygen-Sans',
1209 1208 'Ubuntu',
1210 1209 'Cantarell',
1211 1210 'Helvetica Neue',
1212 1211 'sans-serif'
1213 1212 ]
1214 1213 font_family = ','.join(fonts)
1215 1214 if svg_type:
1216 1215 return self.get_img_data_by_type(font_family, svg_type)
1217 1216
1218 1217 initials = self.get_initials()
1219 1218 img_data = """
1220 1219 <svg xmlns="http://www.w3.org/2000/svg" pointer-events="none"
1221 1220 width="{size}" height="{size}"
1222 1221 style="width: 100%; height: 100%; background-color: {background}"
1223 1222 viewBox="0 0 {size} {size}">
1224 1223 <text text-anchor="middle" y="50%" x="50%" dy="0.35em"
1225 1224 pointer-events="auto" fill="{text_color}"
1226 1225 font-family="{font_family}"
1227 1226 style="font-weight: 400; font-size: {f_size}px;">{text}
1228 1227 </text>
1229 1228 </svg>""".format(
1230 1229 size=self.size,
1231 1230 f_size=self.size/2.05, # scale the text inside the box nicely
1232 1231 background=self.background,
1233 1232 text_color=self.text_color,
1234 1233 text=initials.upper(),
1235 1234 font_family=font_family)
1236 1235
1237 1236 return img_data
1238 1237
1239 1238 def generate_svg(self, svg_type=None):
1240 1239 img_data = self.get_img_data(svg_type)
1241 1240 return "data:image/svg+xml;base64,%s" % img_data.encode('base64')
1242 1241
1243 1242
1244 1243 def initials_gravatar(email_address, first_name, last_name, size=30):
1245 1244 svg_type = None
1246 1245 if email_address == User.DEFAULT_USER_EMAIL:
1247 1246 svg_type = 'default_user'
1248 1247 klass = InitialsGravatar(email_address, first_name, last_name, size)
1249 1248 return klass.generate_svg(svg_type=svg_type)
1250 1249
1251 1250
1252 1251 def gravatar_url(email_address, size=30, request=None):
1253 1252 request = get_current_request()
1254 1253 _use_gravatar = request.call_context.visual.use_gravatar
1255 1254 _gravatar_url = request.call_context.visual.gravatar_url
1256 1255
1257 1256 _gravatar_url = _gravatar_url or User.DEFAULT_GRAVATAR_URL
1258 1257
1259 1258 email_address = email_address or User.DEFAULT_USER_EMAIL
1260 1259 if isinstance(email_address, unicode):
1261 1260 # hashlib crashes on unicode items
1262 1261 email_address = safe_str(email_address)
1263 1262
1264 1263 # empty email or default user
1265 1264 if not email_address or email_address == User.DEFAULT_USER_EMAIL:
1266 1265 return initials_gravatar(User.DEFAULT_USER_EMAIL, '', '', size=size)
1267 1266
1268 1267 if _use_gravatar:
1269 1268 # TODO: Disuse pyramid thread locals. Think about another solution to
1270 1269 # get the host and schema here.
1271 1270 request = get_current_request()
1272 1271 tmpl = safe_str(_gravatar_url)
1273 1272 tmpl = tmpl.replace('{email}', email_address)\
1274 1273 .replace('{md5email}', md5_safe(email_address.lower())) \
1275 1274 .replace('{netloc}', request.host)\
1276 1275 .replace('{scheme}', request.scheme)\
1277 1276 .replace('{size}', safe_str(size))
1278 1277 return tmpl
1279 1278 else:
1280 1279 return initials_gravatar(email_address, '', '', size=size)
1281 1280
1282 1281
1283 1282 class Page(_Page):
1284 1283 """
1285 1284 Custom pager to match rendering style with paginator
1286 1285 """
1287 1286
1288 1287 def _get_pos(self, cur_page, max_page, items):
1289 1288 edge = (items / 2) + 1
1290 1289 if (cur_page <= edge):
1291 1290 radius = max(items / 2, items - cur_page)
1292 1291 elif (max_page - cur_page) < edge:
1293 1292 radius = (items - 1) - (max_page - cur_page)
1294 1293 else:
1295 1294 radius = items / 2
1296 1295
1297 1296 left = max(1, (cur_page - (radius)))
1298 1297 right = min(max_page, cur_page + (radius))
1299 1298 return left, cur_page, right
1300 1299
1301 1300 def _range(self, regexp_match):
1302 1301 """
1303 1302 Return range of linked pages (e.g. '1 2 [3] 4 5 6 7 8').
1304 1303
1305 1304 Arguments:
1306 1305
1307 1306 regexp_match
1308 1307 A "re" (regular expressions) match object containing the
1309 1308 radius of linked pages around the current page in
1310 1309 regexp_match.group(1) as a string
1311 1310
1312 1311 This function is supposed to be called as a callable in
1313 1312 re.sub.
1314 1313
1315 1314 """
1316 1315 radius = int(regexp_match.group(1))
1317 1316
1318 1317 # Compute the first and last page number within the radius
1319 1318 # e.g. '1 .. 5 6 [7] 8 9 .. 12'
1320 1319 # -> leftmost_page = 5
1321 1320 # -> rightmost_page = 9
1322 1321 leftmost_page, _cur, rightmost_page = self._get_pos(self.page,
1323 1322 self.last_page,
1324 1323 (radius * 2) + 1)
1325 1324 nav_items = []
1326 1325
1327 1326 # Create a link to the first page (unless we are on the first page
1328 1327 # or there would be no need to insert '..' spacers)
1329 1328 if self.page != self.first_page and self.first_page < leftmost_page:
1330 1329 nav_items.append(self._pagerlink(self.first_page, self.first_page))
1331 1330
1332 1331 # Insert dots if there are pages between the first page
1333 1332 # and the currently displayed page range
1334 1333 if leftmost_page - self.first_page > 1:
1335 1334 # Wrap in a SPAN tag if nolink_attr is set
1336 1335 text = '..'
1337 1336 if self.dotdot_attr:
1338 1337 text = HTML.span(c=text, **self.dotdot_attr)
1339 1338 nav_items.append(text)
1340 1339
1341 1340 for thispage in xrange(leftmost_page, rightmost_page + 1):
1342 1341 # Hilight the current page number and do not use a link
1343 1342 if thispage == self.page:
1344 1343 text = '%s' % (thispage,)
1345 1344 # Wrap in a SPAN tag if nolink_attr is set
1346 1345 if self.curpage_attr:
1347 1346 text = HTML.span(c=text, **self.curpage_attr)
1348 1347 nav_items.append(text)
1349 1348 # Otherwise create just a link to that page
1350 1349 else:
1351 1350 text = '%s' % (thispage,)
1352 1351 nav_items.append(self._pagerlink(thispage, text))
1353 1352
1354 1353 # Insert dots if there are pages between the displayed
1355 1354 # page numbers and the end of the page range
1356 1355 if self.last_page - rightmost_page > 1:
1357 1356 text = '..'
1358 1357 # Wrap in a SPAN tag if nolink_attr is set
1359 1358 if self.dotdot_attr:
1360 1359 text = HTML.span(c=text, **self.dotdot_attr)
1361 1360 nav_items.append(text)
1362 1361
1363 1362 # Create a link to the very last page (unless we are on the last
1364 1363 # page or there would be no need to insert '..' spacers)
1365 1364 if self.page != self.last_page and rightmost_page < self.last_page:
1366 1365 nav_items.append(self._pagerlink(self.last_page, self.last_page))
1367 1366
1368 1367 ## prerender links
1369 1368 #_page_link = url.current()
1370 1369 #nav_items.append(literal('<link rel="prerender" href="%s?page=%s">' % (_page_link, str(int(self.page)+1))))
1371 1370 #nav_items.append(literal('<link rel="prefetch" href="%s?page=%s">' % (_page_link, str(int(self.page)+1))))
1372 1371 return self.separator.join(nav_items)
1373 1372
1374 1373 def pager(self, format='~2~', page_param='page', partial_param='partial',
1375 1374 show_if_single_page=False, separator=' ', onclick=None,
1376 1375 symbol_first='<<', symbol_last='>>',
1377 1376 symbol_previous='<', symbol_next='>',
1378 1377 link_attr={'class': 'pager_link', 'rel': 'prerender'},
1379 1378 curpage_attr={'class': 'pager_curpage'},
1380 1379 dotdot_attr={'class': 'pager_dotdot'}, **kwargs):
1381 1380
1382 1381 self.curpage_attr = curpage_attr
1383 1382 self.separator = separator
1384 1383 self.pager_kwargs = kwargs
1385 1384 self.page_param = page_param
1386 1385 self.partial_param = partial_param
1387 1386 self.onclick = onclick
1388 1387 self.link_attr = link_attr
1389 1388 self.dotdot_attr = dotdot_attr
1390 1389
1391 1390 # Don't show navigator if there is no more than one page
1392 1391 if self.page_count == 0 or (self.page_count == 1 and not show_if_single_page):
1393 1392 return ''
1394 1393
1395 1394 from string import Template
1396 1395 # Replace ~...~ in token format by range of pages
1397 1396 result = re.sub(r'~(\d+)~', self._range, format)
1398 1397
1399 1398 # Interpolate '%' variables
1400 1399 result = Template(result).safe_substitute({
1401 1400 'first_page': self.first_page,
1402 1401 'last_page': self.last_page,
1403 1402 'page': self.page,
1404 1403 'page_count': self.page_count,
1405 1404 'items_per_page': self.items_per_page,
1406 1405 'first_item': self.first_item,
1407 1406 'last_item': self.last_item,
1408 1407 'item_count': self.item_count,
1409 1408 'link_first': self.page > self.first_page and \
1410 1409 self._pagerlink(self.first_page, symbol_first) or '',
1411 1410 'link_last': self.page < self.last_page and \
1412 1411 self._pagerlink(self.last_page, symbol_last) or '',
1413 1412 'link_previous': self.previous_page and \
1414 1413 self._pagerlink(self.previous_page, symbol_previous) \
1415 1414 or HTML.span(symbol_previous, class_="pg-previous disabled"),
1416 1415 'link_next': self.next_page and \
1417 1416 self._pagerlink(self.next_page, symbol_next) \
1418 1417 or HTML.span(symbol_next, class_="pg-next disabled")
1419 1418 })
1420 1419
1421 1420 return literal(result)
1422 1421
1423 1422
1424 1423 #==============================================================================
1425 1424 # REPO PAGER, PAGER FOR REPOSITORY
1426 1425 #==============================================================================
1427 1426 class RepoPage(Page):
1428 1427
1429 1428 def __init__(self, collection, page=1, items_per_page=20,
1430 1429 item_count=None, url=None, **kwargs):
1431 1430
1432 1431 """Create a "RepoPage" instance. special pager for paging
1433 1432 repository
1434 1433 """
1435 1434 self._url_generator = url
1436 1435
1437 1436 # Safe the kwargs class-wide so they can be used in the pager() method
1438 1437 self.kwargs = kwargs
1439 1438
1440 1439 # Save a reference to the collection
1441 1440 self.original_collection = collection
1442 1441
1443 1442 self.collection = collection
1444 1443
1445 1444 # The self.page is the number of the current page.
1446 1445 # The first page has the number 1!
1447 1446 try:
1448 1447 self.page = int(page) # make it int() if we get it as a string
1449 1448 except (ValueError, TypeError):
1450 1449 self.page = 1
1451 1450
1452 1451 self.items_per_page = items_per_page
1453 1452
1454 1453 # Unless the user tells us how many items the collections has
1455 1454 # we calculate that ourselves.
1456 1455 if item_count is not None:
1457 1456 self.item_count = item_count
1458 1457 else:
1459 1458 self.item_count = len(self.collection)
1460 1459
1461 1460 # Compute the number of the first and last available page
1462 1461 if self.item_count > 0:
1463 1462 self.first_page = 1
1464 1463 self.page_count = int(math.ceil(float(self.item_count) /
1465 1464 self.items_per_page))
1466 1465 self.last_page = self.first_page + self.page_count - 1
1467 1466
1468 1467 # Make sure that the requested page number is the range of
1469 1468 # valid pages
1470 1469 if self.page > self.last_page:
1471 1470 self.page = self.last_page
1472 1471 elif self.page < self.first_page:
1473 1472 self.page = self.first_page
1474 1473
1475 1474 # Note: the number of items on this page can be less than
1476 1475 # items_per_page if the last page is not full
1477 1476 self.first_item = max(0, (self.item_count) - (self.page *
1478 1477 items_per_page))
1479 1478 self.last_item = ((self.item_count - 1) - items_per_page *
1480 1479 (self.page - 1))
1481 1480
1482 1481 self.items = list(self.collection[self.first_item:self.last_item + 1])
1483 1482
1484 1483 # Links to previous and next page
1485 1484 if self.page > self.first_page:
1486 1485 self.previous_page = self.page - 1
1487 1486 else:
1488 1487 self.previous_page = None
1489 1488
1490 1489 if self.page < self.last_page:
1491 1490 self.next_page = self.page + 1
1492 1491 else:
1493 1492 self.next_page = None
1494 1493
1495 1494 # No items available
1496 1495 else:
1497 1496 self.first_page = None
1498 1497 self.page_count = 0
1499 1498 self.last_page = None
1500 1499 self.first_item = None
1501 1500 self.last_item = None
1502 1501 self.previous_page = None
1503 1502 self.next_page = None
1504 1503 self.items = []
1505 1504
1506 1505 # This is a subclass of the 'list' type. Initialise the list now.
1507 1506 list.__init__(self, reversed(self.items))
1508 1507
1509 1508
1510 1509 def breadcrumb_repo_link(repo):
1511 1510 """
1512 1511 Makes a breadcrumbs path link to repo
1513 1512
1514 1513 ex::
1515 1514 group >> subgroup >> repo
1516 1515
1517 1516 :param repo: a Repository instance
1518 1517 """
1519 1518
1520 1519 path = [
1521 1520 link_to(group.name, route_path('repo_group_home', repo_group_name=group.group_name),
1522 1521 title='last change:{}'.format(format_date(group.last_commit_change)))
1523 1522 for group in repo.groups_with_parents
1524 1523 ] + [
1525 1524 link_to(repo.just_name, route_path('repo_summary', repo_name=repo.repo_name),
1526 1525 title='last change:{}'.format(format_date(repo.last_commit_change)))
1527 1526 ]
1528 1527
1529 1528 return literal(' &raquo; '.join(path))
1530 1529
1531 1530
1532 1531 def breadcrumb_repo_group_link(repo_group):
1533 1532 """
1534 1533 Makes a breadcrumbs path link to repo
1535 1534
1536 1535 ex::
1537 1536 group >> subgroup
1538 1537
1539 1538 :param repo_group: a Repository Group instance
1540 1539 """
1541 1540
1542 1541 path = [
1543 1542 link_to(group.name,
1544 1543 route_path('repo_group_home', repo_group_name=group.group_name),
1545 1544 title='last change:{}'.format(format_date(group.last_commit_change)))
1546 1545 for group in repo_group.parents
1547 1546 ] + [
1548 1547 link_to(repo_group.name,
1549 1548 route_path('repo_group_home', repo_group_name=repo_group.group_name),
1550 1549 title='last change:{}'.format(format_date(repo_group.last_commit_change)))
1551 1550 ]
1552 1551
1553 1552 return literal(' &raquo; '.join(path))
1554 1553
1555 1554
1556 1555 def format_byte_size_binary(file_size):
1557 1556 """
1558 1557 Formats file/folder sizes to standard.
1559 1558 """
1560 1559 if file_size is None:
1561 1560 file_size = 0
1562 1561
1563 1562 formatted_size = format_byte_size(file_size, binary=True)
1564 1563 return formatted_size
1565 1564
1566 1565
1567 1566 def urlify_text(text_, safe=True):
1568 1567 """
1569 1568 Extrac urls from text and make html links out of them
1570 1569
1571 1570 :param text_:
1572 1571 """
1573 1572
1574 1573 url_pat = re.compile(r'''(http[s]?://(?:[a-zA-Z]|[0-9]|[$-_@#.&+]'''
1575 1574 '''|[!*\(\),]|(?:%[0-9a-fA-F][0-9a-fA-F]))+)''')
1576 1575
1577 1576 def url_func(match_obj):
1578 1577 url_full = match_obj.groups()[0]
1579 1578 return '<a href="%(url)s">%(url)s</a>' % ({'url': url_full})
1580 1579 _newtext = url_pat.sub(url_func, text_)
1581 1580 if safe:
1582 1581 return literal(_newtext)
1583 1582 return _newtext
1584 1583
1585 1584
1586 1585 def urlify_commits(text_, repository):
1587 1586 """
1588 1587 Extract commit ids from text and make link from them
1589 1588
1590 1589 :param text_:
1591 1590 :param repository: repo name to build the URL with
1592 1591 """
1593 1592
1594 1593 URL_PAT = re.compile(r'(^|\s)([0-9a-fA-F]{12,40})($|\s)')
1595 1594
1596 1595 def url_func(match_obj):
1597 1596 commit_id = match_obj.groups()[1]
1598 1597 pref = match_obj.groups()[0]
1599 1598 suf = match_obj.groups()[2]
1600 1599
1601 1600 tmpl = (
1602 1601 '%(pref)s<a class="%(cls)s" href="%(url)s">'
1603 1602 '%(commit_id)s</a>%(suf)s'
1604 1603 )
1605 1604 return tmpl % {
1606 1605 'pref': pref,
1607 1606 'cls': 'revision-link',
1608 1607 'url': route_url('repo_commit', repo_name=repository, commit_id=commit_id),
1609 1608 'commit_id': commit_id,
1610 1609 'suf': suf
1611 1610 }
1612 1611
1613 1612 newtext = URL_PAT.sub(url_func, text_)
1614 1613
1615 1614 return newtext
1616 1615
1617 1616
1618 1617 def _process_url_func(match_obj, repo_name, uid, entry,
1619 1618 return_raw_data=False, link_format='html'):
1620 1619 pref = ''
1621 1620 if match_obj.group().startswith(' '):
1622 1621 pref = ' '
1623 1622
1624 1623 issue_id = ''.join(match_obj.groups())
1625 1624
1626 1625 if link_format == 'html':
1627 1626 tmpl = (
1628 '%(pref)s<a class="%(cls)s" href="%(url)s">'
1627 '%(pref)s<a class="tooltip %(cls)s" href="%(url)s" title="%(title)s">'
1629 1628 '%(issue-prefix)s%(id-repr)s'
1630 1629 '</a>')
1631 1630 elif link_format == 'rst':
1632 1631 tmpl = '`%(issue-prefix)s%(id-repr)s <%(url)s>`_'
1633 1632 elif link_format == 'markdown':
1634 1633 tmpl = '[%(issue-prefix)s%(id-repr)s](%(url)s)'
1635 1634 else:
1636 1635 raise ValueError('Bad link_format:{}'.format(link_format))
1637 1636
1638 1637 (repo_name_cleaned,
1639 1638 parent_group_name) = RepoGroupModel()._get_group_name_and_parent(repo_name)
1640 1639
1641 1640 # variables replacement
1642 1641 named_vars = {
1643 1642 'id': issue_id,
1644 1643 'repo': repo_name,
1645 1644 'repo_name': repo_name_cleaned,
1646 'group_name': parent_group_name
1645 'group_name': parent_group_name,
1647 1646 }
1648 1647 # named regex variables
1649 1648 named_vars.update(match_obj.groupdict())
1650 1649 _url = string.Template(entry['url']).safe_substitute(**named_vars)
1651 1650
1652 1651 def quote_cleaner(input_str):
1653 1652 """Remove quotes as it's HTML"""
1654 1653 return input_str.replace('"', '')
1655 1654
1656 1655 data = {
1657 1656 'pref': pref,
1658 1657 'cls': quote_cleaner('issue-tracker-link'),
1659 1658 'url': quote_cleaner(_url),
1660 1659 'id-repr': issue_id,
1661 1660 'issue-prefix': entry['pref'],
1662 1661 'serv': entry['url'],
1662 'title': entry['desc']
1663 1663 }
1664 1664 if return_raw_data:
1665 1665 return {
1666 1666 'id': issue_id,
1667 1667 'url': _url
1668 1668 }
1669 1669 return tmpl % data
1670 1670
1671 1671
1672 1672 def get_active_pattern_entries(repo_name):
1673 1673 repo = None
1674 1674 if repo_name:
1675 1675 # Retrieving repo_name to avoid invalid repo_name to explode on
1676 1676 # IssueTrackerSettingsModel but still passing invalid name further down
1677 1677 repo = Repository.get_by_repo_name(repo_name, cache=True)
1678 1678
1679 1679 settings_model = IssueTrackerSettingsModel(repo=repo)
1680 1680 active_entries = settings_model.get_settings(cache=True)
1681 1681 return active_entries
1682 1682
1683 1683
1684 1684 def process_patterns(text_string, repo_name, link_format='html', active_entries=None):
1685 1685
1686 1686 allowed_formats = ['html', 'rst', 'markdown']
1687 1687 if link_format not in allowed_formats:
1688 1688 raise ValueError('Link format can be only one of:{} got {}'.format(
1689 1689 allowed_formats, link_format))
1690 1690
1691 1691 active_entries = active_entries or get_active_pattern_entries(repo_name)
1692 1692 issues_data = []
1693 1693 newtext = text_string
1694 1694
1695 1695 for uid, entry in active_entries.items():
1696 1696 log.debug('found issue tracker entry with uid %s', uid)
1697 1697
1698 1698 if not (entry['pat'] and entry['url']):
1699 1699 log.debug('skipping due to missing data')
1700 1700 continue
1701 1701
1702 1702 log.debug('issue tracker entry: uid: `%s` PAT:%s URL:%s PREFIX:%s',
1703 1703 uid, entry['pat'], entry['url'], entry['pref'])
1704 1704
1705 1705 try:
1706 1706 pattern = re.compile(r'%s' % entry['pat'])
1707 1707 except re.error:
1708 1708 log.exception(
1709 1709 'issue tracker pattern: `%s` failed to compile',
1710 1710 entry['pat'])
1711 1711 continue
1712 1712
1713 1713 data_func = partial(
1714 1714 _process_url_func, repo_name=repo_name, entry=entry, uid=uid,
1715 1715 return_raw_data=True)
1716 1716
1717 1717 for match_obj in pattern.finditer(text_string):
1718 1718 issues_data.append(data_func(match_obj))
1719 1719
1720 1720 url_func = partial(
1721 1721 _process_url_func, repo_name=repo_name, entry=entry, uid=uid,
1722 1722 link_format=link_format)
1723 1723
1724 1724 newtext = pattern.sub(url_func, newtext)
1725 1725 log.debug('processed prefix:uid `%s`', uid)
1726 1726
1727 1727 return newtext, issues_data
1728 1728
1729 1729
1730 1730 def urlify_commit_message(commit_text, repository=None, active_pattern_entries=None):
1731 1731 """
1732 1732 Parses given text message and makes proper links.
1733 1733 issues are linked to given issue-server, and rest is a commit link
1734 1734
1735 1735 :param commit_text:
1736 1736 :param repository:
1737 1737 """
1738 1738 def escaper(string):
1739 1739 return string.replace('<', '&lt;').replace('>', '&gt;')
1740 1740
1741 1741 newtext = escaper(commit_text)
1742 1742
1743 1743 # extract http/https links and make them real urls
1744 1744 newtext = urlify_text(newtext, safe=False)
1745 1745
1746 1746 # urlify commits - extract commit ids and make link out of them, if we have
1747 1747 # the scope of repository present.
1748 1748 if repository:
1749 1749 newtext = urlify_commits(newtext, repository)
1750 1750
1751 1751 # process issue tracker patterns
1752 1752 newtext, issues = process_patterns(newtext, repository or '',
1753 1753 active_entries=active_pattern_entries)
1754 1754
1755 1755 return literal(newtext)
1756 1756
1757 1757
1758 1758 def render_binary(repo_name, file_obj):
1759 1759 """
1760 1760 Choose how to render a binary file
1761 1761 """
1762 1762
1763 1763 filename = file_obj.name
1764 1764
1765 1765 # images
1766 1766 for ext in ['*.png', '*.jpg', '*.ico', '*.gif']:
1767 1767 if fnmatch.fnmatch(filename, pat=ext):
1768 1768 alt = escape(filename)
1769 1769 src = route_path(
1770 1770 'repo_file_raw', repo_name=repo_name,
1771 1771 commit_id=file_obj.commit.raw_id,
1772 1772 f_path=file_obj.path)
1773 1773 return literal(
1774 1774 '<img class="rendered-binary" alt="{}" src="{}">'.format(alt, src))
1775 1775
1776 1776
1777 1777 def renderer_from_filename(filename, exclude=None):
1778 1778 """
1779 1779 choose a renderer based on filename, this works only for text based files
1780 1780 """
1781 1781
1782 1782 # ipython
1783 1783 for ext in ['*.ipynb']:
1784 1784 if fnmatch.fnmatch(filename, pat=ext):
1785 1785 return 'jupyter'
1786 1786
1787 1787 is_markup = MarkupRenderer.renderer_from_filename(filename, exclude=exclude)
1788 1788 if is_markup:
1789 1789 return is_markup
1790 1790 return None
1791 1791
1792 1792
1793 1793 def render(source, renderer='rst', mentions=False, relative_urls=None,
1794 1794 repo_name=None):
1795 1795
1796 1796 def maybe_convert_relative_links(html_source):
1797 1797 if relative_urls:
1798 1798 return relative_links(html_source, relative_urls)
1799 1799 return html_source
1800 1800
1801 1801 if renderer == 'plain':
1802 1802 return literal(
1803 1803 MarkupRenderer.plain(source, leading_newline=False))
1804 1804
1805 1805 elif renderer == 'rst':
1806 1806 if repo_name:
1807 1807 # process patterns on comments if we pass in repo name
1808 1808 source, issues = process_patterns(
1809 1809 source, repo_name, link_format='rst')
1810 1810
1811 1811 return literal(
1812 1812 '<div class="rst-block">%s</div>' %
1813 1813 maybe_convert_relative_links(
1814 1814 MarkupRenderer.rst(source, mentions=mentions)))
1815 1815
1816 1816 elif renderer == 'markdown':
1817 1817 if repo_name:
1818 1818 # process patterns on comments if we pass in repo name
1819 1819 source, issues = process_patterns(
1820 1820 source, repo_name, link_format='markdown')
1821 1821
1822 1822 return literal(
1823 1823 '<div class="markdown-block">%s</div>' %
1824 1824 maybe_convert_relative_links(
1825 1825 MarkupRenderer.markdown(source, flavored=True,
1826 1826 mentions=mentions)))
1827 1827
1828 1828 elif renderer == 'jupyter':
1829 1829 return literal(
1830 1830 '<div class="ipynb">%s</div>' %
1831 1831 maybe_convert_relative_links(
1832 1832 MarkupRenderer.jupyter(source)))
1833 1833
1834 1834 # None means just show the file-source
1835 1835 return None
1836 1836
1837 1837
1838 1838 def commit_status(repo, commit_id):
1839 1839 return ChangesetStatusModel().get_status(repo, commit_id)
1840 1840
1841 1841
1842 1842 def commit_status_lbl(commit_status):
1843 1843 return dict(ChangesetStatus.STATUSES).get(commit_status)
1844 1844
1845 1845
1846 1846 def commit_time(repo_name, commit_id):
1847 1847 repo = Repository.get_by_repo_name(repo_name)
1848 1848 commit = repo.get_commit(commit_id=commit_id)
1849 1849 return commit.date
1850 1850
1851 1851
1852 1852 def get_permission_name(key):
1853 1853 return dict(Permission.PERMS).get(key)
1854 1854
1855 1855
1856 1856 def journal_filter_help(request):
1857 1857 _ = request.translate
1858 1858 from rhodecode.lib.audit_logger import ACTIONS
1859 1859 actions = '\n'.join(textwrap.wrap(', '.join(sorted(ACTIONS.keys())), 80))
1860 1860
1861 1861 return _(
1862 1862 'Example filter terms:\n' +
1863 1863 ' repository:vcs\n' +
1864 1864 ' username:marcin\n' +
1865 1865 ' username:(NOT marcin)\n' +
1866 1866 ' action:*push*\n' +
1867 1867 ' ip:127.0.0.1\n' +
1868 1868 ' date:20120101\n' +
1869 1869 ' date:[20120101100000 TO 20120102]\n' +
1870 1870 '\n' +
1871 1871 'Actions: {actions}\n' +
1872 1872 '\n' +
1873 1873 'Generate wildcards using \'*\' character:\n' +
1874 1874 ' "repository:vcs*" - search everything starting with \'vcs\'\n' +
1875 1875 ' "repository:*vcs*" - search for repository containing \'vcs\'\n' +
1876 1876 '\n' +
1877 1877 'Optional AND / OR operators in queries\n' +
1878 1878 ' "repository:vcs OR repository:test"\n' +
1879 1879 ' "username:test AND repository:test*"\n'
1880 1880 ).format(actions=actions)
1881 1881
1882 1882
1883 1883 def not_mapped_error(repo_name):
1884 1884 from rhodecode.translation import _
1885 1885 flash(_('%s repository is not mapped to db perhaps'
1886 1886 ' it was created or renamed from the filesystem'
1887 1887 ' please run the application again'
1888 1888 ' in order to rescan repositories') % repo_name, category='error')
1889 1889
1890 1890
1891 1891 def ip_range(ip_addr):
1892 1892 from rhodecode.model.db import UserIpMap
1893 1893 s, e = UserIpMap._get_ip_range(ip_addr)
1894 1894 return '%s - %s' % (s, e)
1895 1895
1896 1896
1897 1897 def form(url, method='post', needs_csrf_token=True, **attrs):
1898 1898 """Wrapper around webhelpers.tags.form to prevent CSRF attacks."""
1899 1899 if method.lower() != 'get' and needs_csrf_token:
1900 1900 raise Exception(
1901 1901 'Forms to POST/PUT/DELETE endpoints should have (in general) a ' +
1902 1902 'CSRF token. If the endpoint does not require such token you can ' +
1903 1903 'explicitly set the parameter needs_csrf_token to false.')
1904 1904
1905 1905 return wh_form(url, method=method, **attrs)
1906 1906
1907 1907
1908 1908 def secure_form(form_url, method="POST", multipart=False, **attrs):
1909 1909 """Start a form tag that points the action to an url. This
1910 1910 form tag will also include the hidden field containing
1911 1911 the auth token.
1912 1912
1913 1913 The url options should be given either as a string, or as a
1914 1914 ``url()`` function. The method for the form defaults to POST.
1915 1915
1916 1916 Options:
1917 1917
1918 1918 ``multipart``
1919 1919 If set to True, the enctype is set to "multipart/form-data".
1920 1920 ``method``
1921 1921 The method to use when submitting the form, usually either
1922 1922 "GET" or "POST". If "PUT", "DELETE", or another verb is used, a
1923 1923 hidden input with name _method is added to simulate the verb
1924 1924 over POST.
1925 1925
1926 1926 """
1927 1927 from webhelpers.pylonslib.secure_form import insecure_form
1928 1928
1929 1929 if 'request' in attrs:
1930 1930 session = attrs['request'].session
1931 1931 del attrs['request']
1932 1932 else:
1933 1933 raise ValueError(
1934 1934 'Calling this form requires request= to be passed as argument')
1935 1935
1936 1936 form = insecure_form(form_url, method, multipart, **attrs)
1937 1937 token = literal(
1938 1938 '<input type="hidden" id="{}" name="{}" value="{}">'.format(
1939 1939 csrf_token_key, csrf_token_key, get_csrf_token(session)))
1940 1940
1941 1941 return literal("%s\n%s" % (form, token))
1942 1942
1943 1943
1944 1944 def dropdownmenu(name, selected, options, enable_filter=False, **attrs):
1945 1945 select_html = select(name, selected, options, **attrs)
1946 1946
1947 1947 select2 = """
1948 1948 <script>
1949 1949 $(document).ready(function() {
1950 1950 $('#%s').select2({
1951 1951 containerCssClass: 'drop-menu %s',
1952 1952 dropdownCssClass: 'drop-menu-dropdown',
1953 1953 dropdownAutoWidth: true%s
1954 1954 });
1955 1955 });
1956 1956 </script>
1957 1957 """
1958 1958
1959 1959 filter_option = """,
1960 1960 minimumResultsForSearch: -1
1961 1961 """
1962 1962 input_id = attrs.get('id') or name
1963 1963 extra_classes = ' '.join(attrs.pop('extra_classes', []))
1964 1964 filter_enabled = "" if enable_filter else filter_option
1965 1965 select_script = literal(select2 % (input_id, extra_classes, filter_enabled))
1966 1966
1967 1967 return literal(select_html+select_script)
1968 1968
1969 1969
1970 1970 def get_visual_attr(tmpl_context_var, attr_name):
1971 1971 """
1972 1972 A safe way to get a variable from visual variable of template context
1973 1973
1974 1974 :param tmpl_context_var: instance of tmpl_context, usually present as `c`
1975 1975 :param attr_name: name of the attribute we fetch from the c.visual
1976 1976 """
1977 1977 visual = getattr(tmpl_context_var, 'visual', None)
1978 1978 if not visual:
1979 1979 return
1980 1980 else:
1981 1981 return getattr(visual, attr_name, None)
1982 1982
1983 1983
1984 1984 def get_last_path_part(file_node):
1985 1985 if not file_node.path:
1986 1986 return u'/'
1987 1987
1988 1988 path = safe_unicode(file_node.path.split('/')[-1])
1989 1989 return u'../' + path
1990 1990
1991 1991
1992 1992 def route_url(*args, **kwargs):
1993 1993 """
1994 1994 Wrapper around pyramids `route_url` (fully qualified url) function.
1995 1995 """
1996 1996 req = get_current_request()
1997 1997 return req.route_url(*args, **kwargs)
1998 1998
1999 1999
2000 2000 def route_path(*args, **kwargs):
2001 2001 """
2002 2002 Wrapper around pyramids `route_path` function.
2003 2003 """
2004 2004 req = get_current_request()
2005 2005 return req.route_path(*args, **kwargs)
2006 2006
2007 2007
2008 2008 def route_path_or_none(*args, **kwargs):
2009 2009 try:
2010 2010 return route_path(*args, **kwargs)
2011 2011 except KeyError:
2012 2012 return None
2013 2013
2014 2014
2015 2015 def current_route_path(request, **kw):
2016 2016 new_args = request.GET.mixed()
2017 2017 new_args.update(kw)
2018 2018 return request.current_route_path(_query=new_args)
2019 2019
2020 2020
2021 2021 def curl_api_example(method, args):
2022 2022 args_json = json.dumps(OrderedDict([
2023 2023 ('id', 1),
2024 2024 ('auth_token', 'SECRET'),
2025 2025 ('method', method),
2026 2026 ('args', args)
2027 2027 ]))
2028 2028
2029 2029 return "curl {api_url} -X POST -H 'content-type:text/plain' --data-binary '{args_json}'".format(
2030 2030 api_url=route_url('apiv2'),
2031 2031 args_json=args_json
2032 2032 )
2033 2033
2034 2034
2035 2035 def api_call_example(method, args):
2036 2036 """
2037 2037 Generates an API call example via CURL
2038 2038 """
2039 2039 curl_call = curl_api_example(method, args)
2040 2040
2041 2041 return literal(
2042 2042 curl_call +
2043 2043 "<br/><br/>SECRET can be found in <a href=\"{token_url}\">auth-tokens</a> page, "
2044 2044 "and needs to be of `api calls` role."
2045 2045 .format(token_url=route_url('my_account_auth_tokens')))
2046 2046
2047 2047
2048 2048 def notification_description(notification, request):
2049 2049 """
2050 2050 Generate notification human readable description based on notification type
2051 2051 """
2052 2052 from rhodecode.model.notification import NotificationModel
2053 2053 return NotificationModel().make_description(
2054 2054 notification, translate=request.translate)
2055 2055
2056 2056
2057 2057 def go_import_header(request, db_repo=None):
2058 2058 """
2059 2059 Creates a header for go-import functionality in Go Lang
2060 2060 """
2061 2061
2062 2062 if not db_repo:
2063 2063 return
2064 2064 if 'go-get' not in request.GET:
2065 2065 return
2066 2066
2067 2067 clone_url = db_repo.clone_url()
2068 2068 prefix = re.split(r'^https?:\/\/', clone_url)[-1]
2069 2069 # we have a repo and go-get flag,
2070 2070 return literal('<meta name="go-import" content="{} {} {}">'.format(
2071 2071 prefix, db_repo.repo_type, clone_url))
2072 2072
2073 2073
2074 2074 def reviewer_as_json(*args, **kwargs):
2075 2075 from rhodecode.apps.repository.utils import reviewer_as_json as _reviewer_as_json
2076 2076 return _reviewer_as_json(*args, **kwargs)
2077 2077
2078 2078
2079 2079 def get_repo_view_type(request):
2080 2080 route_name = request.matched_route.name
2081 2081 route_to_view_type = {
2082 2082 'repo_changelog': 'commits',
2083 2083 'repo_commits': 'commits',
2084 2084 'repo_files': 'files',
2085 2085 'repo_summary': 'summary',
2086 2086 'repo_commit': 'commit'
2087 2087 }
2088 2088
2089 2089 return route_to_view_type.get(route_name)
@@ -1,2821 +1,2897 b''
1 1 //Primary CSS
2 2
3 3 //--- IMPORTS ------------------//
4 4
5 5 @import 'helpers';
6 6 @import 'mixins';
7 7 @import 'rcicons';
8 8 @import 'variables';
9 9 @import 'bootstrap-variables';
10 10 @import 'form-bootstrap';
11 11 @import 'codemirror';
12 12 @import 'legacy_code_styles';
13 13 @import 'readme-box';
14 14 @import 'progress-bar';
15 15
16 16 @import 'type';
17 17 @import 'alerts';
18 18 @import 'buttons';
19 19 @import 'tags';
20 20 @import 'code-block';
21 21 @import 'examples';
22 22 @import 'login';
23 23 @import 'main-content';
24 24 @import 'select2';
25 25 @import 'comments';
26 26 @import 'panels-bootstrap';
27 27 @import 'panels';
28 28 @import 'deform';
29 @import 'tooltips';
29 30
30 31 //--- BASE ------------------//
31 32 .noscript-error {
32 33 top: 0;
33 34 left: 0;
34 35 width: 100%;
35 36 z-index: 101;
36 37 text-align: center;
37 38 font-size: 120%;
38 39 color: white;
39 40 background-color: @alert2;
40 41 padding: 5px 0 5px 0;
41 42 font-weight: @text-semibold-weight;
42 43 font-family: @text-semibold;
43 44 }
44 45
45 46 html {
46 47 display: table;
47 48 height: 100%;
48 49 width: 100%;
49 50 }
50 51
51 52 body {
52 53 display: table-cell;
53 54 width: 100%;
54 55 }
55 56
56 57 //--- LAYOUT ------------------//
57 58
58 59 .hidden{
59 60 display: none !important;
60 61 }
61 62
62 63 .box{
63 64 float: left;
64 65 width: 100%;
65 66 }
66 67
67 68 .browser-header {
68 69 clear: both;
69 70 }
70 71 .main {
71 72 clear: both;
72 73 padding:0 0 @pagepadding;
73 74 height: auto;
74 75
75 76 &:after { //clearfix
76 77 content:"";
77 78 clear:both;
78 79 width:100%;
79 80 display:block;
80 81 }
81 82 }
82 83
83 84 .action-link{
84 85 margin-left: @padding;
85 86 padding-left: @padding;
86 87 border-left: @border-thickness solid @border-default-color;
87 88 }
88 89
89 90 input + .action-link, .action-link.first{
90 91 border-left: none;
91 92 }
92 93
93 94 .action-link.last{
94 95 margin-right: @padding;
95 96 padding-right: @padding;
96 97 }
97 98
98 99 .action-link.active,
99 100 .action-link.active a{
100 101 color: @grey4;
101 102 }
102 103
103 104 .action-link.disabled {
104 105 color: @grey4;
105 106 cursor: inherit;
106 107 }
107 108
108 109 .clipboard-action {
109 110 cursor: pointer;
110 111 color: @grey4;
111 112 margin-left: 5px;
112 113
113 114 &:hover {
114 115 color: @grey2;
115 116 }
116 117 }
117 118
118 119 ul.simple-list{
119 120 list-style: none;
120 121 margin: 0;
121 122 padding: 0;
122 123 }
123 124
124 125 .main-content {
125 126 padding-bottom: @pagepadding;
126 127 }
127 128
128 129 .wide-mode-wrapper {
129 130 max-width:4000px !important;
130 131 }
131 132
132 133 .wrapper {
133 134 position: relative;
134 135 max-width: @wrapper-maxwidth;
135 136 margin: 0 auto;
136 137 }
137 138
138 139 #content {
139 140 clear: both;
140 141 padding: 0 @contentpadding;
141 142 }
142 143
143 144 .advanced-settings-fields{
144 145 input{
145 146 margin-left: @textmargin;
146 147 margin-right: @padding/2;
147 148 }
148 149 }
149 150
150 151 .cs_files_title {
151 152 margin: @pagepadding 0 0;
152 153 }
153 154
154 155 input.inline[type="file"] {
155 156 display: inline;
156 157 }
157 158
158 159 .error_page {
159 160 margin: 10% auto;
160 161
161 162 h1 {
162 163 color: @grey2;
163 164 }
164 165
165 166 .alert {
166 167 margin: @padding 0;
167 168 }
168 169
169 170 .error-branding {
170 171 color: @grey4;
171 172 font-weight: @text-semibold-weight;
172 173 font-family: @text-semibold;
173 174 }
174 175
175 176 .error_message {
176 177 font-family: @text-regular;
177 178 }
178 179
179 180 .sidebar {
180 181 min-height: 275px;
181 182 margin: 0;
182 183 padding: 0 0 @sidebarpadding @sidebarpadding;
183 184 border: none;
184 185 }
185 186
186 187 .main-content {
187 188 position: relative;
188 189 margin: 0 @sidebarpadding @sidebarpadding;
189 190 padding: 0 0 0 @sidebarpadding;
190 191 border-left: @border-thickness solid @grey5;
191 192
192 193 @media (max-width:767px) {
193 194 clear: both;
194 195 width: 100%;
195 196 margin: 0;
196 197 border: none;
197 198 }
198 199 }
199 200
200 201 .inner-column {
201 202 float: left;
202 203 width: 29.75%;
203 204 min-height: 150px;
204 205 margin: @sidebarpadding 2% 0 0;
205 206 padding: 0 2% 0 0;
206 207 border-right: @border-thickness solid @grey5;
207 208
208 209 @media (max-width:767px) {
209 210 clear: both;
210 211 width: 100%;
211 212 border: none;
212 213 }
213 214
214 215 ul {
215 216 padding-left: 1.25em;
216 217 }
217 218
218 219 &:last-child {
219 220 margin: @sidebarpadding 0 0;
220 221 border: none;
221 222 }
222 223
223 224 h4 {
224 225 margin: 0 0 @padding;
225 226 font-weight: @text-semibold-weight;
226 227 font-family: @text-semibold;
227 228 }
228 229 }
229 230 }
230 231 .error-page-logo {
231 232 width: 130px;
232 233 height: 160px;
233 234 }
234 235
235 236 // HEADER
236 237 .header {
237 238
238 239 // TODO: johbo: Fix login pages, so that they work without a min-height
239 240 // for the header and then remove the min-height. I chose a smaller value
240 241 // intentionally here to avoid rendering issues in the main navigation.
241 242 min-height: 49px;
242 243 min-width: 1024px;
243 244
244 245 position: relative;
245 246 vertical-align: bottom;
246 247 padding: 0 @header-padding;
247 248 background-color: @grey1;
248 249 color: @grey5;
249 250
250 251 .title {
251 252 overflow: visible;
252 253 }
253 254
254 255 &:before,
255 256 &:after {
256 257 content: "";
257 258 clear: both;
258 259 width: 100%;
259 260 }
260 261
261 262 // TODO: johbo: Avoids breaking "Repositories" chooser
262 263 .select2-container .select2-choice .select2-arrow {
263 264 display: none;
264 265 }
265 266 }
266 267
267 268 #header-inner {
268 269 &.title {
269 270 margin: 0;
270 271 }
271 272 &:before,
272 273 &:after {
273 274 content: "";
274 275 clear: both;
275 276 }
276 277 }
277 278
278 279 // Gists
279 280 #files_data {
280 281 clear: both; //for firefox
281 282 padding-top: 10px;
282 283 }
283 284
284 285 #gistid {
285 286 margin-right: @padding;
286 287 }
287 288
288 289 // Global Settings Editor
289 290 .textarea.editor {
290 291 float: left;
291 292 position: relative;
292 293 max-width: @texteditor-width;
293 294
294 295 select {
295 296 position: absolute;
296 297 top:10px;
297 298 right:0;
298 299 }
299 300
300 301 .CodeMirror {
301 302 margin: 0;
302 303 }
303 304
304 305 .help-block {
305 306 margin: 0 0 @padding;
306 307 padding:.5em;
307 308 background-color: @grey6;
308 309 &.pre-formatting {
309 310 white-space: pre;
310 311 }
311 312 }
312 313 }
313 314
314 315 ul.auth_plugins {
315 316 margin: @padding 0 @padding @legend-width;
316 317 padding: 0;
317 318
318 319 li {
319 320 margin-bottom: @padding;
320 321 line-height: 1em;
321 322 list-style-type: none;
322 323
323 324 .auth_buttons .btn {
324 325 margin-right: @padding;
325 326 }
326 327
327 328 }
328 329 }
329 330
330 331
331 332 // My Account PR list
332 333
333 334 #show_closed {
334 335 margin: 0 1em 0 0;
335 336 }
336 337
337 338 #pull_request_list_table {
338 339 .closed {
339 340 background-color: @grey6;
340 341 }
341 342
342 343 .state-creating,
343 344 .state-updating,
344 345 .state-merging
345 346 {
346 347 background-color: @grey6;
347 348 }
348 349
349 350 .td-status {
350 351 padding-left: .5em;
351 352 }
352 353 .log-container .truncate {
353 354 height: 2.75em;
354 355 white-space: pre-line;
355 356 }
356 357 table.rctable .user {
357 358 padding-left: 0;
358 359 }
359 360 table.rctable {
360 361 td.td-description,
361 362 .rc-user {
362 363 min-width: auto;
363 364 }
364 365 }
365 366 }
366 367
367 368 // Pull Requests
368 369
369 370 .pullrequests_section_head {
370 371 display: block;
371 372 clear: both;
372 373 margin: @padding 0;
373 374 font-weight: @text-bold-weight;
374 375 font-family: @text-bold;
375 376 }
376 377
377 378 .pr-origininfo, .pr-targetinfo {
378 379 position: relative;
379 380
380 381 .tag {
381 382 display: inline-block;
382 383 margin: 0 1em .5em 0;
383 384 }
384 385
385 386 .clone-url {
386 387 display: inline-block;
387 388 margin: 0 0 .5em 0;
388 389 padding: 0;
389 390 line-height: 1.2em;
390 391 }
391 392 }
392 393
393 394 .pr-mergeinfo {
394 395 min-width: 95% !important;
395 396 padding: 0 !important;
396 397 border: 0;
397 398 }
398 399 .pr-mergeinfo-copy {
399 400 padding: 0 0;
400 401 }
401 402
402 403 .pr-pullinfo {
403 404 min-width: 95% !important;
404 405 padding: 0 !important;
405 406 border: 0;
406 407 }
407 408 .pr-pullinfo-copy {
408 409 padding: 0 0;
409 410 }
410 411
411 412
412 413 #pr-title-input {
413 414 width: 72%;
414 415 font-size: 1em;
415 416 margin: 0;
416 417 padding: 0 0 0 @padding/4;
417 418 line-height: 1.7em;
418 419 color: @text-color;
419 420 letter-spacing: .02em;
420 421 font-weight: @text-bold-weight;
421 422 font-family: @text-bold;
422 423 }
423 424
424 425 #pullrequest_title {
425 426 width: 100%;
426 427 box-sizing: border-box;
427 428 }
428 429
429 430 #pr_open_message {
430 431 border: @border-thickness solid #fff;
431 432 border-radius: @border-radius;
432 433 padding: @padding-large-vertical @padding-large-vertical @padding-large-vertical 0;
433 434 text-align: left;
434 435 overflow: hidden;
435 436 }
436 437
437 438 .pr-submit-button {
438 439 float: right;
439 440 margin: 0 0 0 5px;
440 441 }
441 442
442 443 .pr-spacing-container {
443 444 padding: 20px;
444 445 clear: both
445 446 }
446 447
447 448 #pr-description-input {
448 449 margin-bottom: 0;
449 450 }
450 451
451 452 .pr-description-label {
452 453 vertical-align: top;
453 454 }
454 455
455 456 .perms_section_head {
456 457 min-width: 625px;
457 458
458 459 h2 {
459 460 margin-bottom: 0;
460 461 }
461 462
462 463 .label-checkbox {
463 464 float: left;
464 465 }
465 466
466 467 &.field {
467 468 margin: @space 0 @padding;
468 469 }
469 470
470 471 &:first-child.field {
471 472 margin-top: 0;
472 473
473 474 .label {
474 475 margin-top: 0;
475 476 padding-top: 0;
476 477 }
477 478
478 479 .radios {
479 480 padding-top: 0;
480 481 }
481 482 }
482 483
483 484 .radios {
484 485 position: relative;
485 486 width: 505px;
486 487 }
487 488 }
488 489
489 490 //--- MODULES ------------------//
490 491
491 492
492 493 // Server Announcement
493 494 #server-announcement {
494 495 width: 95%;
495 496 margin: @padding auto;
496 497 padding: @padding;
497 498 border-width: 2px;
498 499 border-style: solid;
499 500 .border-radius(2px);
500 501 font-weight: @text-bold-weight;
501 502 font-family: @text-bold;
502 503
503 504 &.info { border-color: @alert4; background-color: @alert4-inner; }
504 505 &.warning { border-color: @alert3; background-color: @alert3-inner; }
505 506 &.error { border-color: @alert2; background-color: @alert2-inner; }
506 507 &.success { border-color: @alert1; background-color: @alert1-inner; }
507 508 &.neutral { border-color: @grey3; background-color: @grey6; }
508 509 }
509 510
510 511 // Fixed Sidebar Column
511 512 .sidebar-col-wrapper {
512 513 padding-left: @sidebar-all-width;
513 514
514 515 .sidebar {
515 516 width: @sidebar-width;
516 517 margin-left: -@sidebar-all-width;
517 518 }
518 519 }
519 520
520 521 .sidebar-col-wrapper.scw-small {
521 522 padding-left: @sidebar-small-all-width;
522 523
523 524 .sidebar {
524 525 width: @sidebar-small-width;
525 526 margin-left: -@sidebar-small-all-width;
526 527 }
527 528 }
528 529
529 530
530 531 // FOOTER
531 532 #footer {
532 533 padding: 0;
533 534 text-align: center;
534 535 vertical-align: middle;
535 536 color: @grey2;
536 537 font-size: 11px;
537 538
538 539 p {
539 540 margin: 0;
540 541 padding: 1em;
541 542 line-height: 1em;
542 543 }
543 544
544 545 .server-instance { //server instance
545 546 display: none;
546 547 }
547 548
548 549 .title {
549 550 float: none;
550 551 margin: 0 auto;
551 552 }
552 553 }
553 554
554 555 button.close {
555 556 padding: 0;
556 557 cursor: pointer;
557 558 background: transparent;
558 559 border: 0;
559 560 .box-shadow(none);
560 561 -webkit-appearance: none;
561 562 }
562 563
563 564 .close {
564 565 float: right;
565 566 font-size: 21px;
566 567 font-family: @text-bootstrap;
567 568 line-height: 1em;
568 569 font-weight: bold;
569 570 color: @grey2;
570 571
571 572 &:hover,
572 573 &:focus {
573 574 color: @grey1;
574 575 text-decoration: none;
575 576 cursor: pointer;
576 577 }
577 578 }
578 579
579 580 // GRID
580 581 .sorting,
581 582 .sorting_desc,
582 583 .sorting_asc {
583 584 cursor: pointer;
584 585 }
585 586 .sorting_desc:after {
586 587 content: "\00A0\25B2";
587 588 font-size: .75em;
588 589 }
589 590 .sorting_asc:after {
590 591 content: "\00A0\25BC";
591 592 font-size: .68em;
592 593 }
593 594
594 595
595 596 .user_auth_tokens {
596 597
597 598 &.truncate {
598 599 white-space: nowrap;
599 600 overflow: hidden;
600 601 text-overflow: ellipsis;
601 602 }
602 603
603 604 .fields .field .input {
604 605 margin: 0;
605 606 }
606 607
607 608 input#description {
608 609 width: 100px;
609 610 margin: 0;
610 611 }
611 612
612 613 .drop-menu {
613 614 // TODO: johbo: Remove this, should work out of the box when
614 615 // having multiple inputs inline
615 616 margin: 0 0 0 5px;
616 617 }
617 618 }
618 619 #user_list_table {
619 620 .closed {
620 621 background-color: @grey6;
621 622 }
622 623 }
623 624
624 625
625 626 input, textarea {
626 627 &.disabled {
627 628 opacity: .5;
628 629 }
629 630
630 631 &:hover {
631 632 border-color: @grey3;
632 633 box-shadow: @button-shadow;
633 634 }
634 635
635 636 &:focus {
636 637 border-color: @rcblue;
637 638 box-shadow: @button-shadow;
638 639 }
639 640 }
640 641
641 642 // remove extra padding in firefox
642 643 input::-moz-focus-inner { border:0; padding:0 }
643 644
644 645 .adjacent input {
645 646 margin-bottom: @padding;
646 647 }
647 648
648 649 .permissions_boxes {
649 650 display: block;
650 651 }
651 652
652 653 //FORMS
653 654
654 655 .medium-inline,
655 656 input#description.medium-inline {
656 657 display: inline;
657 658 width: @medium-inline-input-width;
658 659 min-width: 100px;
659 660 }
660 661
661 662 select {
662 663 //reset
663 664 -webkit-appearance: none;
664 665 -moz-appearance: none;
665 666
666 667 display: inline-block;
667 668 height: 28px;
668 669 width: auto;
669 670 margin: 0 @padding @padding 0;
670 671 padding: 0 18px 0 8px;
671 672 line-height:1em;
672 673 font-size: @basefontsize;
673 674 border: @border-thickness solid @grey5;
674 675 border-radius: @border-radius;
675 676 background:white url("../images/dt-arrow-dn.png") no-repeat 100% 50%;
676 677 color: @grey4;
677 678 box-shadow: @button-shadow;
678 679
679 680 &:after {
680 681 content: "\00A0\25BE";
681 682 }
682 683
683 684 &:focus, &:hover {
684 685 outline: none;
685 686 border-color: @grey4;
686 687 color: @rcdarkblue;
687 688 }
688 689 }
689 690
690 691 option {
691 692 &:focus {
692 693 outline: none;
693 694 }
694 695 }
695 696
696 697 input,
697 698 textarea {
698 699 padding: @input-padding;
699 700 border: @input-border-thickness solid @border-highlight-color;
700 701 .border-radius (@border-radius);
701 702 font-family: @text-light;
702 703 font-size: @basefontsize;
703 704
704 705 &.input-sm {
705 706 padding: 5px;
706 707 }
707 708
708 709 &#description {
709 710 min-width: @input-description-minwidth;
710 711 min-height: 1em;
711 712 padding: 10px;
712 713 }
713 714 }
714 715
715 716 .field-sm {
716 717 input,
717 718 textarea {
718 719 padding: 5px;
719 720 }
720 721 }
721 722
722 723 textarea {
723 724 display: block;
724 725 clear: both;
725 726 width: 100%;
726 727 min-height: 100px;
727 728 margin-bottom: @padding;
728 729 .box-sizing(border-box);
729 730 overflow: auto;
730 731 }
731 732
732 733 label {
733 734 font-family: @text-light;
734 735 }
735 736
736 737 // GRAVATARS
737 738 // centers gravatar on username to the right
738 739
739 740 .gravatar {
740 741 display: inline;
741 742 min-width: 16px;
742 743 min-height: 16px;
743 744 margin: -5px 0;
744 745 padding: 0;
745 746 line-height: 1em;
746 747 box-sizing: content-box;
747 748 border-radius: 50%;
748 749
749 750 &.gravatar-large {
750 751 margin: -0.5em .25em -0.5em 0;
751 752 }
752 753
753 754 & + .user {
754 755 display: inline;
755 756 margin: 0;
756 757 padding: 0 0 0 .17em;
757 758 line-height: 1em;
758 759 }
759 760 }
760 761
761 762 .user-inline-data {
762 763 display: inline-block;
763 764 float: left;
764 765 padding-left: .5em;
765 766 line-height: 1.3em;
766 767 }
767 768
768 769 .rc-user { // gravatar + user wrapper
769 770 float: left;
770 771 position: relative;
771 772 min-width: 100px;
772 773 max-width: 200px;
773 774 min-height: (@gravatar-size + @border-thickness * 2); // account for border
774 775 display: block;
775 776 padding: 0 0 0 (@gravatar-size + @basefontsize/2 + @border-thickness * 2);
776 777
777 778
778 779 .gravatar {
779 780 display: block;
780 781 position: absolute;
781 782 top: 0;
782 783 left: 0;
783 784 min-width: @gravatar-size;
784 785 min-height: @gravatar-size;
785 786 margin: 0;
786 787 }
787 788
788 789 .user {
789 790 display: block;
790 791 max-width: 175px;
791 792 padding-top: 2px;
792 793 overflow: hidden;
793 794 text-overflow: ellipsis;
794 795 }
795 796 }
796 797
797 798 .gist-gravatar,
798 799 .journal_container {
799 800 .gravatar-large {
800 801 margin: 0 .5em -10px 0;
801 802 }
802 803 }
803 804
804 805
805 806 // ADMIN SETTINGS
806 807
807 808 // Tag Patterns
808 809 .tag_patterns {
809 810 .tag_input {
810 811 margin-bottom: @padding;
811 812 }
812 813 }
813 814
814 815 .locked_input {
815 816 position: relative;
816 817
817 818 input {
818 819 display: inline;
819 820 margin: 3px 5px 0px 0px;
820 821 }
821 822
822 823 br {
823 824 display: none;
824 825 }
825 826
826 827 .error-message {
827 828 float: left;
828 829 width: 100%;
829 830 }
830 831
831 832 .lock_input_button {
832 833 display: inline;
833 834 }
834 835
835 836 .help-block {
836 837 clear: both;
837 838 }
838 839 }
839 840
840 841 // Notifications
841 842
842 843 .notifications_buttons {
843 844 margin: 0 0 @space 0;
844 845 padding: 0;
845 846
846 847 .btn {
847 848 display: inline-block;
848 849 }
849 850 }
850 851
851 852 .notification-list {
852 853
853 854 div {
854 855 display: inline-block;
855 856 vertical-align: middle;
856 857 }
857 858
858 859 .container {
859 860 display: block;
860 861 margin: 0 0 @padding 0;
861 862 }
862 863
863 864 .delete-notifications {
864 865 margin-left: @padding;
865 866 text-align: right;
866 867 cursor: pointer;
867 868 }
868 869
869 870 .read-notifications {
870 871 margin-left: @padding/2;
871 872 text-align: right;
872 873 width: 35px;
873 874 cursor: pointer;
874 875 }
875 876
876 877 .icon-minus-sign {
877 878 color: @alert2;
878 879 }
879 880
880 881 .icon-ok-sign {
881 882 color: @alert1;
882 883 }
883 884 }
884 885
885 886 .user_settings {
886 887 float: left;
887 888 clear: both;
888 889 display: block;
889 890 width: 100%;
890 891
891 892 .gravatar_box {
892 893 margin-bottom: @padding;
893 894
894 895 &:after {
895 896 content: " ";
896 897 clear: both;
897 898 width: 100%;
898 899 }
899 900 }
900 901
901 902 .fields .field {
902 903 clear: both;
903 904 }
904 905 }
905 906
906 907 .advanced_settings {
907 908 margin-bottom: @space;
908 909
909 910 .help-block {
910 911 margin-left: 0;
911 912 }
912 913
913 914 button + .help-block {
914 915 margin-top: @padding;
915 916 }
916 917 }
917 918
918 919 // admin settings radio buttons and labels
919 920 .label-2 {
920 921 float: left;
921 922 width: @label2-width;
922 923
923 924 label {
924 925 color: @grey1;
925 926 }
926 927 }
927 928 .checkboxes {
928 929 float: left;
929 930 width: @checkboxes-width;
930 931 margin-bottom: @padding;
931 932
932 933 .checkbox {
933 934 width: 100%;
934 935
935 936 label {
936 937 margin: 0;
937 938 padding: 0;
938 939 }
939 940 }
940 941
941 942 .checkbox + .checkbox {
942 943 display: inline-block;
943 944 }
944 945
945 946 label {
946 947 margin-right: 1em;
947 948 }
948 949 }
949 950
950 951 // CHANGELOG
951 952 .container_header {
952 953 float: left;
953 954 display: block;
954 955 width: 100%;
955 956 margin: @padding 0 @padding;
956 957
957 958 #filter_changelog {
958 959 float: left;
959 960 margin-right: @padding;
960 961 }
961 962
962 963 .breadcrumbs_light {
963 964 display: inline-block;
964 965 }
965 966 }
966 967
967 968 .info_box {
968 969 float: right;
969 970 }
970 971
971 972
972 973
973 974 #graph_content{
974 975
975 976 // adjust for table headers so that graph renders properly
976 977 // #graph_nodes padding - table cell padding
977 978 padding-top: (@space - (@basefontsize * 2.4));
978 979
979 980 &.graph_full_width {
980 981 width: 100%;
981 982 max-width: 100%;
982 983 }
983 984 }
984 985
985 986 #graph {
986 987
987 988 .pagination-left {
988 989 float: left;
989 990 clear: both;
990 991 }
991 992
992 993 .log-container {
993 994 max-width: 345px;
994 995
995 996 .message{
996 997 max-width: 340px;
997 998 }
998 999 }
999 1000
1000 1001 .graph-col-wrapper {
1001 1002
1002 1003 #graph_nodes {
1003 1004 width: 100px;
1004 1005 position: absolute;
1005 1006 left: 70px;
1006 1007 z-index: -1;
1007 1008 }
1008 1009 }
1009 1010
1010 1011 .load-more-commits {
1011 1012 text-align: center;
1012 1013 }
1013 1014 .load-more-commits:hover {
1014 1015 background-color: @grey7;
1015 1016 }
1016 1017 .load-more-commits {
1017 1018 a {
1018 1019 display: block;
1019 1020 }
1020 1021 }
1021 1022 }
1022 1023
1023 1024 .obsolete-toggle {
1024 1025 line-height: 30px;
1025 1026 margin-left: -15px;
1026 1027 }
1027 1028
1028 1029 #rev_range_container, #rev_range_clear, #rev_range_more {
1029 1030 margin-top: -5px;
1030 1031 margin-bottom: -5px;
1031 1032 }
1032 1033
1033 1034 #filter_changelog {
1034 1035 float: left;
1035 1036 }
1036 1037
1037 1038
1038 1039 //--- THEME ------------------//
1039 1040
1040 1041 #logo {
1041 1042 float: left;
1042 1043 margin: 9px 0 0 0;
1043 1044
1044 1045 .header {
1045 1046 background-color: transparent;
1046 1047 }
1047 1048
1048 1049 a {
1049 1050 display: inline-block;
1050 1051 }
1051 1052
1052 1053 img {
1053 1054 height:30px;
1054 1055 }
1055 1056 }
1056 1057
1057 1058 .logo-wrapper {
1058 1059 float:left;
1059 1060 }
1060 1061
1061 1062 .branding {
1062 1063 float: left;
1063 1064 padding: 9px 2px;
1064 1065 line-height: 1em;
1065 1066 font-size: @navigation-fontsize;
1066 1067
1067 1068 a {
1068 1069 color: @grey5
1069 1070 }
1070 1071 @media screen and (max-width: 1200px) {
1071 1072 display: none;
1072 1073 }
1073 1074 }
1074 1075
1075 1076 img {
1076 1077 border: none;
1077 1078 outline: none;
1078 1079 }
1079 1080 user-profile-header
1080 1081 label {
1081 1082
1082 1083 input[type="checkbox"] {
1083 1084 margin-right: 1em;
1084 1085 }
1085 1086 input[type="radio"] {
1086 1087 margin-right: 1em;
1087 1088 }
1088 1089 }
1089 1090
1090 1091 .review-status {
1091 1092 &.under_review {
1092 1093 color: @alert3;
1093 1094 }
1094 1095 &.approved {
1095 1096 color: @alert1;
1096 1097 }
1097 1098 &.rejected,
1098 1099 &.forced_closed{
1099 1100 color: @alert2;
1100 1101 }
1101 1102 &.not_reviewed {
1102 1103 color: @grey5;
1103 1104 }
1104 1105 }
1105 1106
1106 1107 .review-status-under_review {
1107 1108 color: @alert3;
1108 1109 }
1109 1110 .status-tag-under_review {
1110 1111 border-color: @alert3;
1111 1112 }
1112 1113
1113 1114 .review-status-approved {
1114 1115 color: @alert1;
1115 1116 }
1116 1117 .status-tag-approved {
1117 1118 border-color: @alert1;
1118 1119 }
1119 1120
1120 1121 .review-status-rejected,
1121 1122 .review-status-forced_closed {
1122 1123 color: @alert2;
1123 1124 }
1124 1125 .status-tag-rejected,
1125 1126 .status-tag-forced_closed {
1126 1127 border-color: @alert2;
1127 1128 }
1128 1129
1129 1130 .review-status-not_reviewed {
1130 1131 color: @grey5;
1131 1132 }
1132 1133 .status-tag-not_reviewed {
1133 1134 border-color: @grey5;
1134 1135 }
1135 1136
1136 1137 .test_pattern_preview {
1137 1138 margin: @space 0;
1138 1139
1139 1140 p {
1140 1141 margin-bottom: 0;
1141 1142 border-bottom: @border-thickness solid @border-default-color;
1142 1143 color: @grey3;
1143 1144 }
1144 1145
1145 1146 .btn {
1146 1147 margin-bottom: @padding;
1147 1148 }
1148 1149 }
1149 1150 #test_pattern_result {
1150 1151 display: none;
1151 1152 &:extend(pre);
1152 1153 padding: .9em;
1153 1154 color: @grey3;
1154 1155 background-color: @grey7;
1155 1156 border-right: @border-thickness solid @border-default-color;
1156 1157 border-bottom: @border-thickness solid @border-default-color;
1157 1158 border-left: @border-thickness solid @border-default-color;
1158 1159 }
1159 1160
1160 1161 #repo_vcs_settings {
1161 1162 #inherit_overlay_vcs_default {
1162 1163 display: none;
1163 1164 }
1164 1165 #inherit_overlay_vcs_custom {
1165 1166 display: custom;
1166 1167 }
1167 1168 &.inherited {
1168 1169 #inherit_overlay_vcs_default {
1169 1170 display: block;
1170 1171 }
1171 1172 #inherit_overlay_vcs_custom {
1172 1173 display: none;
1173 1174 }
1174 1175 }
1175 1176 }
1176 1177
1177 1178 .issue-tracker-link {
1178 1179 color: @rcblue;
1179 1180 }
1180 1181
1181 1182 // Issue Tracker Table Show/Hide
1182 1183 #repo_issue_tracker {
1183 1184 #inherit_overlay {
1184 1185 display: none;
1185 1186 }
1186 1187 #custom_overlay {
1187 1188 display: custom;
1188 1189 }
1189 1190 &.inherited {
1190 1191 #inherit_overlay {
1191 1192 display: block;
1192 1193 }
1193 1194 #custom_overlay {
1194 1195 display: none;
1195 1196 }
1196 1197 }
1197 1198 }
1198 1199 table.issuetracker {
1199 1200 &.readonly {
1200 1201 tr, td {
1201 1202 color: @grey3;
1202 1203 }
1203 1204 }
1204 1205 .edit {
1205 1206 display: none;
1206 1207 }
1207 1208 .editopen {
1208 1209 .edit {
1209 1210 display: inline;
1210 1211 }
1211 1212 .entry {
1212 1213 display: none;
1213 1214 }
1214 1215 }
1215 1216 tr td.td-action {
1216 1217 min-width: 117px;
1217 1218 }
1218 1219 td input {
1219 1220 max-width: none;
1220 1221 min-width: 30px;
1221 1222 width: 80%;
1222 1223 }
1223 1224 .issuetracker_pref input {
1224 1225 width: 40%;
1225 1226 }
1226 1227 input.edit_issuetracker_update {
1227 1228 margin-right: 0;
1228 1229 width: auto;
1229 1230 }
1230 1231 }
1231 1232
1232 1233 table.integrations {
1233 1234 .td-icon {
1234 1235 width: 20px;
1235 1236 .integration-icon {
1236 1237 height: 20px;
1237 1238 width: 20px;
1238 1239 }
1239 1240 }
1240 1241 }
1241 1242
1242 1243 .integrations {
1243 1244 a.integration-box {
1244 1245 color: @text-color;
1245 1246 &:hover {
1246 1247 .panel {
1247 1248 background: #fbfbfb;
1248 1249 }
1249 1250 }
1250 1251 .integration-icon {
1251 1252 width: 30px;
1252 1253 height: 30px;
1253 1254 margin-right: 20px;
1254 1255 float: left;
1255 1256 }
1256 1257
1257 1258 .panel-body {
1258 1259 padding: 10px;
1259 1260 }
1260 1261 .panel {
1261 1262 margin-bottom: 10px;
1262 1263 }
1263 1264 h2 {
1264 1265 display: inline-block;
1265 1266 margin: 0;
1266 1267 min-width: 140px;
1267 1268 }
1268 1269 }
1269 1270 a.integration-box.dummy-integration {
1270 1271 color: @grey4
1271 1272 }
1272 1273 }
1273 1274
1274 1275 //Permissions Settings
1275 1276 #add_perm {
1276 1277 margin: 0 0 @padding;
1277 1278 cursor: pointer;
1278 1279 }
1279 1280
1280 1281 .perm_ac {
1281 1282 input {
1282 1283 width: 95%;
1283 1284 }
1284 1285 }
1285 1286
1286 1287 .autocomplete-suggestions {
1287 1288 width: auto !important; // overrides autocomplete.js
1288 1289 min-width: 278px;
1289 1290 margin: 0;
1290 1291 border: @border-thickness solid @grey5;
1291 1292 border-radius: @border-radius;
1292 1293 color: @grey2;
1293 1294 background-color: white;
1294 1295 }
1295 1296
1296 1297 .autocomplete-qfilter-suggestions {
1297 1298 width: auto !important; // overrides autocomplete.js
1298 1299 max-height: 100% !important;
1299 1300 min-width: 376px;
1300 1301 margin: 0;
1301 1302 border: @border-thickness solid @grey5;
1302 1303 color: @grey2;
1303 1304 background-color: white;
1304 1305 }
1305 1306
1306 1307 .autocomplete-selected {
1307 1308 background: #F0F0F0;
1308 1309 }
1309 1310
1310 1311 .ac-container-wrap {
1311 1312 margin: 0;
1312 1313 padding: 8px;
1313 1314 border-bottom: @border-thickness solid @grey5;
1314 1315 list-style-type: none;
1315 1316 cursor: pointer;
1316 1317
1317 1318 &:hover {
1318 1319 background-color: @grey7;
1319 1320 }
1320 1321
1321 1322 img {
1322 1323 height: @gravatar-size;
1323 1324 width: @gravatar-size;
1324 1325 margin-right: 1em;
1325 1326 }
1326 1327
1327 1328 strong {
1328 1329 font-weight: normal;
1329 1330 }
1330 1331 }
1331 1332
1332 1333 // Settings Dropdown
1333 1334 .user-menu .container {
1334 1335 padding: 0 4px;
1335 1336 margin: 0;
1336 1337 }
1337 1338
1338 1339 .user-menu .gravatar {
1339 1340 cursor: pointer;
1340 1341 }
1341 1342
1342 1343 .codeblock {
1343 1344 margin-bottom: @padding;
1344 1345 clear: both;
1345 1346
1346 1347 .stats {
1347 1348 overflow: hidden;
1348 1349 }
1349 1350
1350 1351 .message{
1351 1352 textarea{
1352 1353 margin: 0;
1353 1354 }
1354 1355 }
1355 1356
1356 1357 .code-header {
1357 1358 .stats {
1358 1359 line-height: 2em;
1359 1360
1360 1361 .revision_id {
1361 1362 margin-left: 0;
1362 1363 }
1363 1364 .buttons {
1364 1365 padding-right: 0;
1365 1366 }
1366 1367 }
1367 1368
1368 1369 .item{
1369 1370 margin-right: 0.5em;
1370 1371 }
1371 1372 }
1372 1373
1373 1374 #editor_container {
1374 1375 position: relative;
1375 1376 margin: @padding 10px;
1376 1377 }
1377 1378 }
1378 1379
1379 1380 #file_history_container {
1380 1381 display: none;
1381 1382 }
1382 1383
1383 1384 .file-history-inner {
1384 1385 margin-bottom: 10px;
1385 1386 }
1386 1387
1387 1388 // Pull Requests
1388 1389 .summary-details {
1389 1390 width: 72%;
1390 1391 }
1391 1392 .pr-summary {
1392 1393 border-bottom: @border-thickness solid @grey5;
1393 1394 margin-bottom: @space;
1394 1395 }
1395 1396 .reviewers-title {
1396 1397 width: 25%;
1397 1398 min-width: 200px;
1398 1399 }
1399 1400 .reviewers {
1400 1401 width: 25%;
1401 1402 min-width: 200px;
1402 1403 }
1403 1404 .reviewers ul li {
1404 1405 position: relative;
1405 1406 width: 100%;
1406 1407 padding-bottom: 8px;
1407 1408 list-style-type: none;
1408 1409 }
1409 1410
1410 1411 .reviewer_entry {
1411 1412 min-height: 55px;
1412 1413 }
1413 1414
1414 1415 .reviewers_member {
1415 1416 width: 100%;
1416 1417 overflow: auto;
1417 1418 }
1418 1419 .reviewer_reason {
1419 1420 padding-left: 20px;
1420 1421 line-height: 1.5em;
1421 1422 }
1422 1423 .reviewer_status {
1423 1424 display: inline-block;
1424 1425 width: 25px;
1425 1426 min-width: 25px;
1426 1427 height: 1.2em;
1427 1428 line-height: 1em;
1428 1429 }
1429 1430
1430 1431 .reviewer_name {
1431 1432 display: inline-block;
1432 1433 max-width: 83%;
1433 1434 padding-right: 20px;
1434 1435 vertical-align: middle;
1435 1436 line-height: 1;
1436 1437
1437 1438 .rc-user {
1438 1439 min-width: 0;
1439 1440 margin: -2px 1em 0 0;
1440 1441 }
1441 1442
1442 1443 .reviewer {
1443 1444 float: left;
1444 1445 }
1445 1446 }
1446 1447
1447 1448 .reviewer_member_mandatory {
1448 1449 position: absolute;
1449 1450 left: 15px;
1450 1451 top: 8px;
1451 1452 width: 16px;
1452 1453 font-size: 11px;
1453 1454 margin: 0;
1454 1455 padding: 0;
1455 1456 color: black;
1456 1457 }
1457 1458
1458 1459 .reviewer_member_mandatory_remove,
1459 1460 .reviewer_member_remove {
1460 1461 position: absolute;
1461 1462 right: 0;
1462 1463 top: 0;
1463 1464 width: 16px;
1464 1465 margin-bottom: 10px;
1465 1466 padding: 0;
1466 1467 color: black;
1467 1468 }
1468 1469
1469 1470 .reviewer_member_mandatory_remove {
1470 1471 color: @grey4;
1471 1472 }
1472 1473
1473 1474 .reviewer_member_status {
1474 1475 margin-top: 5px;
1475 1476 }
1476 1477 .pr-summary #summary{
1477 1478 width: 100%;
1478 1479 }
1479 1480 .pr-summary .action_button:hover {
1480 1481 border: 0;
1481 1482 cursor: pointer;
1482 1483 }
1483 1484 .pr-details-title {
1484 1485 padding-bottom: 8px;
1485 1486 border-bottom: @border-thickness solid @grey5;
1486 1487
1487 1488 .action_button.disabled {
1488 1489 color: @grey4;
1489 1490 cursor: inherit;
1490 1491 }
1491 1492 .action_button {
1492 1493 color: @rcblue;
1493 1494 }
1494 1495 }
1495 1496 .pr-details-content {
1496 1497 margin-top: @textmargin;
1497 1498 margin-bottom: @textmargin;
1498 1499 }
1499 1500
1500 1501 .pr-reviewer-rules {
1501 1502 padding: 10px 0px 20px 0px;
1502 1503 }
1503 1504
1504 1505 .group_members {
1505 1506 margin-top: 0;
1506 1507 padding: 0;
1507 1508 list-style: outside none none;
1508 1509
1509 1510 img {
1510 1511 height: @gravatar-size;
1511 1512 width: @gravatar-size;
1512 1513 margin-right: .5em;
1513 1514 margin-left: 3px;
1514 1515 }
1515 1516
1516 1517 .to-delete {
1517 1518 .user {
1518 1519 text-decoration: line-through;
1519 1520 }
1520 1521 }
1521 1522 }
1522 1523
1523 1524 .compare_view_commits_title {
1524 1525 .disabled {
1525 1526 cursor: inherit;
1526 1527 &:hover{
1527 1528 background-color: inherit;
1528 1529 color: inherit;
1529 1530 }
1530 1531 }
1531 1532 }
1532 1533
1533 1534 .subtitle-compare {
1534 1535 margin: -15px 0px 0px 0px;
1535 1536 }
1536 1537
1537 1538 // new entry in group_members
1538 1539 .td-author-new-entry {
1539 1540 background-color: rgba(red(@alert1), green(@alert1), blue(@alert1), 0.3);
1540 1541 }
1541 1542
1542 1543 .usergroup_member_remove {
1543 1544 width: 16px;
1544 1545 margin-bottom: 10px;
1545 1546 padding: 0;
1546 1547 color: black !important;
1547 1548 cursor: pointer;
1548 1549 }
1549 1550
1550 1551 .reviewer_ac .ac-input {
1551 1552 width: 92%;
1552 1553 margin-bottom: 1em;
1553 1554 }
1554 1555
1555 1556 .compare_view_commits tr{
1556 1557 height: 20px;
1557 1558 }
1558 1559 .compare_view_commits td {
1559 1560 vertical-align: top;
1560 1561 padding-top: 10px;
1561 1562 }
1562 1563 .compare_view_commits .author {
1563 1564 margin-left: 5px;
1564 1565 }
1565 1566
1566 1567 .compare_view_commits {
1567 1568 .color-a {
1568 1569 color: @alert1;
1569 1570 }
1570 1571
1571 1572 .color-c {
1572 1573 color: @color3;
1573 1574 }
1574 1575
1575 1576 .color-r {
1576 1577 color: @color5;
1577 1578 }
1578 1579
1579 1580 .color-a-bg {
1580 1581 background-color: @alert1;
1581 1582 }
1582 1583
1583 1584 .color-c-bg {
1584 1585 background-color: @alert3;
1585 1586 }
1586 1587
1587 1588 .color-r-bg {
1588 1589 background-color: @alert2;
1589 1590 }
1590 1591
1591 1592 .color-a-border {
1592 1593 border: 1px solid @alert1;
1593 1594 }
1594 1595
1595 1596 .color-c-border {
1596 1597 border: 1px solid @alert3;
1597 1598 }
1598 1599
1599 1600 .color-r-border {
1600 1601 border: 1px solid @alert2;
1601 1602 }
1602 1603
1603 1604 .commit-change-indicator {
1604 1605 width: 15px;
1605 1606 height: 15px;
1606 1607 position: relative;
1607 1608 left: 15px;
1608 1609 }
1609 1610
1610 1611 .commit-change-content {
1611 1612 text-align: center;
1612 1613 vertical-align: middle;
1613 1614 line-height: 15px;
1614 1615 }
1615 1616 }
1616 1617
1617 1618 .compare_view_filepath {
1618 1619 color: @grey1;
1619 1620 }
1620 1621
1621 1622 .show_more {
1622 1623 display: inline-block;
1623 1624 width: 0;
1624 1625 height: 0;
1625 1626 vertical-align: middle;
1626 1627 content: "";
1627 1628 border: 4px solid;
1628 1629 border-right-color: transparent;
1629 1630 border-bottom-color: transparent;
1630 1631 border-left-color: transparent;
1631 1632 font-size: 0;
1632 1633 }
1633 1634
1634 1635 .journal_more .show_more {
1635 1636 display: inline;
1636 1637
1637 1638 &:after {
1638 1639 content: none;
1639 1640 }
1640 1641 }
1641 1642
1642 1643 .compare_view_commits .collapse_commit:after {
1643 1644 cursor: pointer;
1644 1645 content: "\00A0\25B4";
1645 1646 margin-left: -3px;
1646 1647 font-size: 17px;
1647 1648 color: @grey4;
1648 1649 }
1649 1650
1650 1651 .diff_links {
1651 1652 margin-left: 8px;
1652 1653 }
1653 1654
1654 1655 #pull_request_overview {
1655 1656 div.ancestor {
1656 1657 margin: -33px 0;
1657 1658 }
1658 1659 }
1659 1660
1660 1661 div.ancestor {
1661 1662 line-height: 33px;
1662 1663 }
1663 1664
1664 1665 .cs_icon_td input[type="checkbox"] {
1665 1666 display: none;
1666 1667 }
1667 1668
1668 1669 .cs_icon_td .expand_file_icon:after {
1669 1670 cursor: pointer;
1670 1671 content: "\00A0\25B6";
1671 1672 font-size: 12px;
1672 1673 color: @grey4;
1673 1674 }
1674 1675
1675 1676 .cs_icon_td .collapse_file_icon:after {
1676 1677 cursor: pointer;
1677 1678 content: "\00A0\25BC";
1678 1679 font-size: 12px;
1679 1680 color: @grey4;
1680 1681 }
1681 1682
1682 1683 /*new binary
1683 1684 NEW_FILENODE = 1
1684 1685 DEL_FILENODE = 2
1685 1686 MOD_FILENODE = 3
1686 1687 RENAMED_FILENODE = 4
1687 1688 COPIED_FILENODE = 5
1688 1689 CHMOD_FILENODE = 6
1689 1690 BIN_FILENODE = 7
1690 1691 */
1691 1692 .cs_files_expand {
1692 1693 font-size: @basefontsize + 5px;
1693 1694 line-height: 1.8em;
1694 1695 float: right;
1695 1696 }
1696 1697
1697 1698 .cs_files_expand span{
1698 1699 color: @rcblue;
1699 1700 cursor: pointer;
1700 1701 }
1701 1702 .cs_files {
1702 1703 clear: both;
1703 1704 padding-bottom: @padding;
1704 1705
1705 1706 .cur_cs {
1706 1707 margin: 10px 2px;
1707 1708 font-weight: bold;
1708 1709 }
1709 1710
1710 1711 .node {
1711 1712 float: left;
1712 1713 }
1713 1714
1714 1715 .changes {
1715 1716 float: right;
1716 1717 color: white;
1717 1718 font-size: @basefontsize - 4px;
1718 1719 margin-top: 4px;
1719 1720 opacity: 0.6;
1720 1721 filter: Alpha(opacity=60); /* IE8 and earlier */
1721 1722
1722 1723 .added {
1723 1724 background-color: @alert1;
1724 1725 float: left;
1725 1726 text-align: center;
1726 1727 }
1727 1728
1728 1729 .deleted {
1729 1730 background-color: @alert2;
1730 1731 float: left;
1731 1732 text-align: center;
1732 1733 }
1733 1734
1734 1735 .bin {
1735 1736 background-color: @alert1;
1736 1737 text-align: center;
1737 1738 }
1738 1739
1739 1740 /*new binary*/
1740 1741 .bin.bin1 {
1741 1742 background-color: @alert1;
1742 1743 text-align: center;
1743 1744 }
1744 1745
1745 1746 /*deleted binary*/
1746 1747 .bin.bin2 {
1747 1748 background-color: @alert2;
1748 1749 text-align: center;
1749 1750 }
1750 1751
1751 1752 /*mod binary*/
1752 1753 .bin.bin3 {
1753 1754 background-color: @grey2;
1754 1755 text-align: center;
1755 1756 }
1756 1757
1757 1758 /*rename file*/
1758 1759 .bin.bin4 {
1759 1760 background-color: @alert4;
1760 1761 text-align: center;
1761 1762 }
1762 1763
1763 1764 /*copied file*/
1764 1765 .bin.bin5 {
1765 1766 background-color: @alert4;
1766 1767 text-align: center;
1767 1768 }
1768 1769
1769 1770 /*chmod file*/
1770 1771 .bin.bin6 {
1771 1772 background-color: @grey2;
1772 1773 text-align: center;
1773 1774 }
1774 1775 }
1775 1776 }
1776 1777
1777 1778 .cs_files .cs_added, .cs_files .cs_A,
1778 1779 .cs_files .cs_added, .cs_files .cs_M,
1779 1780 .cs_files .cs_added, .cs_files .cs_D {
1780 1781 height: 16px;
1781 1782 padding-right: 10px;
1782 1783 margin-top: 7px;
1783 1784 text-align: left;
1784 1785 }
1785 1786
1786 1787 .cs_icon_td {
1787 1788 min-width: 16px;
1788 1789 width: 16px;
1789 1790 }
1790 1791
1791 1792 .pull-request-merge {
1792 1793 border: 1px solid @grey5;
1793 1794 padding: 10px 0px 20px;
1794 1795 margin-top: 10px;
1795 1796 margin-bottom: 20px;
1796 1797 }
1797 1798
1798 1799 .pull-request-merge-refresh {
1799 1800 margin: 2px 7px;
1800 1801 }
1801 1802
1802 1803 .pull-request-merge ul {
1803 1804 padding: 0px 0px;
1804 1805 }
1805 1806
1806 1807 .pull-request-merge li {
1807 1808 list-style-type: none;
1808 1809 }
1809 1810
1810 1811 .pull-request-merge .pull-request-wrap {
1811 1812 height: auto;
1812 1813 padding: 0px 0px;
1813 1814 text-align: right;
1814 1815 }
1815 1816
1816 1817 .pull-request-merge span {
1817 1818 margin-right: 5px;
1818 1819 }
1819 1820
1820 1821 .pull-request-merge-actions {
1821 1822 min-height: 30px;
1822 1823 padding: 0px 0px;
1823 1824 }
1824 1825
1825 1826 .pull-request-merge-info {
1826 1827 padding: 0px 5px 5px 0px;
1827 1828 }
1828 1829
1829 1830 .merge-status {
1830 1831 margin-right: 5px;
1831 1832 }
1832 1833
1833 1834 .merge-message {
1834 1835 font-size: 1.2em
1835 1836 }
1836 1837
1837 1838 .merge-message.success i,
1838 1839 .merge-icon.success i {
1839 1840 color:@alert1;
1840 1841 }
1841 1842
1842 1843 .merge-message.warning i,
1843 1844 .merge-icon.warning i {
1844 1845 color: @alert3;
1845 1846 }
1846 1847
1847 1848 .merge-message.error i,
1848 1849 .merge-icon.error i {
1849 1850 color:@alert2;
1850 1851 }
1851 1852
1852 1853 .pr-versions {
1853 1854 font-size: 1.1em;
1854 1855
1855 1856 table {
1856 1857 padding: 0px 5px;
1857 1858 }
1858 1859
1859 1860 td {
1860 1861 line-height: 15px;
1861 1862 }
1862 1863
1863 1864 .compare-radio-button {
1864 1865 position: relative;
1865 1866 top: -3px;
1866 1867 }
1867 1868 }
1868 1869
1869 1870
1870 1871 #close_pull_request {
1871 1872 margin-right: 0px;
1872 1873 }
1873 1874
1874 1875 .empty_data {
1875 1876 color: @grey4;
1876 1877 }
1877 1878
1878 1879 #changeset_compare_view_content {
1879 1880 clear: both;
1880 1881 width: 100%;
1881 1882 box-sizing: border-box;
1882 1883 .border-radius(@border-radius);
1883 1884
1884 1885 .help-block {
1885 1886 margin: @padding 0;
1886 1887 color: @text-color;
1887 1888 &.pre-formatting {
1888 1889 white-space: pre;
1889 1890 }
1890 1891 }
1891 1892
1892 1893 .empty_data {
1893 1894 margin: @padding 0;
1894 1895 }
1895 1896
1896 1897 .alert {
1897 1898 margin-bottom: @space;
1898 1899 }
1899 1900 }
1900 1901
1901 1902 .table_disp {
1902 1903 .status {
1903 1904 width: auto;
1904 1905 }
1905 1906 }
1906 1907
1907 1908
1908 1909 .creation_in_progress {
1909 1910 color: @grey4
1910 1911 }
1911 1912
1912 1913 .status_box_menu {
1913 1914 margin: 0;
1914 1915 }
1915 1916
1916 1917 .notification-table{
1917 1918 margin-bottom: @space;
1918 1919 display: table;
1919 1920 width: 100%;
1920 1921
1921 1922 .container{
1922 1923 display: table-row;
1923 1924
1924 1925 .notification-header{
1925 1926 border-bottom: @border-thickness solid @border-default-color;
1926 1927 }
1927 1928
1928 1929 .notification-subject{
1929 1930 display: table-cell;
1930 1931 }
1931 1932 }
1932 1933 }
1933 1934
1934 1935 // Notifications
1935 1936 .notification-header{
1936 1937 display: table;
1937 1938 width: 100%;
1938 1939 padding: floor(@basefontsize/2) 0;
1939 1940 line-height: 1em;
1940 1941
1941 1942 .desc, .delete-notifications, .read-notifications{
1942 1943 display: table-cell;
1943 1944 text-align: left;
1944 1945 }
1945 1946
1946 1947 .desc{
1947 1948 width: 1163px;
1948 1949 }
1949 1950
1950 1951 .delete-notifications, .read-notifications{
1951 1952 width: 35px;
1952 1953 min-width: 35px; //fixes when only one button is displayed
1953 1954 }
1954 1955 }
1955 1956
1956 1957 .notification-body {
1957 1958 .markdown-block,
1958 1959 .rst-block {
1959 1960 padding: @padding 0;
1960 1961 }
1961 1962
1962 1963 .notification-subject {
1963 1964 padding: @textmargin 0;
1964 1965 border-bottom: @border-thickness solid @border-default-color;
1965 1966 }
1966 1967 }
1967 1968
1968 1969
1969 1970 .notifications_buttons{
1970 1971 float: right;
1971 1972 }
1972 1973
1973 1974 #notification-status{
1974 1975 display: inline;
1975 1976 }
1976 1977
1977 1978 // Repositories
1978 1979
1979 1980 #summary.fields{
1980 1981 display: table;
1981 1982
1982 1983 .field{
1983 1984 display: table-row;
1984 1985
1985 1986 .label-summary{
1986 1987 display: table-cell;
1987 1988 min-width: @label-summary-minwidth;
1988 1989 padding-top: @padding/2;
1989 1990 padding-bottom: @padding/2;
1990 1991 padding-right: @padding/2;
1991 1992 }
1992 1993
1993 1994 .input{
1994 1995 display: table-cell;
1995 1996 padding: @padding/2;
1996 1997
1997 1998 input{
1998 1999 min-width: 29em;
1999 2000 padding: @padding/4;
2000 2001 }
2001 2002 }
2002 2003 .statistics, .downloads{
2003 2004 .disabled{
2004 2005 color: @grey4;
2005 2006 }
2006 2007 }
2007 2008 }
2008 2009 }
2009 2010
2010 2011 #summary{
2011 2012 width: 70%;
2012 2013 }
2013 2014
2014 2015
2015 2016 // Journal
2016 2017 .journal.title {
2017 2018 h5 {
2018 2019 float: left;
2019 2020 margin: 0;
2020 2021 width: 70%;
2021 2022 }
2022 2023
2023 2024 ul {
2024 2025 float: right;
2025 2026 display: inline-block;
2026 2027 margin: 0;
2027 2028 width: 30%;
2028 2029 text-align: right;
2029 2030
2030 2031 li {
2031 2032 display: inline;
2032 2033 font-size: @journal-fontsize;
2033 2034 line-height: 1em;
2034 2035
2035 2036 list-style-type: none;
2036 2037 }
2037 2038 }
2038 2039 }
2039 2040
2040 2041 .filterexample {
2041 2042 position: absolute;
2042 2043 top: 95px;
2043 2044 left: @contentpadding;
2044 2045 color: @rcblue;
2045 2046 font-size: 11px;
2046 2047 font-family: @text-regular;
2047 2048 cursor: help;
2048 2049
2049 2050 &:hover {
2050 2051 color: @rcdarkblue;
2051 2052 }
2052 2053
2053 2054 @media (max-width:768px) {
2054 2055 position: relative;
2055 2056 top: auto;
2056 2057 left: auto;
2057 2058 display: block;
2058 2059 }
2059 2060 }
2060 2061
2061 2062
2062 2063 #journal{
2063 2064 margin-bottom: @space;
2064 2065
2065 2066 .journal_day{
2066 2067 margin-bottom: @textmargin/2;
2067 2068 padding-bottom: @textmargin/2;
2068 2069 font-size: @journal-fontsize;
2069 2070 border-bottom: @border-thickness solid @border-default-color;
2070 2071 }
2071 2072
2072 2073 .journal_container{
2073 2074 margin-bottom: @space;
2074 2075
2075 2076 .journal_user{
2076 2077 display: inline-block;
2077 2078 }
2078 2079 .journal_action_container{
2079 2080 display: block;
2080 2081 margin-top: @textmargin;
2081 2082
2082 2083 div{
2083 2084 display: inline;
2084 2085 }
2085 2086
2086 2087 div.journal_action_params{
2087 2088 display: block;
2088 2089 }
2089 2090
2090 2091 div.journal_repo:after{
2091 2092 content: "\A";
2092 2093 white-space: pre;
2093 2094 }
2094 2095
2095 2096 div.date{
2096 2097 display: block;
2097 2098 margin-bottom: @textmargin;
2098 2099 }
2099 2100 }
2100 2101 }
2101 2102 }
2102 2103
2103 2104 // Files
2104 2105 .edit-file-title {
2105 2106 font-size: 16px;
2106 2107
2107 2108 .title-heading {
2108 2109 padding: 2px;
2109 2110 }
2110 2111 }
2111 2112
2112 2113 .edit-file-fieldset {
2113 2114 margin: @sidebarpadding 0;
2114 2115
2115 2116 .fieldset {
2116 2117 .left-label {
2117 2118 width: 13%;
2118 2119 }
2119 2120 .right-content {
2120 2121 width: 87%;
2121 2122 max-width: 100%;
2122 2123 }
2123 2124 .filename-label {
2124 2125 margin-top: 13px;
2125 2126 }
2126 2127 .commit-message-label {
2127 2128 margin-top: 4px;
2128 2129 }
2129 2130 .file-upload-input {
2130 2131 input {
2131 2132 display: none;
2132 2133 }
2133 2134 margin-top: 10px;
2134 2135 }
2135 2136 .file-upload-label {
2136 2137 margin-top: 10px;
2137 2138 }
2138 2139 p {
2139 2140 margin-top: 5px;
2140 2141 }
2141 2142
2142 2143 }
2143 2144 .custom-path-link {
2144 2145 margin-left: 5px;
2145 2146 }
2146 2147 #commit {
2147 2148 resize: vertical;
2148 2149 }
2149 2150 }
2150 2151
2151 2152 .delete-file-preview {
2152 2153 max-height: 250px;
2153 2154 }
2154 2155
2155 2156 .new-file,
2156 2157 #filter_activate,
2157 2158 #filter_deactivate {
2158 2159 float: right;
2159 2160 margin: 0 0 0 10px;
2160 2161 }
2161 2162
2162 2163 .file-upload-transaction-wrapper {
2163 2164 margin-top: 57px;
2164 2165 clear: both;
2165 2166 }
2166 2167
2167 2168 .file-upload-transaction-wrapper .error {
2168 2169 color: @color5;
2169 2170 }
2170 2171
2171 2172 .file-upload-transaction {
2172 2173 min-height: 200px;
2173 2174 padding: 54px;
2174 2175 border: 1px solid @grey5;
2175 2176 text-align: center;
2176 2177 clear: both;
2177 2178 }
2178 2179
2179 2180 .file-upload-transaction i {
2180 2181 font-size: 48px
2181 2182 }
2182 2183
2183 2184 h3.files_location{
2184 2185 line-height: 2.4em;
2185 2186 }
2186 2187
2187 2188 .browser-nav {
2188 2189 width: 100%;
2189 2190 display: table;
2190 2191 margin-bottom: 20px;
2191 2192
2192 2193 .info_box {
2193 2194 float: left;
2194 2195 display: inline-table;
2195 2196 height: 2.5em;
2196 2197
2197 2198 .browser-cur-rev, .info_box_elem {
2198 2199 display: table-cell;
2199 2200 vertical-align: middle;
2200 2201 }
2201 2202
2202 2203 .drop-menu {
2203 2204 margin: 0 10px;
2204 2205 }
2205 2206
2206 2207 .info_box_elem {
2207 2208 border-top: @border-thickness solid @grey5;
2208 2209 border-bottom: @border-thickness solid @grey5;
2209 2210 box-shadow: @button-shadow;
2210 2211
2211 2212 #at_rev, a {
2212 2213 padding: 0.6em 0.4em;
2213 2214 margin: 0;
2214 2215 .box-shadow(none);
2215 2216 border: 0;
2216 2217 height: 12px;
2217 2218 color: @grey2;
2218 2219 }
2219 2220
2220 2221 input#at_rev {
2221 2222 max-width: 50px;
2222 2223 text-align: center;
2223 2224 }
2224 2225
2225 2226 &.previous {
2226 2227 border: @border-thickness solid @grey5;
2227 2228 border-top-left-radius: @border-radius;
2228 2229 border-bottom-left-radius: @border-radius;
2229 2230
2230 2231 &:hover {
2231 2232 border-color: @grey4;
2232 2233 }
2233 2234
2234 2235 .disabled {
2235 2236 color: @grey5;
2236 2237 cursor: not-allowed;
2237 2238 opacity: 0.5;
2238 2239 }
2239 2240 }
2240 2241
2241 2242 &.next {
2242 2243 border: @border-thickness solid @grey5;
2243 2244 border-top-right-radius: @border-radius;
2244 2245 border-bottom-right-radius: @border-radius;
2245 2246
2246 2247 &:hover {
2247 2248 border-color: @grey4;
2248 2249 }
2249 2250
2250 2251 .disabled {
2251 2252 color: @grey5;
2252 2253 cursor: not-allowed;
2253 2254 opacity: 0.5;
2254 2255 }
2255 2256 }
2256 2257 }
2257 2258
2258 2259 .browser-cur-rev {
2259 2260
2260 2261 span{
2261 2262 margin: 0;
2262 2263 color: @rcblue;
2263 2264 height: 12px;
2264 2265 display: inline-block;
2265 2266 padding: 0.7em 1em ;
2266 2267 border: @border-thickness solid @rcblue;
2267 2268 margin-right: @padding;
2268 2269 }
2269 2270 }
2270 2271
2271 2272 }
2272 2273
2273 2274 .select-index-number {
2274 2275 margin: 0 0 0 20px;
2275 2276 color: @grey3;
2276 2277 }
2277 2278
2278 2279 .search_activate {
2279 2280 display: table-cell;
2280 2281 vertical-align: middle;
2281 2282
2282 2283 input, label{
2283 2284 margin: 0;
2284 2285 padding: 0;
2285 2286 }
2286 2287
2287 2288 input{
2288 2289 margin-left: @textmargin;
2289 2290 }
2290 2291
2291 2292 }
2292 2293 }
2293 2294
2294 2295 .browser-cur-rev{
2295 2296 margin-bottom: @textmargin;
2296 2297 }
2297 2298
2298 2299 #node_filter_box_loading{
2299 2300 .info_text;
2300 2301 }
2301 2302
2302 2303 .browser-search {
2303 2304 margin: -25px 0px 5px 0px;
2304 2305 }
2305 2306
2306 2307 .files-quick-filter {
2307 2308 float: right;
2308 2309 width: 180px;
2309 2310 position: relative;
2310 2311 }
2311 2312
2312 2313 .files-filter-box {
2313 2314 display: flex;
2314 2315 padding: 0px;
2315 2316 border-radius: 3px;
2316 2317 margin-bottom: 0;
2317 2318
2318 2319 a {
2319 2320 border: none !important;
2320 2321 }
2321 2322
2322 2323 li {
2323 2324 list-style-type: none
2324 2325 }
2325 2326 }
2326 2327
2327 2328 .files-filter-box-path {
2328 2329 line-height: 33px;
2329 2330 padding: 0;
2330 2331 width: 20px;
2331 2332 position: absolute;
2332 2333 z-index: 11;
2333 2334 left: 5px;
2334 2335 }
2335 2336
2336 2337 .files-filter-box-input {
2337 2338 margin-right: 0;
2338 2339
2339 2340 input {
2340 2341 border: 1px solid @white;
2341 2342 padding-left: 25px;
2342 2343 width: 145px;
2343 2344
2344 2345 &:hover {
2345 2346 border-color: @grey6;
2346 2347 }
2347 2348
2348 2349 &:focus {
2349 2350 border-color: @grey5;
2350 2351 }
2351 2352 }
2352 2353 }
2353 2354
2354 2355 .browser-result{
2355 2356 td a{
2356 2357 margin-left: 0.5em;
2357 2358 display: inline-block;
2358 2359
2359 2360 em {
2360 2361 font-weight: @text-bold-weight;
2361 2362 font-family: @text-bold;
2362 2363 }
2363 2364 }
2364 2365 }
2365 2366
2366 2367 .browser-highlight{
2367 2368 background-color: @grey5-alpha;
2368 2369 }
2369 2370
2370 2371
2371 2372 .edit-file-fieldset #location,
2372 2373 .edit-file-fieldset #filename {
2373 2374 display: flex;
2374 2375 width: -moz-available; /* WebKit-based browsers will ignore this. */
2375 2376 width: -webkit-fill-available; /* Mozilla-based browsers will ignore this. */
2376 2377 width: fill-available;
2377 2378 border: 0;
2378 2379 }
2379 2380
2380 2381 .path-items {
2381 2382 display: flex;
2382 2383 padding: 0;
2383 2384 border: 1px solid #eeeeee;
2384 2385 width: 100%;
2385 2386 float: left;
2386 2387
2387 2388 .breadcrumb-path {
2388 2389 line-height: 30px;
2389 2390 padding: 0 4px;
2390 2391 white-space: nowrap;
2391 2392 }
2392 2393
2393 2394 .location-path {
2394 2395 width: -moz-available; /* WebKit-based browsers will ignore this. */
2395 2396 width: -webkit-fill-available; /* Mozilla-based browsers will ignore this. */
2396 2397 width: fill-available;
2397 2398
2398 2399 .file-name-input {
2399 2400 padding: 0.5em 0;
2400 2401 }
2401 2402
2402 2403 }
2403 2404
2404 2405 ul {
2405 2406 display: flex;
2406 2407 margin: 0;
2407 2408 padding: 0;
2408 2409 width: 100%;
2409 2410 }
2410 2411
2411 2412 li {
2412 2413 list-style-type: none;
2413 2414 }
2414 2415
2415 2416 }
2416 2417
2417 2418 .editor-items {
2418 2419 height: 40px;
2419 2420 margin: 10px 0 -17px 10px;
2420 2421
2421 2422 .editor-action {
2422 2423 cursor: pointer;
2423 2424 }
2424 2425
2425 2426 .editor-action.active {
2426 2427 border-bottom: 2px solid #5C5C5C;
2427 2428 }
2428 2429
2429 2430 li {
2430 2431 list-style-type: none;
2431 2432 }
2432 2433 }
2433 2434
2434 2435 .edit-file-fieldset .message textarea {
2435 2436 border: 1px solid #eeeeee;
2436 2437 }
2437 2438
2438 2439 #files_data .codeblock {
2439 2440 background-color: #F5F5F5;
2440 2441 }
2441 2442
2442 2443 #editor_preview {
2443 2444 background: white;
2444 2445 }
2445 2446
2446 2447 .show-editor {
2447 2448 padding: 10px;
2448 2449 background-color: white;
2449 2450
2450 2451 }
2451 2452
2452 2453 .show-preview {
2453 2454 padding: 10px;
2454 2455 background-color: white;
2455 2456 border-left: 1px solid #eeeeee;
2456 2457 }
2457 2458 // quick filter
2458 2459 .grid-quick-filter {
2459 2460 float: right;
2460 2461 position: relative;
2461 2462 }
2462 2463
2463 2464 .grid-filter-box {
2464 2465 display: flex;
2465 2466 padding: 0px;
2466 2467 border-radius: 3px;
2467 2468 margin-bottom: 0;
2468 2469
2469 2470 a {
2470 2471 border: none !important;
2471 2472 }
2472 2473
2473 2474 li {
2474 2475 list-style-type: none
2475 2476 }
2476 2477 }
2477 2478
2478 2479 .grid-filter-box-icon {
2479 2480 line-height: 33px;
2480 2481 padding: 0;
2481 2482 width: 20px;
2482 2483 position: absolute;
2483 2484 z-index: 11;
2484 2485 left: 5px;
2485 2486 }
2486 2487
2487 2488 .grid-filter-box-input {
2488 2489 margin-right: 0;
2489 2490
2490 2491 input {
2491 2492 border: 1px solid @white;
2492 2493 padding-left: 25px;
2493 2494 width: 145px;
2494 2495
2495 2496 &:hover {
2496 2497 border-color: @grey6;
2497 2498 }
2498 2499
2499 2500 &:focus {
2500 2501 border-color: @grey5;
2501 2502 }
2502 2503 }
2503 2504 }
2504 2505
2505 2506
2506 2507
2507 2508 // Search
2508 2509
2509 2510 .search-form{
2510 2511 #q {
2511 2512 width: @search-form-width;
2512 2513 }
2513 2514 .fields{
2514 2515 margin: 0 0 @space;
2515 2516 }
2516 2517
2517 2518 label{
2518 2519 display: inline-block;
2519 2520 margin-right: @textmargin;
2520 2521 padding-top: 0.25em;
2521 2522 }
2522 2523
2523 2524
2524 2525 .results{
2525 2526 clear: both;
2526 2527 margin: 0 0 @padding;
2527 2528 }
2528 2529
2529 2530 .search-tags {
2530 2531 padding: 5px 0;
2531 2532 }
2532 2533 }
2533 2534
2534 2535 div.search-feedback-items {
2535 2536 display: inline-block;
2536 2537 }
2537 2538
2538 2539 div.search-code-body {
2539 2540 background-color: #ffffff; padding: 5px 0 5px 10px;
2540 2541 pre {
2541 2542 .match { background-color: #faffa6;}
2542 2543 .break { display: block; width: 100%; background-color: #DDE7EF; color: #747474; }
2543 2544 }
2544 2545 }
2545 2546
2546 2547 .expand_commit.search {
2547 2548 .show_more.open {
2548 2549 height: auto;
2549 2550 max-height: none;
2550 2551 }
2551 2552 }
2552 2553
2553 2554 .search-results {
2554 2555
2555 2556 h2 {
2556 2557 margin-bottom: 0;
2557 2558 }
2558 2559 .codeblock {
2559 2560 border: none;
2560 2561 background: transparent;
2561 2562 }
2562 2563
2563 2564 .codeblock-header {
2564 2565 border: none;
2565 2566 background: transparent;
2566 2567 }
2567 2568
2568 2569 .code-body {
2569 2570 border: @border-thickness solid @grey6;
2570 2571 .border-radius(@border-radius);
2571 2572 }
2572 2573
2573 2574 .td-commit {
2574 2575 &:extend(pre);
2575 2576 border-bottom: @border-thickness solid @border-default-color;
2576 2577 }
2577 2578
2578 2579 .message {
2579 2580 height: auto;
2580 2581 max-width: 350px;
2581 2582 white-space: normal;
2582 2583 text-overflow: initial;
2583 2584 overflow: visible;
2584 2585
2585 2586 .match { background-color: #faffa6;}
2586 2587 .break { background-color: #DDE7EF; width: 100%; color: #747474; display: block; }
2587 2588 }
2588 2589
2589 2590 .path {
2590 2591 border-bottom: none !important;
2591 2592 border-left: 1px solid @grey6 !important;
2592 2593 border-right: 1px solid @grey6 !important;
2593 2594 }
2594 2595 }
2595 2596
2596 2597 table.rctable td.td-search-results div {
2597 2598 max-width: 100%;
2598 2599 }
2599 2600
2600 2601 #tip-box, .tip-box{
2601 2602 padding: @menupadding/2;
2602 2603 display: block;
2603 2604 border: @border-thickness solid @border-highlight-color;
2604 2605 .border-radius(@border-radius);
2605 2606 background-color: white;
2606 2607 z-index: 99;
2607 2608 white-space: pre-wrap;
2608 2609 }
2609 2610
2610 2611 #linktt {
2611 2612 width: 79px;
2612 2613 }
2613 2614
2614 2615 #help_kb .modal-content{
2615 2616 max-width: 750px;
2616 2617 margin: 10% auto;
2617 2618
2618 2619 table{
2619 2620 td,th{
2620 2621 border-bottom: none;
2621 2622 line-height: 2.5em;
2622 2623 }
2623 2624 th{
2624 2625 padding-bottom: @textmargin/2;
2625 2626 }
2626 2627 td.keys{
2627 2628 text-align: center;
2628 2629 }
2629 2630 }
2630 2631
2631 2632 .block-left{
2632 2633 width: 45%;
2633 2634 margin-right: 5%;
2634 2635 }
2635 2636 .modal-footer{
2636 2637 clear: both;
2637 2638 }
2638 2639 .key.tag{
2639 2640 padding: 0.5em;
2640 2641 background-color: @rcblue;
2641 2642 color: white;
2642 2643 border-color: @rcblue;
2643 2644 .box-shadow(none);
2644 2645 }
2645 2646 }
2646 2647
2647 2648
2648 2649
2649 2650 //--- IMPORTS FOR REFACTORED STYLES ------------------//
2650 2651
2651 2652 @import 'statistics-graph';
2652 2653 @import 'tables';
2653 2654 @import 'forms';
2654 2655 @import 'diff';
2655 2656 @import 'summary';
2656 2657 @import 'navigation';
2657 2658
2658 2659 //--- SHOW/HIDE SECTIONS --//
2659 2660
2660 2661 .btn-collapse {
2661 2662 float: right;
2662 2663 text-align: right;
2663 2664 font-family: @text-light;
2664 2665 font-size: @basefontsize;
2665 2666 cursor: pointer;
2666 2667 border: none;
2667 2668 color: @rcblue;
2668 2669 }
2669 2670
2670 2671 table.rctable,
2671 2672 table.dataTable {
2672 2673 .btn-collapse {
2673 2674 float: right;
2674 2675 text-align: right;
2675 2676 }
2676 2677 }
2677 2678
2678 2679 table.rctable {
2679 2680 &.permissions {
2680 2681
2681 2682 th.td-owner {
2682 2683 padding: 0;
2683 2684 }
2684 2685
2685 2686 th {
2686 2687 font-weight: normal;
2687 2688 padding: 0 5px;
2688 2689 }
2689 2690
2690 2691 }
2691 2692 }
2692 2693
2693 2694
2694 2695 // TODO: johbo: Fix for IE10, this avoids that we see a border
2695 2696 // and padding around checkboxes and radio boxes. Move to the right place,
2696 2697 // or better: Remove this once we did the form refactoring.
2697 2698 input[type=checkbox],
2698 2699 input[type=radio] {
2699 2700 padding: 0;
2700 2701 border: none;
2701 2702 }
2702 2703
2703 2704 .toggle-ajax-spinner{
2704 2705 height: 16px;
2705 2706 width: 16px;
2706 2707 }
2707 2708
2708 2709
2709 2710 .markup-form .clearfix {
2710 2711 .border-radius(@border-radius);
2711 2712 margin: 0px;
2712 2713 }
2713 2714
2714 2715 .markup-form-area {
2715 2716 padding: 8px 12px;
2716 2717 border: 1px solid @grey4;
2717 2718 .border-radius(@border-radius);
2718 2719 }
2719 2720
2720 2721 .markup-form-area-header .nav-links {
2721 2722 display: flex;
2722 2723 flex-flow: row wrap;
2723 2724 -webkit-flex-flow: row wrap;
2724 2725 width: 100%;
2725 2726 }
2726 2727
2727 2728 .markup-form-area-footer {
2728 2729 display: flex;
2729 2730 }
2730 2731
2731 2732 .markup-form-area-footer .toolbar {
2732 2733
2733 2734 }
2734 2735
2735 2736 // markup Form
2736 2737 div.markup-form {
2737 2738 margin-top: 20px;
2738 2739 }
2739 2740
2740 2741 .markup-form strong {
2741 2742 display: block;
2742 2743 margin-bottom: 15px;
2743 2744 }
2744 2745
2745 2746 .markup-form textarea {
2746 2747 width: 100%;
2747 2748 height: 100px;
2748 2749 font-family: @text-monospace;
2749 2750 }
2750 2751
2751 2752 form.markup-form {
2752 2753 margin-top: 10px;
2753 2754 margin-left: 10px;
2754 2755 }
2755 2756
2756 2757 .markup-form .comment-block-ta,
2757 2758 .markup-form .preview-box {
2758 2759 .border-radius(@border-radius);
2759 2760 .box-sizing(border-box);
2760 2761 background-color: white;
2761 2762 }
2762 2763
2763 2764 .markup-form .preview-box.unloaded {
2764 2765 height: 50px;
2765 2766 text-align: center;
2766 2767 padding: 20px;
2767 2768 background-color: white;
2768 2769 }
2769 2770
2770 2771
2771 2772 .dropzone-wrapper {
2772 2773 border: 1px solid @grey5;
2773 2774 padding: 20px;
2774 2775 }
2775 2776
2776 2777 .dropzone,
2777 2778 .dropzone-pure {
2778 border: 2px dashed @grey5;
2779 border-radius: 5px;
2780 background: white;
2781 min-height: 200px;
2782 padding: 54px;
2779 border: 2px dashed @grey5;
2780 border-radius: 5px;
2781 background: white;
2782 min-height: 200px;
2783 padding: 54px;
2783 2784
2784 2785 .dz-message {
2785 font-weight: 700;
2786 text-align: center;
2787 margin: 2em 0;
2786 font-weight: 700;
2787 text-align: center;
2788 margin: 2em 0;
2788 2789 }
2789 2790
2790 2791 }
2791 2792
2792 2793 .dz-preview {
2793 margin: 10px 0 !important;
2794 position: relative;
2795 vertical-align: top;
2796 padding: 10px;
2797 border-bottom: 1px solid @grey5;
2794 margin: 10px 0 !important;
2795 position: relative;
2796 vertical-align: top;
2797 padding: 10px;
2798 border-bottom: 1px solid @grey5;
2798 2799 }
2799 2800
2800 2801 .dz-filename {
2801 font-weight: 700;
2802 float:left;
2802 font-weight: 700;
2803 float: left;
2803 2804 }
2804 2805
2805 2806 .dz-sending {
2806 2807 float: right;
2807 2808 }
2808 2809
2809 2810 .dz-response {
2810 clear:both
2811 clear: both
2811 2812 }
2812 2813
2813 2814 .dz-filename-size {
2814 float:right
2815 float: right
2815 2816 }
2816 2817
2817 2818 .dz-error-message {
2818 color: @alert2;
2819 padding-top: 10px;
2820 clear: both;
2819 color: @alert2;
2820 padding-top: 10px;
2821 clear: both;
2822 }
2823
2824
2825 .user-hovercard {
2826 padding: 5px;
2827 }
2828
2829 .user-hovercard-icon {
2830 display: inline;
2831 padding: 0;
2832 box-sizing: content-box;
2833 border-radius: 50%;
2834 float: left;
2835 }
2836
2837 .user-hovercard-name {
2838 float: right;
2839 vertical-align: top;
2840 padding-left: 10px;
2841 min-width: 150px;
2842 }
2843
2844 .user-hovercard-bio {
2845 clear: both;
2846 padding-top: 10px;
2847 }
2848
2849 .user-hovercard-header {
2850 clear: both;
2851 min-height: 10px;
2852 }
2853
2854 .user-hovercard-footer {
2855 clear: both;
2856 min-height: 10px;
2821 2857 }
2858
2859 .user-group-hovercard {
2860 padding: 5px;
2861 }
2862
2863 .user-group-hovercard-icon {
2864 display: inline;
2865 padding: 0;
2866 box-sizing: content-box;
2867 border-radius: 50%;
2868 float: left;
2869 }
2870
2871 .user-group-hovercard-name {
2872 float: left;
2873 vertical-align: top;
2874 padding-left: 10px;
2875 min-width: 150px;
2876 }
2877
2878 .user-group-hovercard-icon i {
2879 border: 1px solid @grey4;
2880 border-radius: 4px;
2881 }
2882
2883 .user-group-hovercard-bio {
2884 clear: both;
2885 padding-top: 10px;
2886 line-height: 1.0em;
2887 }
2888
2889 .user-group-hovercard-header {
2890 clear: both;
2891 min-height: 10px;
2892 }
2893
2894 .user-group-hovercard-footer {
2895 clear: both;
2896 min-height: 10px;
2897 }
@@ -1,289 +1,290 b''
1 1 @font-face {
2 2 font-family: 'rcicons';
3 3
4 4 src: url('../fonts/RCIcons/rcicons.eot?44705679');
5 5 src: url('../fonts/RCIcons/rcicons.eot?44705679#iefix') format('embedded-opentype'),
6 6 url('../fonts/RCIcons/rcicons.woff2?44705679') format('woff2'),
7 7 url('../fonts/RCIcons/rcicons.woff?44705679') format('woff'),
8 8 url('../fonts/RCIcons/rcicons.ttf?44705679') format('truetype'),
9 9 url('../fonts/RCIcons/rcicons.svg?44705679#rcicons') format('svg');
10 10
11 11 font-weight: normal;
12 12 font-style: normal;
13 13 }
14 14 /* Chrome hack: SVG is rendered more smooth in Windozze. 100% magic, uncomment if you need it. */
15 15 /* Note, that will break hinting! In other OS-es font will be not as sharp as it could be */
16 16 /*
17 17 @media screen and (-webkit-min-device-pixel-ratio:0) {
18 18 @font-face {
19 19 font-family: 'rcicons';
20 20 src: url('../fonts/RCIcons/rcicons.svg?74666722#rcicons') format('svg');
21 21 }
22 22 }
23 23 */
24 24
25 25 [class^="icon-"]:before, [class*=" icon-"]:before {
26 26 font-family: "rcicons";
27 27 font-style: normal;
28 28 font-weight: normal;
29 29 speak: none;
30 30
31 31 display: inline-block;
32 32 text-decoration: inherit;
33 33 width: 1em;
34 34 margin-right: .2em;
35 35 text-align: center;
36 36 /* opacity: .8; */
37 37
38 38 /* For safety - reset parent styles, that can break glyph codes*/
39 39 font-variant: normal;
40 40 text-transform: none;
41 41
42 42 /* fix buttons height, for twitter bootstrap */
43 43 line-height: 1em;
44 44
45 45 /* Animation center compensation - margins should be symmetric */
46 46 /* remove if not needed */
47 47 margin-left: .2em;
48 48
49 49 /* you can be more comfortable with increased icons size */
50 50 /* font-size: 120%; */
51 51
52 52 /* Font smoothing. That was taken from TWBS */
53 53 -webkit-font-smoothing: antialiased;
54 54 -moz-osx-font-smoothing: grayscale;
55 55
56 56 /* Uncomment for 3D effect */
57 57 /* text-shadow: 1px 1px 1px rgba(127, 127, 127, 0.3); */
58 58 }
59 59
60 60 .animate-spin {
61 61 -moz-animation: spin 2s infinite linear;
62 62 -o-animation: spin 2s infinite linear;
63 63 -webkit-animation: spin 2s infinite linear;
64 64 animation: spin 2s infinite linear;
65 65 display: inline-block;
66 66 }
67 67 @-moz-keyframes spin {
68 68 0% {
69 69 -moz-transform: rotate(0deg);
70 70 -o-transform: rotate(0deg);
71 71 -webkit-transform: rotate(0deg);
72 72 transform: rotate(0deg);
73 73 }
74 74
75 75 100% {
76 76 -moz-transform: rotate(359deg);
77 77 -o-transform: rotate(359deg);
78 78 -webkit-transform: rotate(359deg);
79 79 transform: rotate(359deg);
80 80 }
81 81 }
82 82 @-webkit-keyframes spin {
83 83 0% {
84 84 -moz-transform: rotate(0deg);
85 85 -o-transform: rotate(0deg);
86 86 -webkit-transform: rotate(0deg);
87 87 transform: rotate(0deg);
88 88 }
89 89
90 90 100% {
91 91 -moz-transform: rotate(359deg);
92 92 -o-transform: rotate(359deg);
93 93 -webkit-transform: rotate(359deg);
94 94 transform: rotate(359deg);
95 95 }
96 96 }
97 97 @-o-keyframes spin {
98 98 0% {
99 99 -moz-transform: rotate(0deg);
100 100 -o-transform: rotate(0deg);
101 101 -webkit-transform: rotate(0deg);
102 102 transform: rotate(0deg);
103 103 }
104 104
105 105 100% {
106 106 -moz-transform: rotate(359deg);
107 107 -o-transform: rotate(359deg);
108 108 -webkit-transform: rotate(359deg);
109 109 transform: rotate(359deg);
110 110 }
111 111 }
112 112 @-ms-keyframes spin {
113 113 0% {
114 114 -moz-transform: rotate(0deg);
115 115 -o-transform: rotate(0deg);
116 116 -webkit-transform: rotate(0deg);
117 117 transform: rotate(0deg);
118 118 }
119 119
120 120 100% {
121 121 -moz-transform: rotate(359deg);
122 122 -o-transform: rotate(359deg);
123 123 -webkit-transform: rotate(359deg);
124 124 transform: rotate(359deg);
125 125 }
126 126 }
127 127 @keyframes spin {
128 128 0% {
129 129 -moz-transform: rotate(0deg);
130 130 -o-transform: rotate(0deg);
131 131 -webkit-transform: rotate(0deg);
132 132 transform: rotate(0deg);
133 133 }
134 134
135 135 100% {
136 136 -moz-transform: rotate(359deg);
137 137 -o-transform: rotate(359deg);
138 138 -webkit-transform: rotate(359deg);
139 139 transform: rotate(359deg);
140 140 }
141 141 }
142 142
143 143
144 144
145 145 .icon-no-margin::before {
146 146 margin: 0;
147 147
148 148 }
149 149 // -- ICON CLASSES -- //
150 150 // sorter = lambda s: '\n'.join(sorted(s.splitlines()))
151 151
152 152 .icon-delete:before { content: '\e800'; } /* '' */
153 153 .icon-ok:before { content: '\e801'; } /* '' */
154 154 .icon-comment:before { content: '\e802'; } /* '' */
155 155 .icon-bookmark:before { content: '\e803'; } /* '' */
156 156 .icon-branch:before { content: '\e804'; } /* '' */
157 157 .icon-tag:before { content: '\e805'; } /* '' */
158 158 .icon-lock:before { content: '\e806'; } /* '' */
159 159 .icon-unlock:before { content: '\e807'; } /* '' */
160 160 .icon-feed:before { content: '\e808'; } /* '' */
161 161 .icon-left:before { content: '\e809'; } /* '' */
162 162 .icon-right:before { content: '\e80a'; } /* '' */
163 163 .icon-down:before { content: '\e80b'; } /* '' */
164 164 .icon-folder:before { content: '\e80c'; } /* '' */
165 165 .icon-folder-open:before { content: '\e80d'; } /* '' */
166 166 .icon-trash-empty:before { content: '\e80e'; } /* '' */
167 167 .icon-group:before { content: '\e80f'; } /* '' */
168 168 .icon-remove:before { content: '\e810'; } /* '' */
169 169 .icon-fork:before { content: '\e811'; } /* '' */
170 170 .icon-more:before { content: '\e812'; } /* '' */
171 171 .icon-search:before { content: '\e813'; } /* '' */
172 172 .icon-scissors:before { content: '\e814'; } /* '' */
173 173 .icon-download:before { content: '\e815'; } /* '' */
174 174 .icon-doc:before { content: '\e816'; } /* '' */
175 175 .icon-cog:before { content: '\e817'; } /* '' */
176 176 .icon-cog-alt:before { content: '\e818'; } /* '' */
177 177 .icon-eye:before { content: '\e819'; } /* '' */
178 178 .icon-eye-off:before { content: '\e81a'; } /* '' */
179 179 .icon-cancel-circled2:before { content: '\e81b'; } /* '' */
180 180 .icon-cancel-circled:before { content: '\e81c'; } /* '' */
181 181 .icon-plus:before { content: '\e81d'; } /* '' */
182 182 .icon-plus-circled:before { content: '\e81e'; } /* '' */
183 183 .icon-minus-circled:before { content: '\e81f'; } /* '' */
184 184 .icon-minus:before { content: '\e820'; } /* '' */
185 185 .icon-info-circled:before { content: '\e821'; } /* '' */
186 186 .icon-upload:before { content: '\e822'; } /* '' */
187 187 .icon-home:before { content: '\e823'; } /* '' */
188 188 .icon-flag-filled:before { content: '\e824'; } /* '' */
189 189 .icon-git:before { content: '\e82a'; } /* '' */
190 190 .icon-hg:before { content: '\e82d'; } /* '' */
191 191 .icon-svn:before { content: '\e82e'; } /* '' */
192 192 .icon-comment-add:before { content: '\e82f'; } /* '' */
193 193 .icon-comment-toggle:before { content: '\e830'; } /* '' */
194 194 .icon-rhodecode:before { content: '\e831'; } /* '' */
195 195 .icon-up:before { content: '\e832'; } /* '' */
196 196 .icon-merge:before { content: '\e833'; } /* '' */
197 197 .icon-spin-alt:before { content: '\e834'; } /* '' */
198 198 .icon-spin:before { content: '\e838'; } /* '' */
199 199 .icon-docs:before { content: '\f0c5'; } /* '' */
200 200 .icon-menu:before { content: '\f0c9'; } /* '' */
201 201 .icon-sort:before { content: '\f0dc'; } /* '' */
202 202 .icon-paste:before { content: '\f0ea'; } /* '' */
203 203 .icon-doc-text:before { content: '\f0f6'; } /* '' */
204 204 .icon-plus-squared:before { content: '\f0fe'; } /* '' */
205 205 .icon-angle-left:before { content: '\f104'; } /* '' */
206 206 .icon-angle-right:before { content: '\f105'; } /* '' */
207 207 .icon-angle-up:before { content: '\f106'; } /* '' */
208 208 .icon-angle-down:before { content: '\f107'; } /* '' */
209 209 .icon-circle-empty:before { content: '\f10c'; } /* '' */
210 210 .icon-circle:before { content: '\f111'; } /* '' */
211 211 .icon-folder-empty:before { content: '\f114'; } /* '' */
212 212 .icon-folder-open-empty:before { content: '\f115'; } /* '' */
213 213 .icon-code:before { content: '\f121'; } /* '' */
214 214 .icon-info:before { content: '\f129'; } /* '' */
215 215 .icon-minus-squared:before { content: '\f146'; } /* '' */
216 216 .icon-minus-squared-alt:before { content: '\f147'; } /* '' */
217 217 .icon-doc-inv:before { content: '\f15b'; } /* '' */
218 218 .icon-doc-text-inv:before { content: '\f15c'; } /* '' */
219 219 .icon-plus-squared-alt:before { content: '\f196'; } /* '' */
220 220 .icon-file-code:before { content: '\f1c9'; } /* '' */
221 221 .icon-history:before { content: '\f1da'; } /* '' */
222 222 .icon-circle-thin:before { content: '\f1db'; } /* '' */
223 223 .icon-sliders:before { content: '\f1de'; } /* '' */
224 224 .icon-trash:before { content: '\f1f8'; } /* '' */
225 225
226 226
227 227 // MERGED ICONS BASED ON CURRENT ONES
228 228 .icon-repo-group:before { &:extend(.icon-folder-open:before); }
229 229 .icon-repo-private:before { &:extend(.icon-lock:before); }
230 230 .icon-repo-lock:before { &:extend(.icon-lock:before); }
231 231 .icon-unlock-alt:before { &:extend(.icon-unlock:before); }
232 232 .icon-repo-unlock:before { &:extend(.icon-unlock:before); }
233 233 .icon-repo-public:before { &:extend(.icon-unlock:before); }
234 234 .icon-rss-sign:before { &:extend(.icon-feed:before); }
235 235 .icon-code-fork:before { &:extend(.icon-fork:before); }
236 236 .icon-arrow_up:before { &:extend(.icon-up:before); }
237 237 .icon-file:before { &:extend(.icon-file-code:before); }
238 238 .icon-file-text:before { &:extend(.icon-file-code:before); }
239 239 .icon-directory:before { &:extend(.icon-folder:before); }
240 240 .icon-more-linked:before { &:extend(.icon-more:before); }
241 241 .icon-clipboard:before { &:extend(.icon-docs:before); }
242 242 .icon-copy:before { &:extend(.icon-docs:before); }
243 243 .icon-true:before { &:extend(.icon-ok:before); }
244 244 .icon-false:before { &:extend(.icon-delete:before); }
245 245 .icon-expand-linked:before { &:extend(.icon-down:before); }
246 246 .icon-pr-merge-fail:before { &:extend(.icon-delete:before); }
247 247 .icon-wide-mode:before { &:extend(.icon-sort:before); }
248 248 .icon-flag-filled-red:before { &:extend(.icon-flag-filled:before); }
249 .icon-user-group-alt:before { &:extend(.icon-group:before); }
249 250
250 251 // TRANSFORM
251 252 .icon-merge:before {transform: rotate(180deg);}
252 253 .icon-wide-mode:before {transform: rotate(90deg);}
253 254
254 255 // -- END ICON CLASSES -- //
255 256
256 257
257 258 //--- ICONS STYLING ------------------//
258 259
259 260 .icon-git { color: @color4 !important; }
260 261 .icon-hg { color: @color8 !important; }
261 262 .icon-svn { color: @color1 !important; }
262 263 .icon-git-inv { color: @color4 !important; }
263 264 .icon-hg-inv { color: @color8 !important; }
264 265 .icon-svn-inv { color: @color1 !important; }
265 266 .icon-repo-lock { color: #FF0000; }
266 267 .icon-repo-unlock { color: #FF0000; }
267 268 .icon-false { color: @grey5 }
268 269 .icon-expand-linked { cursor: pointer; color: @grey3; font-size: 14px }
269 270 .icon-more-linked { cursor: pointer; color: @grey3 }
270 271 .icon-flag-filled-red { color: @color5 !important; }
271 272
272 273 .repo-switcher-dropdown .select2-result-label {
273 274 .icon-git:before {
274 275 &:extend(.icon-git-transparent:before);
275 276 }
276 277 .icon-hg:before {
277 278 &:extend(.icon-hg-transparent:before);
278 279 color: @alert4;
279 280 }
280 281 .icon-svn:before {
281 282 &:extend(.icon-svn-transparent:before);
282 283 }
283 284 }
284 285
285 286 .icon-user-group:before {
286 287 &:extend(.icon-group:before);
287 288 margin: 0;
288 289 font-size: 16px;
289 290 }
@@ -1,382 +1,385 b''
1 1
2 2 /******************************************************************************
3 3 * *
4 4 * DO NOT CHANGE THIS FILE MANUALLY *
5 5 * *
6 6 * *
7 7 * This file is automatically generated when the app starts up with *
8 8 * generate_js_files = true *
9 9 * *
10 10 * To add a route here pass jsroute=True to the route definition in the app *
11 11 * *
12 12 ******************************************************************************/
13 13 function registerRCRoutes() {
14 14 // routes registration
15 15 pyroutes.register('favicon', '/favicon.ico', []);
16 16 pyroutes.register('robots', '/robots.txt', []);
17 17 pyroutes.register('auth_home', '/_admin/auth*traverse', []);
18 18 pyroutes.register('global_integrations_new', '/_admin/integrations/new', []);
19 19 pyroutes.register('global_integrations_home', '/_admin/integrations', []);
20 20 pyroutes.register('global_integrations_list', '/_admin/integrations/%(integration)s', ['integration']);
21 21 pyroutes.register('global_integrations_create', '/_admin/integrations/%(integration)s/new', ['integration']);
22 22 pyroutes.register('global_integrations_edit', '/_admin/integrations/%(integration)s/%(integration_id)s', ['integration', 'integration_id']);
23 23 pyroutes.register('repo_group_integrations_home', '/%(repo_group_name)s/_settings/integrations', ['repo_group_name']);
24 24 pyroutes.register('repo_group_integrations_new', '/%(repo_group_name)s/_settings/integrations/new', ['repo_group_name']);
25 25 pyroutes.register('repo_group_integrations_list', '/%(repo_group_name)s/_settings/integrations/%(integration)s', ['repo_group_name', 'integration']);
26 26 pyroutes.register('repo_group_integrations_create', '/%(repo_group_name)s/_settings/integrations/%(integration)s/new', ['repo_group_name', 'integration']);
27 27 pyroutes.register('repo_group_integrations_edit', '/%(repo_group_name)s/_settings/integrations/%(integration)s/%(integration_id)s', ['repo_group_name', 'integration', 'integration_id']);
28 28 pyroutes.register('repo_integrations_home', '/%(repo_name)s/settings/integrations', ['repo_name']);
29 29 pyroutes.register('repo_integrations_new', '/%(repo_name)s/settings/integrations/new', ['repo_name']);
30 30 pyroutes.register('repo_integrations_list', '/%(repo_name)s/settings/integrations/%(integration)s', ['repo_name', 'integration']);
31 31 pyroutes.register('repo_integrations_create', '/%(repo_name)s/settings/integrations/%(integration)s/new', ['repo_name', 'integration']);
32 32 pyroutes.register('repo_integrations_edit', '/%(repo_name)s/settings/integrations/%(integration)s/%(integration_id)s', ['repo_name', 'integration', 'integration_id']);
33 pyroutes.register('hovercard_user', '/_hovercard/user/%(user_id)s', ['user_id']);
34 pyroutes.register('hovercard_user_group', '/_hovercard/user_group/%(user_group_id)s', ['user_group_id']);
35 pyroutes.register('hovercard_commit', '/_hovercard/commit/%(repo_name)s/%(user_id)s', ['repo_name', 'user_id']);
33 36 pyroutes.register('ops_ping', '/_admin/ops/ping', []);
34 37 pyroutes.register('ops_error_test', '/_admin/ops/error', []);
35 38 pyroutes.register('ops_redirect_test', '/_admin/ops/redirect', []);
36 39 pyroutes.register('ops_ping_legacy', '/_admin/ping', []);
37 40 pyroutes.register('ops_error_test_legacy', '/_admin/error_test', []);
38 41 pyroutes.register('admin_home', '/_admin', []);
39 42 pyroutes.register('admin_audit_logs', '/_admin/audit_logs', []);
40 43 pyroutes.register('admin_audit_log_entry', '/_admin/audit_logs/%(audit_log_id)s', ['audit_log_id']);
41 44 pyroutes.register('pull_requests_global_0', '/_admin/pull_requests/%(pull_request_id)s', ['pull_request_id']);
42 45 pyroutes.register('pull_requests_global_1', '/_admin/pull-requests/%(pull_request_id)s', ['pull_request_id']);
43 46 pyroutes.register('pull_requests_global', '/_admin/pull-request/%(pull_request_id)s', ['pull_request_id']);
44 47 pyroutes.register('admin_settings_open_source', '/_admin/settings/open_source', []);
45 48 pyroutes.register('admin_settings_vcs_svn_generate_cfg', '/_admin/settings/vcs/svn_generate_cfg', []);
46 49 pyroutes.register('admin_settings_system', '/_admin/settings/system', []);
47 50 pyroutes.register('admin_settings_system_update', '/_admin/settings/system/updates', []);
48 51 pyroutes.register('admin_settings_exception_tracker', '/_admin/settings/exceptions', []);
49 52 pyroutes.register('admin_settings_exception_tracker_delete_all', '/_admin/settings/exceptions/delete', []);
50 53 pyroutes.register('admin_settings_exception_tracker_show', '/_admin/settings/exceptions/%(exception_id)s', ['exception_id']);
51 54 pyroutes.register('admin_settings_exception_tracker_delete', '/_admin/settings/exceptions/%(exception_id)s/delete', ['exception_id']);
52 55 pyroutes.register('admin_settings_sessions', '/_admin/settings/sessions', []);
53 56 pyroutes.register('admin_settings_sessions_cleanup', '/_admin/settings/sessions/cleanup', []);
54 57 pyroutes.register('admin_settings_process_management', '/_admin/settings/process_management', []);
55 58 pyroutes.register('admin_settings_process_management_data', '/_admin/settings/process_management/data', []);
56 59 pyroutes.register('admin_settings_process_management_signal', '/_admin/settings/process_management/signal', []);
57 60 pyroutes.register('admin_settings_process_management_master_signal', '/_admin/settings/process_management/master_signal', []);
58 61 pyroutes.register('admin_defaults_repositories', '/_admin/defaults/repositories', []);
59 62 pyroutes.register('admin_defaults_repositories_update', '/_admin/defaults/repositories/update', []);
60 63 pyroutes.register('admin_settings', '/_admin/settings', []);
61 64 pyroutes.register('admin_settings_update', '/_admin/settings/update', []);
62 65 pyroutes.register('admin_settings_global', '/_admin/settings/global', []);
63 66 pyroutes.register('admin_settings_global_update', '/_admin/settings/global/update', []);
64 67 pyroutes.register('admin_settings_vcs', '/_admin/settings/vcs', []);
65 68 pyroutes.register('admin_settings_vcs_update', '/_admin/settings/vcs/update', []);
66 69 pyroutes.register('admin_settings_vcs_svn_pattern_delete', '/_admin/settings/vcs/svn_pattern_delete', []);
67 70 pyroutes.register('admin_settings_mapping', '/_admin/settings/mapping', []);
68 71 pyroutes.register('admin_settings_mapping_update', '/_admin/settings/mapping/update', []);
69 72 pyroutes.register('admin_settings_visual', '/_admin/settings/visual', []);
70 73 pyroutes.register('admin_settings_visual_update', '/_admin/settings/visual/update', []);
71 74 pyroutes.register('admin_settings_issuetracker', '/_admin/settings/issue-tracker', []);
72 75 pyroutes.register('admin_settings_issuetracker_update', '/_admin/settings/issue-tracker/update', []);
73 76 pyroutes.register('admin_settings_issuetracker_test', '/_admin/settings/issue-tracker/test', []);
74 77 pyroutes.register('admin_settings_issuetracker_delete', '/_admin/settings/issue-tracker/delete', []);
75 78 pyroutes.register('admin_settings_email', '/_admin/settings/email', []);
76 79 pyroutes.register('admin_settings_email_update', '/_admin/settings/email/update', []);
77 80 pyroutes.register('admin_settings_hooks', '/_admin/settings/hooks', []);
78 81 pyroutes.register('admin_settings_hooks_update', '/_admin/settings/hooks/update', []);
79 82 pyroutes.register('admin_settings_hooks_delete', '/_admin/settings/hooks/delete', []);
80 83 pyroutes.register('admin_settings_search', '/_admin/settings/search', []);
81 84 pyroutes.register('admin_settings_labs', '/_admin/settings/labs', []);
82 85 pyroutes.register('admin_settings_labs_update', '/_admin/settings/labs/update', []);
83 86 pyroutes.register('admin_permissions_application', '/_admin/permissions/application', []);
84 87 pyroutes.register('admin_permissions_application_update', '/_admin/permissions/application/update', []);
85 88 pyroutes.register('admin_permissions_global', '/_admin/permissions/global', []);
86 89 pyroutes.register('admin_permissions_global_update', '/_admin/permissions/global/update', []);
87 90 pyroutes.register('admin_permissions_object', '/_admin/permissions/object', []);
88 91 pyroutes.register('admin_permissions_object_update', '/_admin/permissions/object/update', []);
89 92 pyroutes.register('admin_permissions_ips', '/_admin/permissions/ips', []);
90 93 pyroutes.register('admin_permissions_overview', '/_admin/permissions/overview', []);
91 94 pyroutes.register('admin_permissions_auth_token_access', '/_admin/permissions/auth_token_access', []);
92 95 pyroutes.register('admin_permissions_ssh_keys', '/_admin/permissions/ssh_keys', []);
93 96 pyroutes.register('admin_permissions_ssh_keys_data', '/_admin/permissions/ssh_keys/data', []);
94 97 pyroutes.register('admin_permissions_ssh_keys_update', '/_admin/permissions/ssh_keys/update', []);
95 98 pyroutes.register('users', '/_admin/users', []);
96 99 pyroutes.register('users_data', '/_admin/users_data', []);
97 100 pyroutes.register('users_create', '/_admin/users/create', []);
98 101 pyroutes.register('users_new', '/_admin/users/new', []);
99 102 pyroutes.register('user_edit', '/_admin/users/%(user_id)s/edit', ['user_id']);
100 103 pyroutes.register('user_edit_advanced', '/_admin/users/%(user_id)s/edit/advanced', ['user_id']);
101 104 pyroutes.register('user_edit_global_perms', '/_admin/users/%(user_id)s/edit/global_permissions', ['user_id']);
102 105 pyroutes.register('user_edit_global_perms_update', '/_admin/users/%(user_id)s/edit/global_permissions/update', ['user_id']);
103 106 pyroutes.register('user_update', '/_admin/users/%(user_id)s/update', ['user_id']);
104 107 pyroutes.register('user_delete', '/_admin/users/%(user_id)s/delete', ['user_id']);
105 108 pyroutes.register('user_enable_force_password_reset', '/_admin/users/%(user_id)s/password_reset_enable', ['user_id']);
106 109 pyroutes.register('user_disable_force_password_reset', '/_admin/users/%(user_id)s/password_reset_disable', ['user_id']);
107 110 pyroutes.register('user_create_personal_repo_group', '/_admin/users/%(user_id)s/create_repo_group', ['user_id']);
108 111 pyroutes.register('edit_user_auth_tokens_delete', '/_admin/users/%(user_id)s/edit/auth_tokens/delete', ['user_id']);
109 112 pyroutes.register('edit_user_ssh_keys', '/_admin/users/%(user_id)s/edit/ssh_keys', ['user_id']);
110 113 pyroutes.register('edit_user_ssh_keys_generate_keypair', '/_admin/users/%(user_id)s/edit/ssh_keys/generate', ['user_id']);
111 114 pyroutes.register('edit_user_ssh_keys_add', '/_admin/users/%(user_id)s/edit/ssh_keys/new', ['user_id']);
112 115 pyroutes.register('edit_user_ssh_keys_delete', '/_admin/users/%(user_id)s/edit/ssh_keys/delete', ['user_id']);
113 116 pyroutes.register('edit_user_emails', '/_admin/users/%(user_id)s/edit/emails', ['user_id']);
114 117 pyroutes.register('edit_user_emails_add', '/_admin/users/%(user_id)s/edit/emails/new', ['user_id']);
115 118 pyroutes.register('edit_user_emails_delete', '/_admin/users/%(user_id)s/edit/emails/delete', ['user_id']);
116 119 pyroutes.register('edit_user_ips', '/_admin/users/%(user_id)s/edit/ips', ['user_id']);
117 120 pyroutes.register('edit_user_ips_add', '/_admin/users/%(user_id)s/edit/ips/new', ['user_id']);
118 121 pyroutes.register('edit_user_ips_delete', '/_admin/users/%(user_id)s/edit/ips/delete', ['user_id']);
119 122 pyroutes.register('edit_user_perms_summary', '/_admin/users/%(user_id)s/edit/permissions_summary', ['user_id']);
120 123 pyroutes.register('edit_user_perms_summary_json', '/_admin/users/%(user_id)s/edit/permissions_summary/json', ['user_id']);
121 124 pyroutes.register('edit_user_groups_management', '/_admin/users/%(user_id)s/edit/groups_management', ['user_id']);
122 125 pyroutes.register('edit_user_groups_management_updates', '/_admin/users/%(user_id)s/edit/edit_user_groups_management/updates', ['user_id']);
123 126 pyroutes.register('edit_user_audit_logs', '/_admin/users/%(user_id)s/edit/audit', ['user_id']);
124 127 pyroutes.register('edit_user_audit_logs_download', '/_admin/users/%(user_id)s/edit/audit/download', ['user_id']);
125 128 pyroutes.register('edit_user_caches', '/_admin/users/%(user_id)s/edit/caches', ['user_id']);
126 129 pyroutes.register('edit_user_caches_update', '/_admin/users/%(user_id)s/edit/caches/update', ['user_id']);
127 130 pyroutes.register('user_groups', '/_admin/user_groups', []);
128 131 pyroutes.register('user_groups_data', '/_admin/user_groups_data', []);
129 132 pyroutes.register('user_groups_new', '/_admin/user_groups/new', []);
130 133 pyroutes.register('user_groups_create', '/_admin/user_groups/create', []);
131 134 pyroutes.register('repos', '/_admin/repos', []);
132 135 pyroutes.register('repo_new', '/_admin/repos/new', []);
133 136 pyroutes.register('repo_create', '/_admin/repos/create', []);
134 137 pyroutes.register('repo_groups', '/_admin/repo_groups', []);
135 138 pyroutes.register('repo_groups_data', '/_admin/repo_groups_data', []);
136 139 pyroutes.register('repo_group_new', '/_admin/repo_group/new', []);
137 140 pyroutes.register('repo_group_create', '/_admin/repo_group/create', []);
138 141 pyroutes.register('channelstream_connect', '/_admin/channelstream/connect', []);
139 142 pyroutes.register('channelstream_subscribe', '/_admin/channelstream/subscribe', []);
140 143 pyroutes.register('channelstream_proxy', '/_channelstream', []);
141 144 pyroutes.register('upload_file', '/_file_store/upload', []);
142 145 pyroutes.register('download_file', '/_file_store/download/%(fid)s', ['fid']);
143 146 pyroutes.register('download_file_by_token', '/_file_store/token-download/%(_auth_token)s/%(fid)s', ['_auth_token', 'fid']);
144 147 pyroutes.register('logout', '/_admin/logout', []);
145 148 pyroutes.register('reset_password', '/_admin/password_reset', []);
146 149 pyroutes.register('reset_password_confirmation', '/_admin/password_reset_confirmation', []);
147 150 pyroutes.register('home', '/', []);
148 151 pyroutes.register('user_autocomplete_data', '/_users', []);
149 152 pyroutes.register('user_group_autocomplete_data', '/_user_groups', []);
150 153 pyroutes.register('repo_list_data', '/_repos', []);
151 154 pyroutes.register('repo_group_list_data', '/_repo_groups', []);
152 155 pyroutes.register('goto_switcher_data', '/_goto_data', []);
153 156 pyroutes.register('markup_preview', '/_markup_preview', []);
154 157 pyroutes.register('file_preview', '/_file_preview', []);
155 158 pyroutes.register('store_user_session_value', '/_store_session_attr', []);
156 159 pyroutes.register('journal', '/_admin/journal', []);
157 160 pyroutes.register('journal_rss', '/_admin/journal/rss', []);
158 161 pyroutes.register('journal_atom', '/_admin/journal/atom', []);
159 162 pyroutes.register('journal_public', '/_admin/public_journal', []);
160 163 pyroutes.register('journal_public_atom', '/_admin/public_journal/atom', []);
161 164 pyroutes.register('journal_public_atom_old', '/_admin/public_journal_atom', []);
162 165 pyroutes.register('journal_public_rss', '/_admin/public_journal/rss', []);
163 166 pyroutes.register('journal_public_rss_old', '/_admin/public_journal_rss', []);
164 167 pyroutes.register('toggle_following', '/_admin/toggle_following', []);
165 168 pyroutes.register('repo_creating', '/%(repo_name)s/repo_creating', ['repo_name']);
166 169 pyroutes.register('repo_creating_check', '/%(repo_name)s/repo_creating_check', ['repo_name']);
167 170 pyroutes.register('repo_summary_explicit', '/%(repo_name)s/summary', ['repo_name']);
168 171 pyroutes.register('repo_summary_commits', '/%(repo_name)s/summary-commits', ['repo_name']);
169 172 pyroutes.register('repo_commit', '/%(repo_name)s/changeset/%(commit_id)s', ['repo_name', 'commit_id']);
170 173 pyroutes.register('repo_commit_children', '/%(repo_name)s/changeset_children/%(commit_id)s', ['repo_name', 'commit_id']);
171 174 pyroutes.register('repo_commit_parents', '/%(repo_name)s/changeset_parents/%(commit_id)s', ['repo_name', 'commit_id']);
172 175 pyroutes.register('repo_commit_raw', '/%(repo_name)s/changeset-diff/%(commit_id)s', ['repo_name', 'commit_id']);
173 176 pyroutes.register('repo_commit_patch', '/%(repo_name)s/changeset-patch/%(commit_id)s', ['repo_name', 'commit_id']);
174 177 pyroutes.register('repo_commit_download', '/%(repo_name)s/changeset-download/%(commit_id)s', ['repo_name', 'commit_id']);
175 178 pyroutes.register('repo_commit_data', '/%(repo_name)s/changeset-data/%(commit_id)s', ['repo_name', 'commit_id']);
176 179 pyroutes.register('repo_commit_comment_create', '/%(repo_name)s/changeset/%(commit_id)s/comment/create', ['repo_name', 'commit_id']);
177 180 pyroutes.register('repo_commit_comment_preview', '/%(repo_name)s/changeset/%(commit_id)s/comment/preview', ['repo_name', 'commit_id']);
178 181 pyroutes.register('repo_commit_comment_attachment_upload', '/%(repo_name)s/changeset/%(commit_id)s/comment/attachment_upload', ['repo_name', 'commit_id']);
179 182 pyroutes.register('repo_commit_comment_delete', '/%(repo_name)s/changeset/%(commit_id)s/comment/%(comment_id)s/delete', ['repo_name', 'commit_id', 'comment_id']);
180 183 pyroutes.register('repo_commit_raw_deprecated', '/%(repo_name)s/raw-changeset/%(commit_id)s', ['repo_name', 'commit_id']);
181 184 pyroutes.register('repo_archivefile', '/%(repo_name)s/archive/%(fname)s', ['repo_name', 'fname']);
182 185 pyroutes.register('repo_files_diff', '/%(repo_name)s/diff/%(f_path)s', ['repo_name', 'f_path']);
183 186 pyroutes.register('repo_files_diff_2way_redirect', '/%(repo_name)s/diff-2way/%(f_path)s', ['repo_name', 'f_path']);
184 187 pyroutes.register('repo_files', '/%(repo_name)s/files/%(commit_id)s/%(f_path)s', ['repo_name', 'commit_id', 'f_path']);
185 188 pyroutes.register('repo_files:default_path', '/%(repo_name)s/files/%(commit_id)s/', ['repo_name', 'commit_id']);
186 189 pyroutes.register('repo_files:default_commit', '/%(repo_name)s/files', ['repo_name']);
187 190 pyroutes.register('repo_files:rendered', '/%(repo_name)s/render/%(commit_id)s/%(f_path)s', ['repo_name', 'commit_id', 'f_path']);
188 191 pyroutes.register('repo_files:annotated', '/%(repo_name)s/annotate/%(commit_id)s/%(f_path)s', ['repo_name', 'commit_id', 'f_path']);
189 192 pyroutes.register('repo_files:annotated_previous', '/%(repo_name)s/annotate-previous/%(commit_id)s/%(f_path)s', ['repo_name', 'commit_id', 'f_path']);
190 193 pyroutes.register('repo_nodetree_full', '/%(repo_name)s/nodetree_full/%(commit_id)s/%(f_path)s', ['repo_name', 'commit_id', 'f_path']);
191 194 pyroutes.register('repo_nodetree_full:default_path', '/%(repo_name)s/nodetree_full/%(commit_id)s/', ['repo_name', 'commit_id']);
192 195 pyroutes.register('repo_files_nodelist', '/%(repo_name)s/nodelist/%(commit_id)s/%(f_path)s', ['repo_name', 'commit_id', 'f_path']);
193 196 pyroutes.register('repo_file_raw', '/%(repo_name)s/raw/%(commit_id)s/%(f_path)s', ['repo_name', 'commit_id', 'f_path']);
194 197 pyroutes.register('repo_file_download', '/%(repo_name)s/download/%(commit_id)s/%(f_path)s', ['repo_name', 'commit_id', 'f_path']);
195 198 pyroutes.register('repo_file_download:legacy', '/%(repo_name)s/rawfile/%(commit_id)s/%(f_path)s', ['repo_name', 'commit_id', 'f_path']);
196 199 pyroutes.register('repo_file_history', '/%(repo_name)s/history/%(commit_id)s/%(f_path)s', ['repo_name', 'commit_id', 'f_path']);
197 200 pyroutes.register('repo_file_authors', '/%(repo_name)s/authors/%(commit_id)s/%(f_path)s', ['repo_name', 'commit_id', 'f_path']);
198 201 pyroutes.register('repo_files_remove_file', '/%(repo_name)s/remove_file/%(commit_id)s/%(f_path)s', ['repo_name', 'commit_id', 'f_path']);
199 202 pyroutes.register('repo_files_delete_file', '/%(repo_name)s/delete_file/%(commit_id)s/%(f_path)s', ['repo_name', 'commit_id', 'f_path']);
200 203 pyroutes.register('repo_files_edit_file', '/%(repo_name)s/edit_file/%(commit_id)s/%(f_path)s', ['repo_name', 'commit_id', 'f_path']);
201 204 pyroutes.register('repo_files_update_file', '/%(repo_name)s/update_file/%(commit_id)s/%(f_path)s', ['repo_name', 'commit_id', 'f_path']);
202 205 pyroutes.register('repo_files_add_file', '/%(repo_name)s/add_file/%(commit_id)s/%(f_path)s', ['repo_name', 'commit_id', 'f_path']);
203 206 pyroutes.register('repo_files_upload_file', '/%(repo_name)s/upload_file/%(commit_id)s/%(f_path)s', ['repo_name', 'commit_id', 'f_path']);
204 207 pyroutes.register('repo_files_create_file', '/%(repo_name)s/create_file/%(commit_id)s/%(f_path)s', ['repo_name', 'commit_id', 'f_path']);
205 208 pyroutes.register('repo_refs_data', '/%(repo_name)s/refs-data', ['repo_name']);
206 209 pyroutes.register('repo_refs_changelog_data', '/%(repo_name)s/refs-data-changelog', ['repo_name']);
207 210 pyroutes.register('repo_stats', '/%(repo_name)s/repo_stats/%(commit_id)s', ['repo_name', 'commit_id']);
208 211 pyroutes.register('repo_commits', '/%(repo_name)s/commits', ['repo_name']);
209 212 pyroutes.register('repo_commits_file', '/%(repo_name)s/commits/%(commit_id)s/%(f_path)s', ['repo_name', 'commit_id', 'f_path']);
210 213 pyroutes.register('repo_commits_elements', '/%(repo_name)s/commits_elements', ['repo_name']);
211 214 pyroutes.register('repo_commits_elements_file', '/%(repo_name)s/commits_elements/%(commit_id)s/%(f_path)s', ['repo_name', 'commit_id', 'f_path']);
212 215 pyroutes.register('repo_changelog', '/%(repo_name)s/changelog', ['repo_name']);
213 216 pyroutes.register('repo_changelog_file', '/%(repo_name)s/changelog/%(commit_id)s/%(f_path)s', ['repo_name', 'commit_id', 'f_path']);
214 217 pyroutes.register('repo_compare_select', '/%(repo_name)s/compare', ['repo_name']);
215 218 pyroutes.register('repo_compare', '/%(repo_name)s/compare/%(source_ref_type)s@%(source_ref)s...%(target_ref_type)s@%(target_ref)s', ['repo_name', 'source_ref_type', 'source_ref', 'target_ref_type', 'target_ref']);
216 219 pyroutes.register('tags_home', '/%(repo_name)s/tags', ['repo_name']);
217 220 pyroutes.register('branches_home', '/%(repo_name)s/branches', ['repo_name']);
218 221 pyroutes.register('bookmarks_home', '/%(repo_name)s/bookmarks', ['repo_name']);
219 222 pyroutes.register('repo_fork_new', '/%(repo_name)s/fork', ['repo_name']);
220 223 pyroutes.register('repo_fork_create', '/%(repo_name)s/fork/create', ['repo_name']);
221 224 pyroutes.register('repo_forks_show_all', '/%(repo_name)s/forks', ['repo_name']);
222 225 pyroutes.register('repo_forks_data', '/%(repo_name)s/forks/data', ['repo_name']);
223 226 pyroutes.register('pullrequest_show', '/%(repo_name)s/pull-request/%(pull_request_id)s', ['repo_name', 'pull_request_id']);
224 227 pyroutes.register('pullrequest_show_all', '/%(repo_name)s/pull-request', ['repo_name']);
225 228 pyroutes.register('pullrequest_show_all_data', '/%(repo_name)s/pull-request-data', ['repo_name']);
226 229 pyroutes.register('pullrequest_repo_refs', '/%(repo_name)s/pull-request/refs/%(target_repo_name)s', ['repo_name', 'target_repo_name']);
227 230 pyroutes.register('pullrequest_repo_targets', '/%(repo_name)s/pull-request/repo-targets', ['repo_name']);
228 231 pyroutes.register('pullrequest_new', '/%(repo_name)s/pull-request/new', ['repo_name']);
229 232 pyroutes.register('pullrequest_create', '/%(repo_name)s/pull-request/create', ['repo_name']);
230 233 pyroutes.register('pullrequest_update', '/%(repo_name)s/pull-request/%(pull_request_id)s/update', ['repo_name', 'pull_request_id']);
231 234 pyroutes.register('pullrequest_merge', '/%(repo_name)s/pull-request/%(pull_request_id)s/merge', ['repo_name', 'pull_request_id']);
232 235 pyroutes.register('pullrequest_delete', '/%(repo_name)s/pull-request/%(pull_request_id)s/delete', ['repo_name', 'pull_request_id']);
233 236 pyroutes.register('pullrequest_comment_create', '/%(repo_name)s/pull-request/%(pull_request_id)s/comment', ['repo_name', 'pull_request_id']);
234 237 pyroutes.register('pullrequest_comment_delete', '/%(repo_name)s/pull-request/%(pull_request_id)s/comment/%(comment_id)s/delete', ['repo_name', 'pull_request_id', 'comment_id']);
235 238 pyroutes.register('edit_repo', '/%(repo_name)s/settings', ['repo_name']);
236 239 pyroutes.register('edit_repo_advanced', '/%(repo_name)s/settings/advanced', ['repo_name']);
237 240 pyroutes.register('edit_repo_advanced_archive', '/%(repo_name)s/settings/advanced/archive', ['repo_name']);
238 241 pyroutes.register('edit_repo_advanced_delete', '/%(repo_name)s/settings/advanced/delete', ['repo_name']);
239 242 pyroutes.register('edit_repo_advanced_locking', '/%(repo_name)s/settings/advanced/locking', ['repo_name']);
240 243 pyroutes.register('edit_repo_advanced_journal', '/%(repo_name)s/settings/advanced/journal', ['repo_name']);
241 244 pyroutes.register('edit_repo_advanced_fork', '/%(repo_name)s/settings/advanced/fork', ['repo_name']);
242 245 pyroutes.register('edit_repo_advanced_hooks', '/%(repo_name)s/settings/advanced/hooks', ['repo_name']);
243 246 pyroutes.register('edit_repo_caches', '/%(repo_name)s/settings/caches', ['repo_name']);
244 247 pyroutes.register('edit_repo_perms', '/%(repo_name)s/settings/permissions', ['repo_name']);
245 248 pyroutes.register('edit_repo_perms_set_private', '/%(repo_name)s/settings/permissions/set_private', ['repo_name']);
246 249 pyroutes.register('edit_repo_maintenance', '/%(repo_name)s/settings/maintenance', ['repo_name']);
247 250 pyroutes.register('edit_repo_maintenance_execute', '/%(repo_name)s/settings/maintenance/execute', ['repo_name']);
248 251 pyroutes.register('edit_repo_fields', '/%(repo_name)s/settings/fields', ['repo_name']);
249 252 pyroutes.register('edit_repo_fields_create', '/%(repo_name)s/settings/fields/create', ['repo_name']);
250 253 pyroutes.register('edit_repo_fields_delete', '/%(repo_name)s/settings/fields/%(field_id)s/delete', ['repo_name', 'field_id']);
251 254 pyroutes.register('repo_edit_toggle_locking', '/%(repo_name)s/settings/toggle_locking', ['repo_name']);
252 255 pyroutes.register('edit_repo_remote', '/%(repo_name)s/settings/remote', ['repo_name']);
253 256 pyroutes.register('edit_repo_remote_pull', '/%(repo_name)s/settings/remote/pull', ['repo_name']);
254 257 pyroutes.register('edit_repo_statistics', '/%(repo_name)s/settings/statistics', ['repo_name']);
255 258 pyroutes.register('edit_repo_statistics_reset', '/%(repo_name)s/settings/statistics/update', ['repo_name']);
256 259 pyroutes.register('edit_repo_issuetracker', '/%(repo_name)s/settings/issue_trackers', ['repo_name']);
257 260 pyroutes.register('edit_repo_issuetracker_test', '/%(repo_name)s/settings/issue_trackers/test', ['repo_name']);
258 261 pyroutes.register('edit_repo_issuetracker_delete', '/%(repo_name)s/settings/issue_trackers/delete', ['repo_name']);
259 262 pyroutes.register('edit_repo_issuetracker_update', '/%(repo_name)s/settings/issue_trackers/update', ['repo_name']);
260 263 pyroutes.register('edit_repo_vcs', '/%(repo_name)s/settings/vcs', ['repo_name']);
261 264 pyroutes.register('edit_repo_vcs_update', '/%(repo_name)s/settings/vcs/update', ['repo_name']);
262 265 pyroutes.register('edit_repo_vcs_svn_pattern_delete', '/%(repo_name)s/settings/vcs/svn_pattern/delete', ['repo_name']);
263 266 pyroutes.register('repo_reviewers', '/%(repo_name)s/settings/review/rules', ['repo_name']);
264 267 pyroutes.register('repo_default_reviewers_data', '/%(repo_name)s/settings/review/default-reviewers', ['repo_name']);
265 268 pyroutes.register('edit_repo_strip', '/%(repo_name)s/settings/strip', ['repo_name']);
266 269 pyroutes.register('strip_check', '/%(repo_name)s/settings/strip_check', ['repo_name']);
267 270 pyroutes.register('strip_execute', '/%(repo_name)s/settings/strip_execute', ['repo_name']);
268 271 pyroutes.register('edit_repo_audit_logs', '/%(repo_name)s/settings/audit_logs', ['repo_name']);
269 272 pyroutes.register('rss_feed_home', '/%(repo_name)s/feed-rss', ['repo_name']);
270 273 pyroutes.register('atom_feed_home', '/%(repo_name)s/feed-atom', ['repo_name']);
271 274 pyroutes.register('rss_feed_home_old', '/%(repo_name)s/feed/rss', ['repo_name']);
272 275 pyroutes.register('atom_feed_home_old', '/%(repo_name)s/feed/atom', ['repo_name']);
273 276 pyroutes.register('repo_summary', '/%(repo_name)s', ['repo_name']);
274 277 pyroutes.register('repo_summary_slash', '/%(repo_name)s/', ['repo_name']);
275 278 pyroutes.register('edit_repo_group', '/%(repo_group_name)s/_edit', ['repo_group_name']);
276 279 pyroutes.register('edit_repo_group_advanced', '/%(repo_group_name)s/_settings/advanced', ['repo_group_name']);
277 280 pyroutes.register('edit_repo_group_advanced_delete', '/%(repo_group_name)s/_settings/advanced/delete', ['repo_group_name']);
278 281 pyroutes.register('edit_repo_group_perms', '/%(repo_group_name)s/_settings/permissions', ['repo_group_name']);
279 282 pyroutes.register('edit_repo_group_perms_update', '/%(repo_group_name)s/_settings/permissions/update', ['repo_group_name']);
280 283 pyroutes.register('repo_group_home', '/%(repo_group_name)s', ['repo_group_name']);
281 284 pyroutes.register('repo_group_home_slash', '/%(repo_group_name)s/', ['repo_group_name']);
282 285 pyroutes.register('user_group_members_data', '/_admin/user_groups/%(user_group_id)s/members', ['user_group_id']);
283 286 pyroutes.register('edit_user_group_perms_summary', '/_admin/user_groups/%(user_group_id)s/edit/permissions_summary', ['user_group_id']);
284 287 pyroutes.register('edit_user_group_perms_summary_json', '/_admin/user_groups/%(user_group_id)s/edit/permissions_summary/json', ['user_group_id']);
285 288 pyroutes.register('edit_user_group', '/_admin/user_groups/%(user_group_id)s/edit', ['user_group_id']);
286 289 pyroutes.register('user_groups_update', '/_admin/user_groups/%(user_group_id)s/update', ['user_group_id']);
287 290 pyroutes.register('edit_user_group_global_perms', '/_admin/user_groups/%(user_group_id)s/edit/global_permissions', ['user_group_id']);
288 291 pyroutes.register('edit_user_group_global_perms_update', '/_admin/user_groups/%(user_group_id)s/edit/global_permissions/update', ['user_group_id']);
289 292 pyroutes.register('edit_user_group_perms', '/_admin/user_groups/%(user_group_id)s/edit/permissions', ['user_group_id']);
290 293 pyroutes.register('edit_user_group_perms_update', '/_admin/user_groups/%(user_group_id)s/edit/permissions/update', ['user_group_id']);
291 294 pyroutes.register('edit_user_group_advanced', '/_admin/user_groups/%(user_group_id)s/edit/advanced', ['user_group_id']);
292 295 pyroutes.register('edit_user_group_advanced_sync', '/_admin/user_groups/%(user_group_id)s/edit/advanced/sync', ['user_group_id']);
293 296 pyroutes.register('user_groups_delete', '/_admin/user_groups/%(user_group_id)s/delete', ['user_group_id']);
294 297 pyroutes.register('search', '/_admin/search', []);
295 298 pyroutes.register('search_repo', '/%(repo_name)s/_search', ['repo_name']);
296 299 pyroutes.register('search_repo_alt', '/%(repo_name)s/search', ['repo_name']);
297 300 pyroutes.register('search_repo_group', '/%(repo_group_name)s/_search', ['repo_group_name']);
298 301 pyroutes.register('user_profile', '/_profiles/%(username)s', ['username']);
299 302 pyroutes.register('user_group_profile', '/_profile_user_group/%(user_group_name)s', ['user_group_name']);
300 303 pyroutes.register('my_account_profile', '/_admin/my_account/profile', []);
301 304 pyroutes.register('my_account_edit', '/_admin/my_account/edit', []);
302 305 pyroutes.register('my_account_update', '/_admin/my_account/update', []);
303 306 pyroutes.register('my_account_password', '/_admin/my_account/password', []);
304 307 pyroutes.register('my_account_password_update', '/_admin/my_account/password/update', []);
305 308 pyroutes.register('my_account_auth_tokens_delete', '/_admin/my_account/auth_tokens/delete', []);
306 309 pyroutes.register('my_account_ssh_keys', '/_admin/my_account/ssh_keys', []);
307 310 pyroutes.register('my_account_ssh_keys_generate', '/_admin/my_account/ssh_keys/generate', []);
308 311 pyroutes.register('my_account_ssh_keys_add', '/_admin/my_account/ssh_keys/new', []);
309 312 pyroutes.register('my_account_ssh_keys_delete', '/_admin/my_account/ssh_keys/delete', []);
310 313 pyroutes.register('my_account_user_group_membership', '/_admin/my_account/user_group_membership', []);
311 314 pyroutes.register('my_account_emails', '/_admin/my_account/emails', []);
312 315 pyroutes.register('my_account_emails_add', '/_admin/my_account/emails/new', []);
313 316 pyroutes.register('my_account_emails_delete', '/_admin/my_account/emails/delete', []);
314 317 pyroutes.register('my_account_repos', '/_admin/my_account/repos', []);
315 318 pyroutes.register('my_account_watched', '/_admin/my_account/watched', []);
316 319 pyroutes.register('my_account_bookmarks', '/_admin/my_account/bookmarks', []);
317 320 pyroutes.register('my_account_bookmarks_update', '/_admin/my_account/bookmarks/update', []);
318 321 pyroutes.register('my_account_goto_bookmark', '/_admin/my_account/bookmark/%(bookmark_id)s', ['bookmark_id']);
319 322 pyroutes.register('my_account_perms', '/_admin/my_account/perms', []);
320 323 pyroutes.register('my_account_notifications', '/_admin/my_account/notifications', []);
321 324 pyroutes.register('my_account_notifications_toggle_visibility', '/_admin/my_account/toggle_visibility', []);
322 325 pyroutes.register('my_account_pullrequests', '/_admin/my_account/pull_requests', []);
323 326 pyroutes.register('my_account_pullrequests_data', '/_admin/my_account/pull_requests/data', []);
324 327 pyroutes.register('notifications_show_all', '/_admin/notifications', []);
325 328 pyroutes.register('notifications_mark_all_read', '/_admin/notifications/mark_all_read', []);
326 329 pyroutes.register('notifications_show', '/_admin/notifications/%(notification_id)s', ['notification_id']);
327 330 pyroutes.register('notifications_update', '/_admin/notifications/%(notification_id)s/update', ['notification_id']);
328 331 pyroutes.register('notifications_delete', '/_admin/notifications/%(notification_id)s/delete', ['notification_id']);
329 332 pyroutes.register('my_account_notifications_test_channelstream', '/_admin/my_account/test_channelstream', []);
330 333 pyroutes.register('gists_show', '/_admin/gists', []);
331 334 pyroutes.register('gists_new', '/_admin/gists/new', []);
332 335 pyroutes.register('gists_create', '/_admin/gists/create', []);
333 336 pyroutes.register('gist_show', '/_admin/gists/%(gist_id)s', ['gist_id']);
334 337 pyroutes.register('gist_delete', '/_admin/gists/%(gist_id)s/delete', ['gist_id']);
335 338 pyroutes.register('gist_edit', '/_admin/gists/%(gist_id)s/edit', ['gist_id']);
336 339 pyroutes.register('gist_edit_check_revision', '/_admin/gists/%(gist_id)s/edit/check_revision', ['gist_id']);
337 340 pyroutes.register('gist_update', '/_admin/gists/%(gist_id)s/update', ['gist_id']);
338 341 pyroutes.register('gist_show_rev', '/_admin/gists/%(gist_id)s/%(revision)s', ['gist_id', 'revision']);
339 342 pyroutes.register('gist_show_formatted', '/_admin/gists/%(gist_id)s/%(revision)s/%(format)s', ['gist_id', 'revision', 'format']);
340 343 pyroutes.register('gist_show_formatted_path', '/_admin/gists/%(gist_id)s/%(revision)s/%(format)s/%(f_path)s', ['gist_id', 'revision', 'format', 'f_path']);
341 344 pyroutes.register('debug_style_home', '/_admin/debug_style', []);
342 345 pyroutes.register('debug_style_template', '/_admin/debug_style/t/%(t_path)s', ['t_path']);
343 346 pyroutes.register('apiv2', '/_admin/api', []);
344 347 pyroutes.register('admin_settings_license', '/_admin/settings/license', []);
345 348 pyroutes.register('admin_settings_license_unlock', '/_admin/settings/license_unlock', []);
346 349 pyroutes.register('login', '/_admin/login', []);
347 350 pyroutes.register('register', '/_admin/register', []);
348 351 pyroutes.register('repo_reviewers_review_rule_new', '/%(repo_name)s/settings/review/rules/new', ['repo_name']);
349 352 pyroutes.register('repo_reviewers_review_rule_edit', '/%(repo_name)s/settings/review/rules/%(rule_id)s', ['repo_name', 'rule_id']);
350 353 pyroutes.register('repo_reviewers_review_rule_delete', '/%(repo_name)s/settings/review/rules/%(rule_id)s/delete', ['repo_name', 'rule_id']);
351 354 pyroutes.register('plugin_admin_chat', '/_admin/plugin_admin_chat/%(action)s', ['action']);
352 355 pyroutes.register('edit_user_auth_tokens', '/_admin/users/%(user_id)s/edit/auth_tokens', ['user_id']);
353 356 pyroutes.register('edit_user_auth_tokens_add', '/_admin/users/%(user_id)s/edit/auth_tokens/new', ['user_id']);
354 357 pyroutes.register('admin_settings_scheduler_show_tasks', '/_admin/settings/scheduler/_tasks', []);
355 358 pyroutes.register('admin_settings_scheduler_show_all', '/_admin/settings/scheduler', []);
356 359 pyroutes.register('admin_settings_scheduler_new', '/_admin/settings/scheduler/new', []);
357 360 pyroutes.register('admin_settings_scheduler_create', '/_admin/settings/scheduler/create', []);
358 361 pyroutes.register('admin_settings_scheduler_edit', '/_admin/settings/scheduler/%(schedule_id)s', ['schedule_id']);
359 362 pyroutes.register('admin_settings_scheduler_update', '/_admin/settings/scheduler/%(schedule_id)s/update', ['schedule_id']);
360 363 pyroutes.register('admin_settings_scheduler_delete', '/_admin/settings/scheduler/%(schedule_id)s/delete', ['schedule_id']);
361 364 pyroutes.register('admin_settings_scheduler_execute', '/_admin/settings/scheduler/%(schedule_id)s/execute', ['schedule_id']);
362 365 pyroutes.register('admin_settings_automation', '/_admin/settings/automation', []);
363 366 pyroutes.register('admin_settings_automation_update', '/_admin/settings/automation/%(entry_id)s/update', ['entry_id']);
364 367 pyroutes.register('admin_permissions_branch', '/_admin/permissions/branch', []);
365 368 pyroutes.register('admin_permissions_branch_update', '/_admin/permissions/branch/update', []);
366 369 pyroutes.register('my_account_auth_tokens', '/_admin/my_account/auth_tokens', []);
367 370 pyroutes.register('my_account_auth_tokens_add', '/_admin/my_account/auth_tokens/new', []);
368 371 pyroutes.register('my_account_external_identity', '/_admin/my_account/external-identity', []);
369 372 pyroutes.register('my_account_external_identity_delete', '/_admin/my_account/external-identity/delete', []);
370 373 pyroutes.register('repo_artifacts_list', '/%(repo_name)s/artifacts', ['repo_name']);
371 374 pyroutes.register('repo_artifacts_data', '/%(repo_name)s/artifacts_data', ['repo_name']);
372 375 pyroutes.register('repo_artifacts_new', '/%(repo_name)s/artifacts/new', ['repo_name']);
373 376 pyroutes.register('repo_artifacts_get', '/%(repo_name)s/artifacts/download/%(uid)s', ['repo_name', 'uid']);
374 377 pyroutes.register('repo_artifacts_store', '/%(repo_name)s/artifacts/store', ['repo_name']);
375 378 pyroutes.register('repo_artifacts_info', '/%(repo_name)s/artifacts/info/%(uid)s', ['repo_name', 'uid']);
376 379 pyroutes.register('repo_artifacts_delete', '/%(repo_name)s/artifacts/delete/%(uid)s', ['repo_name', 'uid']);
377 380 pyroutes.register('repo_automation', '/%(repo_name)s/settings/automation', ['repo_name']);
378 381 pyroutes.register('repo_automation_update', '/%(repo_name)s/settings/automation/%(entry_id)s/update', ['repo_name', 'entry_id']);
379 382 pyroutes.register('edit_repo_remote_push', '/%(repo_name)s/settings/remote/push', ['repo_name']);
380 383 pyroutes.register('edit_repo_perms_branch', '/%(repo_name)s/settings/branch_permissions', ['repo_name']);
381 384 pyroutes.register('edit_repo_perms_branch_delete', '/%(repo_name)s/settings/branch_permissions/%(rule_id)s/delete', ['repo_name', 'rule_id']);
382 385 }
@@ -1,572 +1,656 b''
1 1 // # Copyright (C) 2010-2019 RhodeCode GmbH
2 2 // #
3 3 // # This program is free software: you can redistribute it and/or modify
4 4 // # it under the terms of the GNU Affero General Public License, version 3
5 5 // # (only), as published by the Free Software Foundation.
6 6 // #
7 7 // # This program is distributed in the hope that it will be useful,
8 8 // # but WITHOUT ANY WARRANTY; without even the implied warranty of
9 9 // # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
10 10 // # GNU General Public License for more details.
11 11 // #
12 12 // # You should have received a copy of the GNU Affero General Public License
13 13 // # along with this program. If not, see <http://www.gnu.org/licenses/>.
14 14 // #
15 15 // # This program is dual-licensed. If you wish to learn more about the
16 16 // # RhodeCode Enterprise Edition, including its added features, Support services,
17 17 // # and proprietary license terms, please see https://rhodecode.com/licenses/
18 18
19 19 /**
20 20 RhodeCode JS Files
21 21 **/
22 22
23 23 if (typeof console == "undefined" || typeof console.log == "undefined"){
24 24 console = { log: function() {} }
25 25 }
26 26
27 27 // TODO: move the following function to submodules
28 28
29 29 /**
30 30 * show more
31 31 */
32 32 var show_more_event = function(){
33 33 $('table .show_more').click(function(e) {
34 34 var cid = e.target.id.substring(1);
35 35 var button = $(this);
36 36 if (button.hasClass('open')) {
37 37 $('#'+cid).hide();
38 38 button.removeClass('open');
39 39 } else {
40 40 $('#'+cid).show();
41 41 button.addClass('open one');
42 42 }
43 43 });
44 44 };
45 45
46 46 var compare_radio_buttons = function(repo_name, compare_ref_type){
47 47 $('#compare_action').on('click', function(e){
48 48 e.preventDefault();
49 49
50 50 var source = $('input[name=compare_source]:checked').val();
51 51 var target = $('input[name=compare_target]:checked').val();
52 52 if(source && target){
53 53 var url_data = {
54 54 repo_name: repo_name,
55 55 source_ref: source,
56 56 source_ref_type: compare_ref_type,
57 57 target_ref: target,
58 58 target_ref_type: compare_ref_type,
59 59 merge: 1
60 60 };
61 61 window.location = pyroutes.url('repo_compare', url_data);
62 62 }
63 63 });
64 64 $('.compare-radio-button').on('click', function(e){
65 65 var source = $('input[name=compare_source]:checked').val();
66 66 var target = $('input[name=compare_target]:checked').val();
67 67 if(source && target){
68 68 $('#compare_action').removeAttr("disabled");
69 69 $('#compare_action').removeClass("disabled");
70 70 }
71 71 })
72 72 };
73 73
74 74 var showRepoSize = function(target, repo_name, commit_id, callback) {
75 75 var container = $('#' + target);
76 76 var url = pyroutes.url('repo_stats',
77 77 {"repo_name": repo_name, "commit_id": commit_id});
78 78
79 79 container.show();
80 80 if (!container.hasClass('loaded')) {
81 81 $.ajax({url: url})
82 82 .complete(function (data) {
83 83 var responseJSON = data.responseJSON;
84 84 container.addClass('loaded');
85 85 container.html(responseJSON.size);
86 86 callback(responseJSON.code_stats)
87 87 })
88 88 .fail(function (data) {
89 89 console.log('failed to load repo stats');
90 90 });
91 91 }
92 92
93 93 };
94 94
95 95 var showRepoStats = function(target, data){
96 96 var container = $('#' + target);
97 97
98 98 if (container.hasClass('loaded')) {
99 99 return
100 100 }
101 101
102 102 var total = 0;
103 103 var no_data = true;
104 104 var tbl = document.createElement('table');
105 105 tbl.setAttribute('class', 'trending_language_tbl rctable');
106 106
107 107 $.each(data, function(key, val){
108 108 total += val.count;
109 109 });
110 110
111 111 var sortedStats = [];
112 112 for (var obj in data){
113 113 sortedStats.push([obj, data[obj]])
114 114 }
115 115 var sortedData = sortedStats.sort(function (a, b) {
116 116 return b[1].count - a[1].count
117 117 });
118 118 var cnt = 0;
119 119 $.each(sortedData, function(idx, val){
120 120 cnt += 1;
121 121 no_data = false;
122 122
123 123 var tr = document.createElement('tr');
124 124
125 125 var key = val[0];
126 126 var obj = {"desc": val[1].desc, "count": val[1].count};
127 127
128 128 // meta language names
129 129 var td1 = document.createElement('td');
130 130 var trending_language_label = document.createElement('div');
131 131 trending_language_label.innerHTML = obj.desc;
132 132 td1.appendChild(trending_language_label);
133 133
134 134 // extensions
135 135 var td2 = document.createElement('td');
136 136 var extension = document.createElement('div');
137 137 extension.innerHTML = ".{0}".format(key)
138 138 td2.appendChild(extension);
139 139
140 140 // number of files
141 141 var td3 = document.createElement('td');
142 142 var file_count = document.createElement('div');
143 143 var percentage_num = Math.round((obj.count / total * 100), 2);
144 144 var label = _ngettext('file', 'files', obj.count);
145 145 file_count.innerHTML = "{0} {1} ({2}%)".format(obj.count, label, percentage_num) ;
146 146 td3.appendChild(file_count);
147 147
148 148 // percentage
149 149 var td4 = document.createElement('td');
150 150 td4.setAttribute("class", 'trending_language');
151 151
152 152 var percentage = document.createElement('div');
153 153 percentage.setAttribute('class', 'lang-bar');
154 154 percentage.innerHTML = "&nbsp;";
155 155 percentage.style.width = percentage_num + '%';
156 156 td4.appendChild(percentage);
157 157
158 158 tr.appendChild(td1);
159 159 tr.appendChild(td2);
160 160 tr.appendChild(td3);
161 161 tr.appendChild(td4);
162 162 tbl.appendChild(tr);
163 163
164 164 });
165 165
166 166 $(container).html(tbl);
167 167 $(container).addClass('loaded');
168 168
169 169 $('#code_stats_show_more').on('click', function (e) {
170 170 e.preventDefault();
171 171 $('.stats_hidden').each(function (idx) {
172 172 $(this).css("display", "");
173 173 });
174 174 $('#code_stats_show_more').hide();
175 175 });
176 176
177 177 };
178 178
179 179 // returns a node from given html;
180 180 var fromHTML = function(html){
181 181 var _html = document.createElement('element');
182 182 _html.innerHTML = html;
183 183 return _html;
184 184 };
185 185
186 186 // Toggle Collapsable Content
187 187 function collapsableContent() {
188 188
189 189 $('.collapsable-content').not('.no-hide').hide();
190 190
191 191 $('.btn-collapse').unbind(); //in case we've been here before
192 192 $('.btn-collapse').click(function() {
193 193 var button = $(this);
194 194 var togglename = $(this).data("toggle");
195 195 $('.collapsable-content[data-toggle='+togglename+']').toggle();
196 196 if ($(this).html()=="Show Less")
197 197 $(this).html("Show More");
198 198 else
199 199 $(this).html("Show Less");
200 200 });
201 201 };
202 202
203 203 var timeagoActivate = function() {
204 204 $("time.timeago").timeago();
205 205 };
206 206
207 207
208 208 var clipboardActivate = function() {
209 209 /*
210 210 *
211 211 * <i class="tooltip icon-plus clipboard-action" data-clipboard-text="${commit.raw_id}" title="${_('Copy the full commit id')}"></i>
212 212 * */
213 213 var clipboard = new ClipboardJS('.clipboard-action');
214 214
215 215 clipboard.on('success', function(e) {
216 216 var callback = function () {
217 217 $(e.trigger).animate({'opacity': 1.00}, 200)
218 218 };
219 219 $(e.trigger).animate({'opacity': 0.15}, 200, callback);
220 220 e.clearSelection();
221 221 });
222 222 };
223 223
224 var tooltipActivate = function () {
225 var delay = 50;
226 var animation = 'fade';
227 var theme = 'tooltipster-shadow';
228 var debug = false;
229
230 $('.tooltip').tooltipster({
231 debug: debug,
232 theme: theme,
233 animation: animation,
234 delay: delay,
235 contentCloning: true,
236 contentAsHTML: true,
237
238 functionBefore: function (instance, helper) {
239 var $origin = $(helper.origin);
240 var data = '<div style="white-space: pre-wrap">{0}</div>'.format(instance.content());
241 instance.content(data);
242 }
243 });
244 var hovercardCache = {};
245
246 var loadHoverCard = function (url, callback) {
247 var id = url;
248
249 if (hovercardCache[id] !== undefined) {
250 callback(hovercardCache[id]);
251 return;
252 }
253
254 hovercardCache[id] = undefined;
255 $.get(url, function (data) {
256 hovercardCache[id] = data;
257 callback(hovercardCache[id]);
258 }).fail(function (data, textStatus, errorThrown) {
259 var msg = "Error while fetching hovercard.\nError code {0} ({1}).".format(data.status,data.statusText);
260 callback(msg);
261 });
262 };
263
264 $('.tooltip-hovercard').tooltipster({
265 debug: debug,
266 theme: theme,
267 animation: animation,
268 delay: delay,
269 interactive: true,
270 contentCloning: true,
271
272 trigger: 'custom',
273 triggerOpen: {
274 mouseenter: true,
275 },
276 triggerClose: {
277 mouseleave: true,
278 originClick: true,
279 touchleave: true
280 },
281 content: _gettext('Loading...'),
282 contentAsHTML: true,
283 updateAnimation: null,
284
285 functionBefore: function (instance, helper) {
286
287 var $origin = $(helper.origin);
288
289 // we set a variable so the data is only loaded once via Ajax, not every time the tooltip opens
290 if ($origin.data('loaded') !== true) {
291 var hovercardUrl = $origin.data('hovercardUrl');
292
293 if (hovercardUrl !== undefined && hovercardUrl !== "") {
294 loadHoverCard(hovercardUrl, function (data) {
295 instance.content(data);
296 })
297 } else {
298 var data = '<div style="white-space: pre-wrap">{0}</div>'.format($origin.data('hovercardAlt'))
299 instance.content(data);
300 }
301
302 // to remember that the data has been loaded
303 $origin.data('loaded', true);
304 }
305 }
306 })
307 };
224 308
225 309 // Formatting values in a Select2 dropdown of commit references
226 310 var formatSelect2SelectionRefs = function(commit_ref){
227 311 var tmpl = '';
228 312 if (!commit_ref.text || commit_ref.type === 'sha'){
229 313 return commit_ref.text;
230 314 }
231 315 if (commit_ref.type === 'branch'){
232 316 tmpl = tmpl.concat('<i class="icon-branch"></i> ');
233 317 } else if (commit_ref.type === 'tag'){
234 318 tmpl = tmpl.concat('<i class="icon-tag"></i> ');
235 319 } else if (commit_ref.type === 'book'){
236 320 tmpl = tmpl.concat('<i class="icon-bookmark"></i> ');
237 321 }
238 322 return tmpl.concat(escapeHtml(commit_ref.text));
239 323 };
240 324
241 325 // takes a given html element and scrolls it down offset pixels
242 326 function offsetScroll(element, offset) {
243 327 setTimeout(function() {
244 328 var location = element.offset().top;
245 329 // some browsers use body, some use html
246 330 $('html, body').animate({ scrollTop: (location - offset) });
247 331 }, 100);
248 332 }
249 333
250 334 // scroll an element `percent`% from the top of page in `time` ms
251 335 function scrollToElement(element, percent, time) {
252 336 percent = (percent === undefined ? 25 : percent);
253 337 time = (time === undefined ? 100 : time);
254 338
255 339 var $element = $(element);
256 340 if ($element.length == 0) {
257 341 throw('Cannot scroll to {0}'.format(element))
258 342 }
259 343 var elOffset = $element.offset().top;
260 344 var elHeight = $element.height();
261 345 var windowHeight = $(window).height();
262 346 var offset = elOffset;
263 347 if (elHeight < windowHeight) {
264 348 offset = elOffset - ((windowHeight / (100 / percent)) - (elHeight / 2));
265 349 }
266 350 setTimeout(function() {
267 351 $('html, body').animate({ scrollTop: offset});
268 352 }, time);
269 353 }
270 354
271 355 /**
272 356 * global hooks after DOM is loaded
273 357 */
274 358 $(document).ready(function() {
275 359 firefoxAnchorFix();
276 360
277 361 $('.navigation a.menulink').on('click', function(e){
278 362 var menuitem = $(this).parent('li');
279 363 if (menuitem.hasClass('open')) {
280 364 menuitem.removeClass('open');
281 365 } else {
282 366 menuitem.addClass('open');
283 367 $(document).on('click', function(event) {
284 368 if (!$(event.target).closest(menuitem).length) {
285 369 menuitem.removeClass('open');
286 370 }
287 371 });
288 372 }
289 373 });
290 374
291 375 $('body').on('click', '.cb-lineno a', function(event) {
292 376 function sortNumber(a,b) {
293 377 return a - b;
294 378 }
295 379
296 380 var lineNo = $(this).data('lineNo');
297 381 var lineName = $(this).attr('name');
298 382
299 383 if (lineNo) {
300 384 var prevLine = $('.cb-line-selected a').data('lineNo');
301 385
302 386 // on shift, we do a range selection, if we got previous line
303 387 if (event.shiftKey && prevLine !== undefined) {
304 388 var prevLine = parseInt(prevLine);
305 389 var nextLine = parseInt(lineNo);
306 390 var pos = [prevLine, nextLine].sort(sortNumber);
307 391 var anchor = '#L{0}-{1}'.format(pos[0], pos[1]);
308 392
309 393 // single click
310 394 } else {
311 395 var nextLine = parseInt(lineNo);
312 396 var pos = [nextLine, nextLine];
313 397 var anchor = '#L{0}'.format(pos[0]);
314 398
315 399 }
316 400 // highlight
317 401 var range = [];
318 402 for (var i = pos[0]; i <= pos[1]; i++) {
319 403 range.push(i);
320 404 }
321 405 // clear old selected lines
322 406 $('.cb-line-selected').removeClass('cb-line-selected');
323 407
324 408 $.each(range, function (i, lineNo) {
325 409 var line_td = $('td.cb-lineno#L' + lineNo);
326 410
327 411 if (line_td.length) {
328 412 line_td.addClass('cb-line-selected'); // line number td
329 413 line_td.prev().addClass('cb-line-selected'); // line data
330 414 line_td.next().addClass('cb-line-selected'); // line content
331 415 }
332 416 });
333 417
334 418 } else if (lineName !== undefined) { // lineName only occurs in diffs
335 419 // clear old selected lines
336 420 $('td.cb-line-selected').removeClass('cb-line-selected');
337 421 var anchor = '#{0}'.format(lineName);
338 422 var diffmode = templateContext.session_attrs.diffmode || "sideside";
339 423
340 424 if (diffmode === "unified") {
341 425 $(this).closest('tr').find('td').addClass('cb-line-selected');
342 426 } else {
343 427 var activeTd = $(this).closest('td');
344 428 activeTd.addClass('cb-line-selected');
345 429 activeTd.next('td').addClass('cb-line-selected');
346 430 }
347 431
348 432 }
349 433
350 434 // Replace URL without jumping to it if browser supports.
351 435 // Default otherwise
352 436 if (history.pushState && anchor !== undefined) {
353 437 var new_location = location.href.rstrip('#');
354 438 if (location.hash) {
355 439 // location without hash
356 440 new_location = new_location.replace(location.hash, "");
357 441 }
358 442
359 443 // Make new anchor url
360 444 new_location = new_location + anchor;
361 445 history.pushState(true, document.title, new_location);
362 446
363 447 return false;
364 448 }
365 449
366 450 });
367 451
368 452 $('.collapse_file').on('click', function(e) {
369 453 e.stopPropagation();
370 454 if ($(e.target).is('a')) { return; }
371 455 var node = $(e.delegateTarget).first();
372 456 var icon = $($(node.children().first()).children().first());
373 457 var id = node.attr('fid');
374 458 var target = $('#'+id);
375 459 var tr = $('#tr_'+id);
376 460 var diff = $('#diff_'+id);
377 461 if(node.hasClass('expand_file')){
378 462 node.removeClass('expand_file');
379 463 icon.removeClass('expand_file_icon');
380 464 node.addClass('collapse_file');
381 465 icon.addClass('collapse_file_icon');
382 466 diff.show();
383 467 tr.show();
384 468 target.show();
385 469 } else {
386 470 node.removeClass('collapse_file');
387 471 icon.removeClass('collapse_file_icon');
388 472 node.addClass('expand_file');
389 473 icon.addClass('expand_file_icon');
390 474 diff.hide();
391 475 tr.hide();
392 476 target.hide();
393 477 }
394 478 });
395 479
396 480 $('#expand_all_files').click(function() {
397 481 $('.expand_file').each(function() {
398 482 var node = $(this);
399 483 var icon = $($(node.children().first()).children().first());
400 484 var id = $(this).attr('fid');
401 485 var target = $('#'+id);
402 486 var tr = $('#tr_'+id);
403 487 var diff = $('#diff_'+id);
404 488 node.removeClass('expand_file');
405 489 icon.removeClass('expand_file_icon');
406 490 node.addClass('collapse_file');
407 491 icon.addClass('collapse_file_icon');
408 492 diff.show();
409 493 tr.show();
410 494 target.show();
411 495 });
412 496 });
413 497
414 498 $('#collapse_all_files').click(function() {
415 499 $('.collapse_file').each(function() {
416 500 var node = $(this);
417 501 var icon = $($(node.children().first()).children().first());
418 502 var id = $(this).attr('fid');
419 503 var target = $('#'+id);
420 504 var tr = $('#tr_'+id);
421 505 var diff = $('#diff_'+id);
422 506 node.removeClass('collapse_file');
423 507 icon.removeClass('collapse_file_icon');
424 508 node.addClass('expand_file');
425 509 icon.addClass('expand_file_icon');
426 510 diff.hide();
427 511 tr.hide();
428 512 target.hide();
429 513 });
430 514 });
431 515
432 516 // Mouse over behavior for comments and line selection
433 517
434 518 // Select the line that comes from the url anchor
435 519 // At the time of development, Chrome didn't seem to support jquery's :target
436 520 // element, so I had to scroll manually
437 521
438 522 if (location.hash) {
439 523 var result = splitDelimitedHash(location.hash);
440 524 var loc = result.loc;
441 525 if (loc.length > 1) {
442 526
443 527 var highlightable_line_tds = [];
444 528
445 529 // source code line format
446 530 var page_highlights = loc.substring(
447 531 loc.indexOf('#') + 1).split('L');
448 532
449 533 if (page_highlights.length > 1) {
450 534 var highlight_ranges = page_highlights[1].split(",");
451 535 var h_lines = [];
452 536 for (var pos in highlight_ranges) {
453 537 var _range = highlight_ranges[pos].split('-');
454 538 if (_range.length === 2) {
455 539 var start = parseInt(_range[0]);
456 540 var end = parseInt(_range[1]);
457 541 if (start < end) {
458 542 for (var i = start; i <= end; i++) {
459 543 h_lines.push(i);
460 544 }
461 545 }
462 546 }
463 547 else {
464 548 h_lines.push(parseInt(highlight_ranges[pos]));
465 549 }
466 550 }
467 551 for (pos in h_lines) {
468 552 var line_td = $('td.cb-lineno#L' + h_lines[pos]);
469 553 if (line_td.length) {
470 554 highlightable_line_tds.push(line_td);
471 555 }
472 556 }
473 557 }
474 558
475 559 // now check a direct id reference (diff page)
476 560 if ($(loc).length && $(loc).hasClass('cb-lineno')) {
477 561 highlightable_line_tds.push($(loc));
478 562 }
479 563 $.each(highlightable_line_tds, function (i, $td) {
480 564 $td.addClass('cb-line-selected'); // line number td
481 565 $td.prev().addClass('cb-line-selected'); // line data
482 566 $td.next().addClass('cb-line-selected'); // line content
483 567 });
484 568
485 569 if (highlightable_line_tds.length) {
486 570 var $first_line_td = highlightable_line_tds[0];
487 571 scrollToElement($first_line_td);
488 572 $.Topic('/ui/plugins/code/anchor_focus').prepareOrPublish({
489 573 td: $first_line_td,
490 574 remainder: result.remainder
491 575 });
492 576 }
493 577 }
494 578 }
495 579 collapsableContent();
496 580 });
497 581
498 582 var feedLifetimeOptions = function(query, initialData){
499 583 var data = {results: []};
500 584 var isQuery = typeof query.term !== 'undefined';
501 585
502 586 var section = _gettext('Lifetime');
503 587 var children = [];
504 588
505 589 //filter results
506 590 $.each(initialData.results, function(idx, value) {
507 591
508 592 if (!isQuery || query.term.length === 0 || value.text.toUpperCase().indexOf(query.term.toUpperCase()) >= 0) {
509 593 children.push({
510 594 'id': this.id,
511 595 'text': this.text
512 596 })
513 597 }
514 598
515 599 });
516 600 data.results.push({
517 601 'text': section,
518 602 'children': children
519 603 });
520 604
521 605 if (isQuery) {
522 606
523 607 var now = moment.utc();
524 608
525 609 var parseQuery = function(entry, now){
526 610 var fmt = 'DD/MM/YYYY H:mm';
527 611 var parsed = moment.utc(entry, fmt);
528 612 var diffInMin = parsed.diff(now, 'minutes');
529 613
530 614 if (diffInMin > 0){
531 615 return {
532 616 id: diffInMin,
533 617 text: parsed.format(fmt)
534 618 }
535 619 } else {
536 620 return {
537 621 id: undefined,
538 622 text: parsed.format('DD/MM/YYYY') + ' ' + _gettext('date not in future')
539 623 }
540 624 }
541 625
542 626
543 627 };
544 628
545 629 data.results.push({
546 630 'text': _gettext('Specified expiration date'),
547 631 'children': [{
548 632 'id': parseQuery(query.term, now).id,
549 633 'text': parseQuery(query.term, now).text
550 634 }]
551 635 });
552 636 }
553 637
554 638 query.callback(data);
555 639 };
556 640
557 641
558 642 var storeUserSessionAttr = function (key, val) {
559 643
560 644 var postData = {
561 645 'key': key,
562 646 'val': val,
563 647 'csrf_token': CSRF_TOKEN
564 648 };
565 649
566 650 var success = function(o) {
567 651 return true
568 652 };
569 653
570 654 ajaxPOST(pyroutes.url('store_user_session_value'), postData, success);
571 655 return false;
572 656 };
@@ -1,927 +1,929 b''
1 1 // # Copyright (C) 2010-2019 RhodeCode GmbH
2 2 // #
3 3 // # This program is free software: you can redistribute it and/or modify
4 4 // # it under the terms of the GNU Affero General Public License, version 3
5 5 // # (only), as published by the Free Software Foundation.
6 6 // #
7 7 // # This program is distributed in the hope that it will be useful,
8 8 // # but WITHOUT ANY WARRANTY; without even the implied warranty of
9 9 // # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
10 10 // # GNU General Public License for more details.
11 11 // #
12 12 // # You should have received a copy of the GNU Affero General Public License
13 13 // # along with this program. If not, see <http://www.gnu.org/licenses/>.
14 14 // #
15 15 // # This program is dual-licensed. If you wish to learn more about the
16 16 // # RhodeCode Enterprise Edition, including its added features, Support services,
17 17 // # and proprietary license terms, please see https://rhodecode.com/licenses/
18 18
19 19 var firefoxAnchorFix = function() {
20 20 // hack to make anchor links behave properly on firefox, in our inline
21 21 // comments generation when comments are injected firefox is misbehaving
22 22 // when jumping to anchor links
23 23 if (location.href.indexOf('#') > -1) {
24 24 location.href += '';
25 25 }
26 26 };
27 27
28 28 var linkifyComments = function(comments) {
29 29 var firstCommentId = null;
30 30 if (comments) {
31 31 firstCommentId = $(comments[0]).data('comment-id');
32 32 }
33 33
34 34 if (firstCommentId){
35 35 $('#inline-comments-counter').attr('href', '#comment-' + firstCommentId);
36 36 }
37 37 };
38 38
39 39 var bindToggleButtons = function() {
40 40 $('.comment-toggle').on('click', function() {
41 41 $(this).parent().nextUntil('tr.line').toggle('inline-comments');
42 42 });
43 43 };
44 44
45 45
46 46
47 47 var _submitAjaxPOST = function(url, postData, successHandler, failHandler) {
48 48 failHandler = failHandler || function() {};
49 49 postData = toQueryString(postData);
50 50 var request = $.ajax({
51 51 url: url,
52 52 type: 'POST',
53 53 data: postData,
54 54 headers: {'X-PARTIAL-XHR': true}
55 55 })
56 56 .done(function (data) {
57 57 successHandler(data);
58 58 })
59 59 .fail(function (data, textStatus, errorThrown) {
60 60 failHandler(data, textStatus, errorThrown)
61 61 });
62 62 return request;
63 63 };
64 64
65 65
66 66
67 67
68 68 /* Comment form for main and inline comments */
69 69 (function(mod) {
70 70
71 71 if (typeof exports == "object" && typeof module == "object") {
72 72 // CommonJS
73 73 module.exports = mod();
74 74 }
75 75 else {
76 76 // Plain browser env
77 77 (this || window).CommentForm = mod();
78 78 }
79 79
80 80 })(function() {
81 81 "use strict";
82 82
83 83 function CommentForm(formElement, commitId, pullRequestId, lineNo, initAutocompleteActions, resolvesCommentId) {
84 84 if (!(this instanceof CommentForm)) {
85 85 return new CommentForm(formElement, commitId, pullRequestId, lineNo, initAutocompleteActions, resolvesCommentId);
86 86 }
87 87
88 88 // bind the element instance to our Form
89 89 $(formElement).get(0).CommentForm = this;
90 90
91 91 this.withLineNo = function(selector) {
92 92 var lineNo = this.lineNo;
93 93 if (lineNo === undefined) {
94 94 return selector
95 95 } else {
96 96 return selector + '_' + lineNo;
97 97 }
98 98 };
99 99
100 100 this.commitId = commitId;
101 101 this.pullRequestId = pullRequestId;
102 102 this.lineNo = lineNo;
103 103 this.initAutocompleteActions = initAutocompleteActions;
104 104
105 105 this.previewButton = this.withLineNo('#preview-btn');
106 106 this.previewContainer = this.withLineNo('#preview-container');
107 107
108 108 this.previewBoxSelector = this.withLineNo('#preview-box');
109 109
110 110 this.editButton = this.withLineNo('#edit-btn');
111 111 this.editContainer = this.withLineNo('#edit-container');
112 112 this.cancelButton = this.withLineNo('#cancel-btn');
113 113 this.commentType = this.withLineNo('#comment_type');
114 114
115 115 this.resolvesId = null;
116 116 this.resolvesActionId = null;
117 117
118 118 this.closesPr = '#close_pull_request';
119 119
120 120 this.cmBox = this.withLineNo('#text');
121 121 this.cm = initCommentBoxCodeMirror(this, this.cmBox, this.initAutocompleteActions);
122 122
123 123 this.statusChange = this.withLineNo('#change_status');
124 124
125 125 this.submitForm = formElement;
126 126 this.submitButton = $(this.submitForm).find('input[type="submit"]');
127 127 this.submitButtonText = this.submitButton.val();
128 128
129 129 this.previewUrl = pyroutes.url('repo_commit_comment_preview',
130 130 {'repo_name': templateContext.repo_name,
131 131 'commit_id': templateContext.commit_data.commit_id});
132 132
133 133 if (resolvesCommentId){
134 134 this.resolvesId = '#resolve_comment_{0}'.format(resolvesCommentId);
135 135 this.resolvesActionId = '#resolve_comment_action_{0}'.format(resolvesCommentId);
136 136 $(this.commentType).prop('disabled', true);
137 137 $(this.commentType).addClass('disabled');
138 138
139 139 // disable select
140 140 setTimeout(function() {
141 141 $(self.statusChange).select2('readonly', true);
142 142 }, 10);
143 143
144 144 var resolvedInfo = (
145 145 '<li class="resolve-action">' +
146 146 '<input type="hidden" id="resolve_comment_{0}" name="resolve_comment_{0}" value="{0}">' +
147 147 '<button id="resolve_comment_action_{0}" class="resolve-text btn btn-sm" onclick="return Rhodecode.comments.submitResolution({0})">{1} #{0}</button>' +
148 148 '</li>'
149 149 ).format(resolvesCommentId, _gettext('resolve comment'));
150 150 $(resolvedInfo).insertAfter($(this.commentType).parent());
151 151 }
152 152
153 153 // based on commitId, or pullRequestId decide where do we submit
154 154 // out data
155 155 if (this.commitId){
156 156 this.submitUrl = pyroutes.url('repo_commit_comment_create',
157 157 {'repo_name': templateContext.repo_name,
158 158 'commit_id': this.commitId});
159 159 this.selfUrl = pyroutes.url('repo_commit',
160 160 {'repo_name': templateContext.repo_name,
161 161 'commit_id': this.commitId});
162 162
163 163 } else if (this.pullRequestId) {
164 164 this.submitUrl = pyroutes.url('pullrequest_comment_create',
165 165 {'repo_name': templateContext.repo_name,
166 166 'pull_request_id': this.pullRequestId});
167 167 this.selfUrl = pyroutes.url('pullrequest_show',
168 168 {'repo_name': templateContext.repo_name,
169 169 'pull_request_id': this.pullRequestId});
170 170
171 171 } else {
172 172 throw new Error(
173 173 'CommentForm requires pullRequestId, or commitId to be specified.')
174 174 }
175 175
176 176 // FUNCTIONS and helpers
177 177 var self = this;
178 178
179 179 this.isInline = function(){
180 180 return this.lineNo && this.lineNo != 'general';
181 181 };
182 182
183 183 this.getCmInstance = function(){
184 184 return this.cm
185 185 };
186 186
187 187 this.setPlaceholder = function(placeholder) {
188 188 var cm = this.getCmInstance();
189 189 if (cm){
190 190 cm.setOption('placeholder', placeholder);
191 191 }
192 192 };
193 193
194 194 this.getCommentStatus = function() {
195 195 return $(this.submitForm).find(this.statusChange).val();
196 196 };
197 197 this.getCommentType = function() {
198 198 return $(this.submitForm).find(this.commentType).val();
199 199 };
200 200
201 201 this.getResolvesId = function() {
202 202 return $(this.submitForm).find(this.resolvesId).val() || null;
203 203 };
204 204
205 205 this.getClosePr = function() {
206 206 return $(this.submitForm).find(this.closesPr).val() || null;
207 207 };
208 208
209 209 this.markCommentResolved = function(resolvedCommentId){
210 210 $('#comment-label-{0}'.format(resolvedCommentId)).find('.resolved').show();
211 211 $('#comment-label-{0}'.format(resolvedCommentId)).find('.resolve').hide();
212 212 };
213 213
214 214 this.isAllowedToSubmit = function() {
215 215 return !$(this.submitButton).prop('disabled');
216 216 };
217 217
218 218 this.initStatusChangeSelector = function(){
219 219 var formatChangeStatus = function(state, escapeMarkup) {
220 220 var originalOption = state.element;
221 221 var tmpl = '<i class="icon-circle review-status-{0}"></i><span>{1}</span>'.format($(originalOption).data('status'), escapeMarkup(state.text));
222 222 return tmpl
223 223 };
224 224 var formatResult = function(result, container, query, escapeMarkup) {
225 225 return formatChangeStatus(result, escapeMarkup);
226 226 };
227 227
228 228 var formatSelection = function(data, container, escapeMarkup) {
229 229 return formatChangeStatus(data, escapeMarkup);
230 230 };
231 231
232 232 $(this.submitForm).find(this.statusChange).select2({
233 233 placeholder: _gettext('Status Review'),
234 234 formatResult: formatResult,
235 235 formatSelection: formatSelection,
236 236 containerCssClass: "drop-menu status_box_menu",
237 237 dropdownCssClass: "drop-menu-dropdown",
238 238 dropdownAutoWidth: true,
239 239 minimumResultsForSearch: -1
240 240 });
241 241 $(this.submitForm).find(this.statusChange).on('change', function() {
242 242 var status = self.getCommentStatus();
243 243
244 244 if (status && !self.isInline()) {
245 245 $(self.submitButton).prop('disabled', false);
246 246 }
247 247
248 248 var placeholderText = _gettext('Comment text will be set automatically based on currently selected status ({0}) ...').format(status);
249 249 self.setPlaceholder(placeholderText)
250 250 })
251 251 };
252 252
253 253 // reset the comment form into it's original state
254 254 this.resetCommentFormState = function(content) {
255 255 content = content || '';
256 256
257 257 $(this.editContainer).show();
258 258 $(this.editButton).parent().addClass('active');
259 259
260 260 $(this.previewContainer).hide();
261 261 $(this.previewButton).parent().removeClass('active');
262 262
263 263 this.setActionButtonsDisabled(true);
264 264 self.cm.setValue(content);
265 265 self.cm.setOption("readOnly", false);
266 266
267 267 if (this.resolvesId) {
268 268 // destroy the resolve action
269 269 $(this.resolvesId).parent().remove();
270 270 }
271 271 // reset closingPR flag
272 272 $('.close-pr-input').remove();
273 273
274 274 $(this.statusChange).select2('readonly', false);
275 275 };
276 276
277 277 this.globalSubmitSuccessCallback = function(){
278 278 // default behaviour is to call GLOBAL hook, if it's registered.
279 279 if (window.commentFormGlobalSubmitSuccessCallback !== undefined){
280 280 commentFormGlobalSubmitSuccessCallback()
281 281 }
282 282 };
283 283
284 284 this.submitAjaxPOST = function(url, postData, successHandler, failHandler) {
285 285 return _submitAjaxPOST(url, postData, successHandler, failHandler);
286 286 };
287 287
288 288 // overwrite a submitHandler, we need to do it for inline comments
289 289 this.setHandleFormSubmit = function(callback) {
290 290 this.handleFormSubmit = callback;
291 291 };
292 292
293 293 // overwrite a submitSuccessHandler
294 294 this.setGlobalSubmitSuccessCallback = function(callback) {
295 295 this.globalSubmitSuccessCallback = callback;
296 296 };
297 297
298 298 // default handler for for submit for main comments
299 299 this.handleFormSubmit = function() {
300 300 var text = self.cm.getValue();
301 301 var status = self.getCommentStatus();
302 302 var commentType = self.getCommentType();
303 303 var resolvesCommentId = self.getResolvesId();
304 304 var closePullRequest = self.getClosePr();
305 305
306 306 if (text === "" && !status) {
307 307 return;
308 308 }
309 309
310 310 var excludeCancelBtn = false;
311 311 var submitEvent = true;
312 312 self.setActionButtonsDisabled(true, excludeCancelBtn, submitEvent);
313 313 self.cm.setOption("readOnly", true);
314 314
315 315 var postData = {
316 316 'text': text,
317 317 'changeset_status': status,
318 318 'comment_type': commentType,
319 319 'csrf_token': CSRF_TOKEN
320 320 };
321 321
322 322 if (resolvesCommentId) {
323 323 postData['resolves_comment_id'] = resolvesCommentId;
324 324 }
325 325
326 326 if (closePullRequest) {
327 327 postData['close_pull_request'] = true;
328 328 }
329 329
330 330 var submitSuccessCallback = function(o) {
331 331 // reload page if we change status for single commit.
332 332 if (status && self.commitId) {
333 333 location.reload(true);
334 334 } else {
335 335 $('#injected_page_comments').append(o.rendered_text);
336 336 self.resetCommentFormState();
337 337 timeagoActivate();
338 tooltipActivate();
338 339
339 340 // mark visually which comment was resolved
340 341 if (resolvesCommentId) {
341 342 self.markCommentResolved(resolvesCommentId);
342 343 }
343 344 }
344 345
345 346 // run global callback on submit
346 347 self.globalSubmitSuccessCallback();
347 348
348 349 };
349 350 var submitFailCallback = function(data) {
350 351 alert(
351 352 "Error while submitting comment.\n" +
352 353 "Error code {0} ({1}).".format(data.status, data.statusText)
353 354 );
354 355 self.resetCommentFormState(text);
355 356 };
356 357 self.submitAjaxPOST(
357 358 self.submitUrl, postData, submitSuccessCallback, submitFailCallback);
358 359 };
359 360
360 361 this.previewSuccessCallback = function(o) {
361 362 $(self.previewBoxSelector).html(o);
362 363 $(self.previewBoxSelector).removeClass('unloaded');
363 364
364 365 // swap buttons, making preview active
365 366 $(self.previewButton).parent().addClass('active');
366 367 $(self.editButton).parent().removeClass('active');
367 368
368 369 // unlock buttons
369 370 self.setActionButtonsDisabled(false);
370 371 };
371 372
372 373 this.setActionButtonsDisabled = function(state, excludeCancelBtn, submitEvent) {
373 374 excludeCancelBtn = excludeCancelBtn || false;
374 375 submitEvent = submitEvent || false;
375 376
376 377 $(this.editButton).prop('disabled', state);
377 378 $(this.previewButton).prop('disabled', state);
378 379
379 380 if (!excludeCancelBtn) {
380 381 $(this.cancelButton).prop('disabled', state);
381 382 }
382 383
383 384 var submitState = state;
384 385 if (!submitEvent && this.getCommentStatus() && !self.isInline()) {
385 386 // if the value of commit review status is set, we allow
386 387 // submit button, but only on Main form, isInline means inline
387 388 submitState = false
388 389 }
389 390
390 391 $(this.submitButton).prop('disabled', submitState);
391 392 if (submitEvent) {
392 393 $(this.submitButton).val(_gettext('Submitting...'));
393 394 } else {
394 395 $(this.submitButton).val(this.submitButtonText);
395 396 }
396 397
397 398 };
398 399
399 400 // lock preview/edit/submit buttons on load, but exclude cancel button
400 401 var excludeCancelBtn = true;
401 402 this.setActionButtonsDisabled(true, excludeCancelBtn);
402 403
403 404 // anonymous users don't have access to initialized CM instance
404 405 if (this.cm !== undefined){
405 406 this.cm.on('change', function(cMirror) {
406 407 if (cMirror.getValue() === "") {
407 408 self.setActionButtonsDisabled(true, excludeCancelBtn)
408 409 } else {
409 410 self.setActionButtonsDisabled(false, excludeCancelBtn)
410 411 }
411 412 });
412 413 }
413 414
414 415 $(this.editButton).on('click', function(e) {
415 416 e.preventDefault();
416 417
417 418 $(self.previewButton).parent().removeClass('active');
418 419 $(self.previewContainer).hide();
419 420
420 421 $(self.editButton).parent().addClass('active');
421 422 $(self.editContainer).show();
422 423
423 424 });
424 425
425 426 $(this.previewButton).on('click', function(e) {
426 427 e.preventDefault();
427 428 var text = self.cm.getValue();
428 429
429 430 if (text === "") {
430 431 return;
431 432 }
432 433
433 434 var postData = {
434 435 'text': text,
435 436 'renderer': templateContext.visual.default_renderer,
436 437 'csrf_token': CSRF_TOKEN
437 438 };
438 439
439 440 // lock ALL buttons on preview
440 441 self.setActionButtonsDisabled(true);
441 442
442 443 $(self.previewBoxSelector).addClass('unloaded');
443 444 $(self.previewBoxSelector).html(_gettext('Loading ...'));
444 445
445 446 $(self.editContainer).hide();
446 447 $(self.previewContainer).show();
447 448
448 449 // by default we reset state of comment preserving the text
449 450 var previewFailCallback = function(data){
450 451 alert(
451 452 "Error while preview of comment.\n" +
452 453 "Error code {0} ({1}).".format(data.status, data.statusText)
453 454 );
454 455 self.resetCommentFormState(text)
455 456 };
456 457 self.submitAjaxPOST(
457 458 self.previewUrl, postData, self.previewSuccessCallback,
458 459 previewFailCallback);
459 460
460 461 $(self.previewButton).parent().addClass('active');
461 462 $(self.editButton).parent().removeClass('active');
462 463 });
463 464
464 465 $(this.submitForm).submit(function(e) {
465 466 e.preventDefault();
466 467 var allowedToSubmit = self.isAllowedToSubmit();
467 468 if (!allowedToSubmit){
468 469 return false;
469 470 }
470 471 self.handleFormSubmit();
471 472 });
472 473
473 474 }
474 475
475 476 return CommentForm;
476 477 });
477 478
478 479 /* comments controller */
479 480 var CommentsController = function() {
480 481 var mainComment = '#text';
481 482 var self = this;
482 483
483 484 this.cancelComment = function(node) {
484 485 var $node = $(node);
485 486 var $td = $node.closest('td');
486 487 $node.closest('.comment-inline-form').remove();
487 488 return false;
488 489 };
489 490
490 491 this.getLineNumber = function(node) {
491 492 var $node = $(node);
492 493 var lineNo = $node.closest('td').attr('data-line-no');
493 494 if (lineNo === undefined && $node.data('commentInline')){
494 495 lineNo = $node.data('commentLineNo')
495 496 }
496 497
497 498 return lineNo
498 499 };
499 500
500 501 this.scrollToComment = function(node, offset, outdated) {
501 502 if (offset === undefined) {
502 503 offset = 0;
503 504 }
504 505 var outdated = outdated || false;
505 506 var klass = outdated ? 'div.comment-outdated' : 'div.comment-current';
506 507
507 508 if (!node) {
508 509 node = $('.comment-selected');
509 510 if (!node.length) {
510 511 node = $('comment-current')
511 512 }
512 513 }
513 514 $wrapper = $(node).closest('div.comment');
514 515 $comment = $(node).closest(klass);
515 516 $comments = $(klass);
516 517
517 518 // show hidden comment when referenced.
518 519 if (!$wrapper.is(':visible')){
519 520 $wrapper.show();
520 521 }
521 522
522 523 $('.comment-selected').removeClass('comment-selected');
523 524
524 525 var nextIdx = $(klass).index($comment) + offset;
525 526 if (nextIdx >= $comments.length) {
526 527 nextIdx = 0;
527 528 }
528 529 var $next = $(klass).eq(nextIdx);
529 530
530 531 var $cb = $next.closest('.cb');
531 532 $cb.removeClass('cb-collapsed');
532 533
533 534 var $filediffCollapseState = $cb.closest('.filediff').prev();
534 535 $filediffCollapseState.prop('checked', false);
535 536 $next.addClass('comment-selected');
536 537 scrollToElement($next);
537 538 return false;
538 539 };
539 540
540 541 this.nextComment = function(node) {
541 542 return self.scrollToComment(node, 1);
542 543 };
543 544
544 545 this.prevComment = function(node) {
545 546 return self.scrollToComment(node, -1);
546 547 };
547 548
548 549 this.nextOutdatedComment = function(node) {
549 550 return self.scrollToComment(node, 1, true);
550 551 };
551 552
552 553 this.prevOutdatedComment = function(node) {
553 554 return self.scrollToComment(node, -1, true);
554 555 };
555 556
556 557 this.deleteComment = function(node) {
557 558 if (!confirm(_gettext('Delete this comment?'))) {
558 559 return false;
559 560 }
560 561 var $node = $(node);
561 562 var $td = $node.closest('td');
562 563 var $comment = $node.closest('.comment');
563 564 var comment_id = $comment.attr('data-comment-id');
564 565 var url = AJAX_COMMENT_DELETE_URL.replace('__COMMENT_ID__', comment_id);
565 566 var postData = {
566 567 'csrf_token': CSRF_TOKEN
567 568 };
568 569
569 570 $comment.addClass('comment-deleting');
570 571 $comment.hide('fast');
571 572
572 573 var success = function(response) {
573 574 $comment.remove();
574 575 return false;
575 576 };
576 577 var failure = function(data, textStatus, xhr) {
577 578 alert("error processing request: " + textStatus);
578 579 $comment.show('fast');
579 580 $comment.removeClass('comment-deleting');
580 581 return false;
581 582 };
582 583 ajaxPOST(url, postData, success, failure);
583 584 };
584 585
585 586 this.toggleWideMode = function (node) {
586 587 if ($('#content').hasClass('wrapper')) {
587 588 $('#content').removeClass("wrapper");
588 589 $('#content').addClass("wide-mode-wrapper");
589 590 $(node).addClass('btn-success');
590 591 return true
591 592 } else {
592 593 $('#content').removeClass("wide-mode-wrapper");
593 594 $('#content').addClass("wrapper");
594 595 $(node).removeClass('btn-success');
595 596 return false
596 597 }
597 598
598 599 };
599 600
600 601 this.toggleComments = function(node, show) {
601 602 var $filediff = $(node).closest('.filediff');
602 603 if (show === true) {
603 604 $filediff.removeClass('hide-comments');
604 605 } else if (show === false) {
605 606 $filediff.find('.hide-line-comments').removeClass('hide-line-comments');
606 607 $filediff.addClass('hide-comments');
607 608 } else {
608 609 $filediff.find('.hide-line-comments').removeClass('hide-line-comments');
609 610 $filediff.toggleClass('hide-comments');
610 611 }
611 612 return false;
612 613 };
613 614
614 615 this.toggleLineComments = function(node) {
615 616 self.toggleComments(node, true);
616 617 var $node = $(node);
617 618 // mark outdated comments as visible before the toggle;
618 619 $(node.closest('tr')).find('.comment-outdated').show();
619 620 $node.closest('tr').toggleClass('hide-line-comments');
620 621 };
621 622
622 623 this.createCommentForm = function(formElement, lineno, placeholderText, initAutocompleteActions, resolvesCommentId){
623 624 var pullRequestId = templateContext.pull_request_data.pull_request_id;
624 625 var commitId = templateContext.commit_data.commit_id;
625 626
626 627 var commentForm = new CommentForm(
627 628 formElement, commitId, pullRequestId, lineno, initAutocompleteActions, resolvesCommentId);
628 629 var cm = commentForm.getCmInstance();
629 630
630 631 if (resolvesCommentId){
631 632 var placeholderText = _gettext('Leave a comment, or click resolve button to resolve TODO comment #{0}').format(resolvesCommentId);
632 633 }
633 634
634 635 setTimeout(function() {
635 636 // callbacks
636 637 if (cm !== undefined) {
637 638 commentForm.setPlaceholder(placeholderText);
638 639 if (commentForm.isInline()) {
639 640 cm.focus();
640 641 cm.refresh();
641 642 }
642 643 }
643 644 }, 10);
644 645
645 646 // trigger scrolldown to the resolve comment, since it might be away
646 647 // from the clicked
647 648 if (resolvesCommentId){
648 649 var actionNode = $(commentForm.resolvesActionId).offset();
649 650
650 651 setTimeout(function() {
651 652 if (actionNode) {
652 653 $('body, html').animate({scrollTop: actionNode.top}, 10);
653 654 }
654 655 }, 100);
655 656 }
656 657
657 658 // add dropzone support
658 659 var insertAttachmentText = function (cm, attachmentName, attachmentStoreUrl, isRendered) {
659 660 var renderer = templateContext.visual.default_renderer;
660 661 if (renderer == 'rst') {
661 662 var attachmentUrl = '`#{0} <{1}>`_'.format(attachmentName, attachmentStoreUrl);
662 663 if (isRendered){
663 664 attachmentUrl = '\n.. image:: {0}'.format(attachmentStoreUrl);
664 665 }
665 666 } else if (renderer == 'markdown') {
666 667 var attachmentUrl = '[{0}]({1})'.format(attachmentName, attachmentStoreUrl);
667 668 if (isRendered){
668 669 attachmentUrl = '!' + attachmentUrl;
669 670 }
670 671 } else {
671 672 var attachmentUrl = '{}'.format(attachmentStoreUrl);
672 673 }
673 674 cm.replaceRange(attachmentUrl+'\n', CodeMirror.Pos(cm.lastLine()));
674 675
675 676 return false;
676 677 };
677 678
678 679 //see: https://www.dropzonejs.com/#configuration
679 680 var storeUrl = pyroutes.url('repo_commit_comment_attachment_upload',
680 681 {'repo_name': templateContext.repo_name,
681 682 'commit_id': templateContext.commit_data.commit_id})
682 683
683 684 var previewTmpl = $(formElement).find('.comment-attachment-uploader-template').get(0);
684 685 if (previewTmpl !== undefined){
685 686 var selectLink = $(formElement).find('.pick-attachment').get(0);
686 687 $(formElement).find('.comment-attachment-uploader').dropzone({
687 688 url: storeUrl,
688 689 headers: {"X-CSRF-Token": CSRF_TOKEN},
689 690 paramName: function () {
690 691 return "attachment"
691 692 }, // The name that will be used to transfer the file
692 693 clickable: selectLink,
693 694 parallelUploads: 1,
694 695 maxFiles: 10,
695 696 maxFilesize: templateContext.attachment_store.max_file_size_mb,
696 697 uploadMultiple: false,
697 698 autoProcessQueue: true, // if false queue will not be processed automatically.
698 699 createImageThumbnails: false,
699 700 previewTemplate: previewTmpl.innerHTML,
700 701
701 702 accept: function (file, done) {
702 703 done();
703 704 },
704 705 init: function () {
705 706
706 707 this.on("sending", function (file, xhr, formData) {
707 708 $(formElement).find('.comment-attachment-uploader').find('.dropzone-text').hide();
708 709 $(formElement).find('.comment-attachment-uploader').find('.dropzone-upload').show();
709 710 });
710 711
711 712 this.on("success", function (file, response) {
712 713 $(formElement).find('.comment-attachment-uploader').find('.dropzone-text').show();
713 714 $(formElement).find('.comment-attachment-uploader').find('.dropzone-upload').hide();
714 715
715 716 var isRendered = false;
716 717 var ext = file.name.split('.').pop();
717 718 var imageExts = templateContext.attachment_store.image_ext;
718 719 if (imageExts.indexOf(ext) !== -1){
719 720 isRendered = true;
720 721 }
721 722
722 723 insertAttachmentText(cm, file.name, response.repo_fqn_access_path, isRendered)
723 724 });
724 725
725 726 this.on("error", function (file, errorMessage, xhr) {
726 727 $(formElement).find('.comment-attachment-uploader').find('.dropzone-upload').hide();
727 728
728 729 var error = null;
729 730
730 731 if (xhr !== undefined){
731 732 var httpStatus = xhr.status + " " + xhr.statusText;
732 733 if (xhr !== undefined && xhr.status >= 500) {
733 734 error = httpStatus;
734 735 }
735 736 }
736 737
737 738 if (error === null) {
738 739 error = errorMessage.error || errorMessage || httpStatus;
739 740 }
740 741 $(file.previewElement).find('.dz-error-message').html('ERROR: {0}'.format(error));
741 742
742 743 });
743 744 }
744 745 });
745 746 }
746 747 return commentForm;
747 748 };
748 749
749 750 this.createGeneralComment = function (lineNo, placeholderText, resolvesCommentId) {
750 751
751 752 var tmpl = $('#cb-comment-general-form-template').html();
752 753 tmpl = tmpl.format(null, 'general');
753 754 var $form = $(tmpl);
754 755
755 756 var $formPlaceholder = $('#cb-comment-general-form-placeholder');
756 757 var curForm = $formPlaceholder.find('form');
757 758 if (curForm){
758 759 curForm.remove();
759 760 }
760 761 $formPlaceholder.append($form);
761 762
762 763 var _form = $($form[0]);
763 764 var autocompleteActions = ['approve', 'reject', 'as_note', 'as_todo'];
764 765 var commentForm = this.createCommentForm(
765 766 _form, lineNo, placeholderText, autocompleteActions, resolvesCommentId);
766 767 commentForm.initStatusChangeSelector();
767 768
768 769 return commentForm;
769 770 };
770 771
771 772 this.createComment = function(node, resolutionComment) {
772 773 var resolvesCommentId = resolutionComment || null;
773 774 var $node = $(node);
774 775 var $td = $node.closest('td');
775 776 var $form = $td.find('.comment-inline-form');
776 777
777 778 if (!$form.length) {
778 779
779 780 var $filediff = $node.closest('.filediff');
780 781 $filediff.removeClass('hide-comments');
781 782 var f_path = $filediff.attr('data-f-path');
782 783 var lineno = self.getLineNumber(node);
783 784 // create a new HTML from template
784 785 var tmpl = $('#cb-comment-inline-form-template').html();
785 786 tmpl = tmpl.format(escapeHtml(f_path), lineno);
786 787 $form = $(tmpl);
787 788
788 789 var $comments = $td.find('.inline-comments');
789 790 if (!$comments.length) {
790 791 $comments = $(
791 792 $('#cb-comments-inline-container-template').html());
792 793 $td.append($comments);
793 794 }
794 795
795 796 $td.find('.cb-comment-add-button').before($form);
796 797
797 798 var placeholderText = _gettext('Leave a comment on line {0}.').format(lineno);
798 799 var _form = $($form[0]).find('form');
799 800 var autocompleteActions = ['as_note', 'as_todo'];
800 801 var commentForm = this.createCommentForm(
801 802 _form, lineno, placeholderText, autocompleteActions, resolvesCommentId);
802 803
803 804 $.Topic('/ui/plugins/code/comment_form_built').prepareOrPublish({
804 805 form: _form,
805 806 parent: $td[0],
806 807 lineno: lineno,
807 808 f_path: f_path}
808 809 );
809 810
810 811 // set a CUSTOM submit handler for inline comments.
811 812 commentForm.setHandleFormSubmit(function(o) {
812 813 var text = commentForm.cm.getValue();
813 814 var commentType = commentForm.getCommentType();
814 815 var resolvesCommentId = commentForm.getResolvesId();
815 816
816 817 if (text === "") {
817 818 return;
818 819 }
819 820
820 821 if (lineno === undefined) {
821 822 alert('missing line !');
822 823 return;
823 824 }
824 825 if (f_path === undefined) {
825 826 alert('missing file path !');
826 827 return;
827 828 }
828 829
829 830 var excludeCancelBtn = false;
830 831 var submitEvent = true;
831 832 commentForm.setActionButtonsDisabled(true, excludeCancelBtn, submitEvent);
832 833 commentForm.cm.setOption("readOnly", true);
833 834 var postData = {
834 835 'text': text,
835 836 'f_path': f_path,
836 837 'line': lineno,
837 838 'comment_type': commentType,
838 839 'csrf_token': CSRF_TOKEN
839 840 };
840 841 if (resolvesCommentId){
841 842 postData['resolves_comment_id'] = resolvesCommentId;
842 843 }
843 844
844 845 var submitSuccessCallback = function(json_data) {
845 846 $form.remove();
846 847 try {
847 848 var html = json_data.rendered_text;
848 849 var lineno = json_data.line_no;
849 850 var target_id = json_data.target_id;
850 851
851 852 $comments.find('.cb-comment-add-button').before(html);
852 853
853 854 //mark visually which comment was resolved
854 855 if (resolvesCommentId) {
855 856 commentForm.markCommentResolved(resolvesCommentId);
856 857 }
857 858
858 859 // run global callback on submit
859 860 commentForm.globalSubmitSuccessCallback();
860 861
861 862 } catch (e) {
862 863 console.error(e);
863 864 }
864 865
865 866 // re trigger the linkification of next/prev navigation
866 867 linkifyComments($('.inline-comment-injected'));
867 868 timeagoActivate();
869 tooltipActivate();
868 870
869 871 if (window.updateSticky !== undefined) {
870 872 // potentially our comments change the active window size, so we
871 873 // notify sticky elements
872 874 updateSticky()
873 875 }
874 876
875 877 commentForm.setActionButtonsDisabled(false);
876 878
877 879 };
878 880 var submitFailCallback = function(data){
879 881 alert(
880 882 "Error while submitting comment.\n" +
881 883 "Error code {0} ({1}).".format(data.status, data.statusText)
882 884 );
883 885 commentForm.resetCommentFormState(text)
884 886 };
885 887 commentForm.submitAjaxPOST(
886 888 commentForm.submitUrl, postData, submitSuccessCallback, submitFailCallback);
887 889 });
888 890 }
889 891
890 892 $form.addClass('comment-inline-form-open');
891 893 };
892 894
893 895 this.createResolutionComment = function(commentId){
894 896 // hide the trigger text
895 897 $('#resolve-comment-{0}'.format(commentId)).hide();
896 898
897 899 var comment = $('#comment-'+commentId);
898 900 var commentData = comment.data();
899 901 if (commentData.commentInline) {
900 902 this.createComment(comment, commentId)
901 903 } else {
902 904 Rhodecode.comments.createGeneralComment('general', "$placeholder", commentId)
903 905 }
904 906
905 907 return false;
906 908 };
907 909
908 910 this.submitResolution = function(commentId){
909 911 var form = $('#resolve_comment_{0}'.format(commentId)).closest('form');
910 912 var commentForm = form.get(0).CommentForm;
911 913
912 914 var cm = commentForm.getCmInstance();
913 915 var renderer = templateContext.visual.default_renderer;
914 916 if (renderer == 'rst'){
915 917 var commentUrl = '`#{0} <{1}#comment-{0}>`_'.format(commentId, commentForm.selfUrl);
916 918 } else if (renderer == 'markdown') {
917 919 var commentUrl = '[#{0}]({1}#comment-{0})'.format(commentId, commentForm.selfUrl);
918 920 } else {
919 921 var commentUrl = '{1}#comment-{0}'.format(commentId, commentForm.selfUrl);
920 922 }
921 923
922 924 cm.setValue(_gettext('TODO from comment {0} was fixed.').format(commentUrl));
923 925 form.submit();
924 926 return false;
925 927 };
926 928
927 929 };
@@ -1,517 +1,519 b''
1 1 // # Copyright (C) 2010-2019 RhodeCode GmbH
2 2 // #
3 3 // # This program is free software: you can redistribute it and/or modify
4 4 // # it under the terms of the GNU Affero General Public License, version 3
5 5 // # (only), as published by the Free Software Foundation.
6 6 // #
7 7 // # This program is distributed in the hope that it will be useful,
8 8 // # but WITHOUT ANY WARRANTY; without even the implied warranty of
9 9 // # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
10 10 // # GNU General Public License for more details.
11 11 // #
12 12 // # You should have received a copy of the GNU Affero General Public License
13 13 // # along with this program. If not, see <http://www.gnu.org/licenses/>.
14 14 // #
15 15 // # This program is dual-licensed. If you wish to learn more about the
16 16 // # RhodeCode Enterprise Edition, including its added features, Support services,
17 17 // # and proprietary license terms, please see https://rhodecode.com/licenses/
18 18
19 19 /**
20 20 * Search file list
21 21 */
22 22
23 23 var NodeFilter = {};
24 24
25 25 var fileBrowserListeners = function (node_list_url, url_base) {
26 26 var $filterInput = $('#node_filter');
27 27 var n_filter = $filterInput.get(0);
28 28
29 29 NodeFilter.filterTimeout = null;
30 30 var nodes = null;
31 31
32 32 NodeFilter.focus = function () {
33 33 $filterInput.focus()
34 34 };
35 35
36 36 NodeFilter.fetchNodes = function (callback) {
37 37 $.ajax(
38 38 {url: node_list_url, headers: {'X-PARTIAL-XHR': true}})
39 39 .done(function (data) {
40 40 nodes = data.nodes;
41 41 if (callback) {
42 42 callback();
43 43 }
44 44 })
45 45 .fail(function (data) {
46 46 console.log('failed to load');
47 47 });
48 48 };
49 49
50 50 NodeFilter.initFilter = function (e) {
51 51 if ($filterInput.hasClass('loading')) {
52 52 return
53 53 }
54 54
55 55 // in case we are already loaded, do nothing
56 56 if (!$filterInput.hasClass('init')) {
57 57 return NodeFilter.handleKey(e);
58 58 }
59 59 var iconLoading = 'icon-spin animate-spin';
60 60 var iconSearch = 'icon-search';
61 61 $('.files-filter-box-path i').removeClass(iconSearch).addClass(iconLoading);
62 62 $filterInput.addClass('loading');
63 63
64 64 var callback = function (org) {
65 65 return function () {
66 66 if ($filterInput.hasClass('init')) {
67 67 $filterInput.removeClass('init');
68 68 $filterInput.removeClass('loading');
69 69 }
70 70 $('.files-filter-box-path i').removeClass(iconLoading).addClass(iconSearch);
71 71
72 72 // auto re-filter if we filled in the input
73 73 if (n_filter.value !== "") {
74 74 NodeFilter.updateFilter(n_filter, e)()
75 75 }
76 76
77 77 }
78 78 };
79 79 // load node data
80 80 NodeFilter.fetchNodes(callback());
81 81
82 82 };
83 83
84 84 NodeFilter.resetFilter = function () {
85 85 $('#tbody').show();
86 86 $('#tbody_filtered').hide();
87 87 $filterInput.val('');
88 88 };
89 89
90 90 NodeFilter.handleKey = function (e) {
91 91 var scrollDown = function (element) {
92 92 var elementBottom = element.offset().top + $(element).outerHeight();
93 93 var windowBottom = window.innerHeight + $(window).scrollTop();
94 94 if (elementBottom > windowBottom) {
95 95 var offset = elementBottom - window.innerHeight;
96 96 $('html,body').scrollTop(offset);
97 97 return false;
98 98 }
99 99 return true;
100 100 };
101 101
102 102 var scrollUp = function (element) {
103 103 if (element.offset().top < $(window).scrollTop()) {
104 104 $('html,body').scrollTop(element.offset().top);
105 105 return false;
106 106 }
107 107 return true;
108 108 };
109 109 var $hlElem = $('.browser-highlight');
110 110
111 111 if (e.keyCode === 40) { // Down
112 112 if ($hlElem.length === 0) {
113 113 $('.browser-result').first().addClass('browser-highlight');
114 114 } else {
115 115 var next = $hlElem.next();
116 116 if (next.length !== 0) {
117 117 $hlElem.removeClass('browser-highlight');
118 118 next.addClass('browser-highlight');
119 119 }
120 120 }
121 121
122 122 if ($hlElem.get(0) !== undefined){
123 123 scrollDown($hlElem);
124 124 }
125 125 }
126 126 if (e.keyCode === 38) { // Up
127 127 e.preventDefault();
128 128 if ($hlElem.length !== 0) {
129 129 var next = $hlElem.prev();
130 130 if (next.length !== 0) {
131 131 $('.browser-highlight').removeClass('browser-highlight');
132 132 next.addClass('browser-highlight');
133 133 }
134 134 }
135 135
136 136 if ($hlElem.get(0) !== undefined){
137 137 scrollUp($hlElem);
138 138 }
139 139
140 140 }
141 141 if (e.keyCode === 13) { // Enter
142 142 if ($('.browser-highlight').length !== 0) {
143 143 var url = $('.browser-highlight').find('.match-link').attr('href');
144 144 window.location = url;
145 145 }
146 146 }
147 147 if (e.keyCode === 27) { // Esc
148 148 NodeFilter.resetFilter();
149 149 $('html,body').scrollTop(0);
150 150 }
151 151
152 152 var capture_keys = [
153 153 40, // ArrowDown
154 154 38, // ArrowUp
155 155 39, // ArrowRight
156 156 37, // ArrowLeft
157 157 13, // Enter
158 158 27 // Esc
159 159 ];
160 160
161 161 if ($.inArray(e.keyCode, capture_keys) === -1) {
162 162 clearTimeout(NodeFilter.filterTimeout);
163 163 NodeFilter.filterTimeout = setTimeout(NodeFilter.updateFilter(n_filter, e), 200);
164 164 }
165 165
166 166 };
167 167
168 168 NodeFilter.fuzzy_match = function (filepath, query) {
169 169 var highlight = [];
170 170 var order = 0;
171 171 for (var i = 0; i < query.length; i++) {
172 172 var match_position = filepath.indexOf(query[i]);
173 173 if (match_position !== -1) {
174 174 var prev_match_position = highlight[highlight.length - 1];
175 175 if (prev_match_position === undefined) {
176 176 highlight.push(match_position);
177 177 } else {
178 178 var current_match_position = prev_match_position + match_position + 1;
179 179 highlight.push(current_match_position);
180 180 order = order + current_match_position - prev_match_position;
181 181 }
182 182 filepath = filepath.substring(match_position + 1);
183 183 } else {
184 184 return false;
185 185 }
186 186 }
187 187 return {
188 188 'order': order,
189 189 'highlight': highlight
190 190 };
191 191 };
192 192
193 193 NodeFilter.sortPredicate = function (a, b) {
194 194 if (a.order < b.order) return -1;
195 195 if (a.order > b.order) return 1;
196 196 if (a.filepath < b.filepath) return -1;
197 197 if (a.filepath > b.filepath) return 1;
198 198 return 0;
199 199 };
200 200
201 201 NodeFilter.updateFilter = function (elem, e) {
202 202 return function () {
203 203 // Reset timeout
204 204 NodeFilter.filterTimeout = null;
205 205 var query = elem.value.toLowerCase();
206 206 var match = [];
207 207 var matches_max = 20;
208 208 if (query !== "") {
209 209 var results = [];
210 210 for (var k = 0; k < nodes.length; k++) {
211 211 var result = NodeFilter.fuzzy_match(
212 212 nodes[k].name.toLowerCase(), query);
213 213 if (result) {
214 214 result.type = nodes[k].type;
215 215 result.filepath = nodes[k].name;
216 216 results.push(result);
217 217 }
218 218 }
219 219 results = results.sort(NodeFilter.sortPredicate);
220 220 var limit = matches_max;
221 221 if (results.length < matches_max) {
222 222 limit = results.length;
223 223 }
224 224 for (var i = 0; i < limit; i++) {
225 225 if (query && results.length > 0) {
226 226 var n = results[i].filepath;
227 227 var t = results[i].type;
228 228 var n_hl = n.split("");
229 229 var pos = results[i].highlight;
230 230 for (var j = 0; j < pos.length; j++) {
231 231 n_hl[pos[j]] = "<em>" + n_hl[pos[j]] + "</em>";
232 232 }
233 233 n_hl = n_hl.join("");
234 234 var new_url = url_base.replace('__FPATH__', n);
235 235
236 236 var typeObj = {
237 237 dir: 'icon-directory browser-dir',
238 238 file: 'icon-file-text browser-file'
239 239 };
240 240
241 241 var typeIcon = '<i class="{0}"></i>'.format(typeObj[t]);
242 242 match.push('<tr class="browser-result"><td><a class="match-link" href="{0}">{1}{2}</a></td><td colspan="5"></td></tr>'.format(new_url, typeIcon, n_hl));
243 243 }
244 244 }
245 245 if (results.length > limit) {
246 246 var truncated_count = results.length - matches_max;
247 247 if (truncated_count === 1) {
248 248 match.push('<tr><td>{0} {1}</td><td colspan="5"></td></tr>'.format(truncated_count, _gettext('truncated result')));
249 249 } else {
250 250 match.push('<tr><td>{0} {1}</td><td colspan="5"></td></tr>'.format(truncated_count, _gettext('truncated results')));
251 251 }
252 252 }
253 253 }
254 254 if (query !== "") {
255 255 $('#tbody').hide();
256 256 $('#tbody_filtered').show();
257 257
258 258 if (match.length === 0) {
259 259 match.push('<tr><td>{0}</td><td colspan="5"></td></tr>'.format(_gettext('No matching files')));
260 260 }
261 261 $('#tbody_filtered').html(match.join(""));
262 262 } else {
263 263 $('#tbody').show();
264 264 $('#tbody_filtered').hide();
265 265 }
266 266
267 267 };
268 268 };
269 269
270 270 };
271 271
272 272 var getIdentNode = function(n){
273 273 // iterate through nodes until matched interesting node
274 274 if (typeof n === 'undefined'){
275 275 return -1;
276 276 }
277 277 if(typeof n.id !== "undefined" && n.id.match('L[0-9]+')){
278 278 return n;
279 279 }
280 280 else{
281 281 return getIdentNode(n.parentNode);
282 282 }
283 283 };
284 284
285 285 var getSelectionLink = function(e) {
286 286 // get selection from start/to nodes
287 287 if (typeof window.getSelection !== "undefined") {
288 288 s = window.getSelection();
289 289
290 290 from = getIdentNode(s.anchorNode);
291 291 till = getIdentNode(s.focusNode);
292 292
293 293 f_int = parseInt(from.id.replace('L',''));
294 294 t_int = parseInt(till.id.replace('L',''));
295 295
296 296 if (f_int > t_int){
297 297 // highlight from bottom
298 298 offset = -35;
299 299 ranges = [t_int,f_int];
300 300 }
301 301 else{
302 302 // highligth from top
303 303 offset = 35;
304 304 ranges = [f_int,t_int];
305 305 }
306 306 // if we select more than 2 lines
307 307 if (ranges[0] !== ranges[1]){
308 308 if($('#linktt').length === 0){
309 309 hl_div = document.createElement('div');
310 310 hl_div.id = 'linktt';
311 311 }
312 312 hl_div.innerHTML = '';
313 313
314 314 anchor = '#L'+ranges[0]+'-'+ranges[1];
315 315 var link = document.createElement('a');
316 316 link.href = location.href.substring(0,location.href.indexOf('#'))+anchor;
317 317 link.innerHTML = _gettext('Selection link');
318 318 hl_div.appendChild(link);
319 319 $('#codeblock').append(hl_div);
320 320
321 321 var xy = $(till).offset();
322 322 $('#linktt').addClass('hl-tip-box tip-box');
323 323 $('#linktt').offset({top: xy.top + offset, left: xy.left});
324 324 $('#linktt').css('visibility','visible');
325 325 }
326 326 else{
327 327 $('#linktt').css('visibility','hidden');
328 328 }
329 329 }
330 330 };
331 331
332 332 var getFileState = function() {
333 333 // relies on a global set filesUrlData
334 334 var f_path = filesUrlData['f_path'];
335 335 var commit_id = filesUrlData['commit_id'];
336 336
337 337 var url_params = {
338 338 repo_name: templateContext.repo_name,
339 339 commit_id: commit_id,
340 340 f_path:'__FPATH__'
341 341 };
342 342 if (atRef !== '') {
343 343 url_params['at'] = atRef
344 344 }
345 345
346 346 var _url_base = pyroutes.url('repo_files', url_params);
347 347 var _node_list_url = pyroutes.url('repo_files_nodelist',
348 348 {repo_name: templateContext.repo_name,
349 349 commit_id: commit_id, f_path: f_path});
350 350
351 351 return {
352 352 f_path: f_path,
353 353 commit_id: commit_id,
354 354 node_list_url: _node_list_url,
355 355 url_base: _url_base
356 356 };
357 357 };
358 358
359 359 var getFilesMetadata = function() {
360 360 // relies on metadataRequest global state
361 361 if (metadataRequest && metadataRequest.readyState != 4) {
362 362 metadataRequest.abort();
363 363 }
364 364
365 365 if ($('#file-tree-wrapper').hasClass('full-load')) {
366 366 // in case our HTML wrapper has full-load class we don't
367 367 // trigger the async load of metadata
368 368 return false;
369 369 }
370 370
371 371 var state = getFileState();
372 372 var url_data = {
373 373 'repo_name': templateContext.repo_name,
374 374 'commit_id': state.commit_id,
375 375 'f_path': state.f_path
376 376 };
377 377
378 378 var url = pyroutes.url('repo_nodetree_full', url_data);
379 379
380 380 metadataRequest = $.ajax({url: url});
381 381
382 382 metadataRequest.done(function(data) {
383 383 $('#file-tree').html(data);
384 384 timeagoActivate();
385 tooltipActivate();
385 386 });
386 387 metadataRequest.fail(function (data, textStatus, errorThrown) {
387 388 if (data.status != 0) {
388 389 alert("Error while fetching metadata.\nError code {0} ({1}).Please consider reloading the page".format(data.status,data.statusText));
389 390 }
390 391 });
391 392 };
392 393
393 394 // show more authors
394 395 var showAuthors = function(elem, annotate) {
395 396 var state = getFileState('callbacks');
396 397
397 398 var url = pyroutes.url('repo_file_authors',
398 399 {'repo_name': templateContext.repo_name,
399 400 'commit_id': state.commit_id, 'f_path': state.f_path});
400 401
401 402 $.pjax({
402 403 url: url,
403 404 data: 'annotate={0}'.format(annotate),
404 405 container: '#file_authors',
405 406 push: false,
406 407 timeout: 5000
407 408 }).complete(function(){
408 409 $(elem).hide();
409 $('#file_authors_title').html(_gettext('All Authors'))
410 $('#file_authors_title').html(_gettext('All Authors'));
411 tooltipActivate();
410 412 })
411 413 };
412 414
413 415
414 416 (function (mod) {
415 417
416 418 if (typeof exports == "object" && typeof module == "object") {
417 419 // CommonJS
418 420 module.exports = mod();
419 421 } else {
420 422 // Plain browser env
421 423 (this || window).FileEditor = mod();
422 424 }
423 425
424 426 })(function () {
425 427 "use strict";
426 428
427 429 function FileEditor(textAreaElement, options) {
428 430 if (!(this instanceof FileEditor)) {
429 431 return new FileEditor(textAreaElement, options);
430 432 }
431 433 // bind the element instance to our Form
432 434 var te = $(textAreaElement).get(0);
433 435 if (te !== undefined) {
434 436 te.FileEditor = this;
435 437 }
436 438
437 439 this.modes_select = '#set_mode';
438 440 this.filename_selector = '#filename';
439 441 this.commit_btn_selector = '#commit_btn';
440 442 this.line_wrap_selector = '#line_wrap';
441 443 this.editor_preview_selector = '#editor_preview';
442 444
443 445 if (te !== undefined) {
444 446 this.cm = initCodeMirror(textAreaElement, null, false);
445 447 }
446 448
447 449 // FUNCTIONS and helpers
448 450 var self = this;
449 451
450 452 this.submitHandler = function() {
451 453 $(self.commit_btn_selector).on('click', function(e) {
452 454
453 455 var filename = $(self.filename_selector).val();
454 456 if (filename === "") {
455 457 alert("Missing filename");
456 458 e.preventDefault();
457 459 }
458 460
459 461 var button = $(this);
460 462 if (button.hasClass('clicked')) {
461 463 button.attr('disabled', true);
462 464 } else {
463 465 button.addClass('clicked');
464 466 }
465 467 });
466 468 };
467 469 this.submitHandler();
468 470
469 471 // on select line wraps change the editor
470 472 this.lineWrapHandler = function () {
471 473 $(self.line_wrap_selector).on('change', function (e) {
472 474 var selected = e.currentTarget;
473 475 var line_wraps = {'on': true, 'off': false}[selected.value];
474 476 setCodeMirrorLineWrap(self.cm, line_wraps)
475 477 });
476 478 };
477 479 this.lineWrapHandler();
478 480
479 481
480 482 this.showPreview = function () {
481 483
482 484 var _text = self.cm.getValue();
483 485 var _file_path = $(self.filename_selector).val();
484 486 if (_text && _file_path) {
485 487 $('.show-preview').addClass('active');
486 488 $('.show-editor').removeClass('active');
487 489
488 490 $(self.editor_preview_selector).show();
489 491 $(self.cm.getWrapperElement()).hide();
490 492
491 493
492 494 var post_data = {'text': _text, 'file_path': _file_path, 'csrf_token': CSRF_TOKEN};
493 495 $(self.editor_preview_selector).html(_gettext('Loading ...'));
494 496
495 497 var url = pyroutes.url('file_preview');
496 498
497 499 ajaxPOST(url, post_data, function (o) {
498 500 $(self.editor_preview_selector).html(o);
499 501 })
500 502 }
501 503
502 504 };
503 505
504 506 this.showEditor = function () {
505 507 $(self.editor_preview_selector).hide();
506 508 $('.show-editor').addClass('active');
507 509 $('.show-preview').removeClass('active');
508 510
509 511 $(self.cm.getWrapperElement()).show();
510 512 };
511 513
512 514
513 515 }
514 516
515 517 return FileEditor;
516 518 });
517 519
@@ -1,551 +1,552 b''
1 1 // # Copyright (C) 2010-2019 RhodeCode GmbH
2 2 // #
3 3 // # This program is free software: you can redistribute it and/or modify
4 4 // # it under the terms of the GNU Affero General Public License, version 3
5 5 // # (only), as published by the Free Software Foundation.
6 6 // #
7 7 // # This program is distributed in the hope that it will be useful,
8 8 // # but WITHOUT ANY WARRANTY; without even the implied warranty of
9 9 // # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
10 10 // # GNU General Public License for more details.
11 11 // #
12 12 // # You should have received a copy of the GNU Affero General Public License
13 13 // # along with this program. If not, see <http://www.gnu.org/licenses/>.
14 14 // #
15 15 // # This program is dual-licensed. If you wish to learn more about the
16 16 // # RhodeCode Enterprise Edition, including its added features, Support services,
17 17 // # and proprietary license terms, please see https://rhodecode.com/licenses/
18 18
19 19
20 20 var prButtonLockChecks = {
21 21 'compare': false,
22 22 'reviewers': false
23 23 };
24 24
25 25 /**
26 26 * lock button until all checks and loads are made. E.g reviewer calculation
27 27 * should prevent from submitting a PR
28 28 * @param lockEnabled
29 29 * @param msg
30 30 * @param scope
31 31 */
32 32 var prButtonLock = function(lockEnabled, msg, scope) {
33 33 scope = scope || 'all';
34 34 if (scope == 'all'){
35 35 prButtonLockChecks['compare'] = !lockEnabled;
36 36 prButtonLockChecks['reviewers'] = !lockEnabled;
37 37 } else if (scope == 'compare') {
38 38 prButtonLockChecks['compare'] = !lockEnabled;
39 39 } else if (scope == 'reviewers'){
40 40 prButtonLockChecks['reviewers'] = !lockEnabled;
41 41 }
42 42 var checksMeet = prButtonLockChecks.compare && prButtonLockChecks.reviewers;
43 43 if (lockEnabled) {
44 44 $('#pr_submit').attr('disabled', 'disabled');
45 45 }
46 46 else if (checksMeet) {
47 47 $('#pr_submit').removeAttr('disabled');
48 48 }
49 49
50 50 if (msg) {
51 51 $('#pr_open_message').html(msg);
52 52 }
53 53 };
54 54
55 55
56 56 /**
57 57 Generate Title and Description for a PullRequest.
58 58 In case of 1 commits, the title and description is that one commit
59 59 in case of multiple commits, we iterate on them with max N number of commits,
60 60 and build description in a form
61 61 - commitN
62 62 - commitN+1
63 63 ...
64 64
65 65 Title is then constructed from branch names, or other references,
66 66 replacing '-' and '_' into spaces
67 67
68 68 * @param sourceRef
69 69 * @param elements
70 70 * @param limit
71 71 * @returns {*[]}
72 72 */
73 73 var getTitleAndDescription = function(sourceRef, elements, limit) {
74 74 var title = '';
75 75 var desc = '';
76 76
77 77 $.each($(elements).get().reverse().slice(0, limit), function(idx, value) {
78 78 var rawMessage = $(value).find('td.td-description .message').data('messageRaw');
79 79 desc += '- ' + rawMessage.split('\n')[0].replace(/\n+$/, "") + '\n';
80 80 });
81 81 // only 1 commit, use commit message as title
82 82 if (elements.length === 1) {
83 83 title = $(elements[0]).find('td.td-description .message').data('messageRaw').split('\n')[0];
84 84 }
85 85 else {
86 86 // use reference name
87 87 title = sourceRef.replace(/-/g, ' ').replace(/_/g, ' ').capitalizeFirstLetter();
88 88 }
89 89
90 90 return [title, desc]
91 91 };
92 92
93 93
94 94
95 95 ReviewersController = function () {
96 96 var self = this;
97 97 this.$reviewRulesContainer = $('#review_rules');
98 98 this.$rulesList = this.$reviewRulesContainer.find('.pr-reviewer-rules');
99 99 this.forbidReviewUsers = undefined;
100 100 this.$reviewMembers = $('#review_members');
101 101 this.currentRequest = null;
102 102
103 103 this.defaultForbidReviewUsers = function() {
104 104 return [
105 105 {'username': 'default',
106 106 'user_id': templateContext.default_user.user_id}
107 107 ];
108 108 };
109 109
110 110 this.hideReviewRules = function() {
111 111 self.$reviewRulesContainer.hide();
112 112 };
113 113
114 114 this.showReviewRules = function() {
115 115 self.$reviewRulesContainer.show();
116 116 };
117 117
118 118 this.addRule = function(ruleText) {
119 119 self.showReviewRules();
120 120 return '<div>- {0}</div>'.format(ruleText)
121 121 };
122 122
123 123 this.loadReviewRules = function(data) {
124 124 // reset forbidden Users
125 125 this.forbidReviewUsers = self.defaultForbidReviewUsers();
126 126
127 127 // reset state of review rules
128 128 self.$rulesList.html('');
129 129
130 130 if (!data || data.rules === undefined || $.isEmptyObject(data.rules)) {
131 131 // default rule, case for older repo that don't have any rules stored
132 132 self.$rulesList.append(
133 133 self.addRule(
134 134 _gettext('All reviewers must vote.'))
135 135 );
136 136 return self.forbidReviewUsers
137 137 }
138 138
139 139 if (data.rules.voting !== undefined) {
140 140 if (data.rules.voting < 0) {
141 141 self.$rulesList.append(
142 142 self.addRule(
143 143 _gettext('All individual reviewers must vote.'))
144 144 )
145 145 } else if (data.rules.voting === 1) {
146 146 self.$rulesList.append(
147 147 self.addRule(
148 148 _gettext('At least {0} reviewer must vote.').format(data.rules.voting))
149 149 )
150 150
151 151 } else {
152 152 self.$rulesList.append(
153 153 self.addRule(
154 154 _gettext('At least {0} reviewers must vote.').format(data.rules.voting))
155 155 )
156 156 }
157 157 }
158 158
159 159 if (data.rules.voting_groups !== undefined) {
160 160 $.each(data.rules.voting_groups, function(index, rule_data) {
161 161 self.$rulesList.append(
162 162 self.addRule(rule_data.text)
163 163 )
164 164 });
165 165 }
166 166
167 167 if (data.rules.use_code_authors_for_review) {
168 168 self.$rulesList.append(
169 169 self.addRule(
170 170 _gettext('Reviewers picked from source code changes.'))
171 171 )
172 172 }
173 173 if (data.rules.forbid_adding_reviewers) {
174 174 $('#add_reviewer_input').remove();
175 175 self.$rulesList.append(
176 176 self.addRule(
177 177 _gettext('Adding new reviewers is forbidden.'))
178 178 )
179 179 }
180 180 if (data.rules.forbid_author_to_review) {
181 181 self.forbidReviewUsers.push(data.rules_data.pr_author);
182 182 self.$rulesList.append(
183 183 self.addRule(
184 184 _gettext('Author is not allowed to be a reviewer.'))
185 185 )
186 186 }
187 187 if (data.rules.forbid_commit_author_to_review) {
188 188
189 189 if (data.rules_data.forbidden_users) {
190 190 $.each(data.rules_data.forbidden_users, function(index, member_data) {
191 191 self.forbidReviewUsers.push(member_data)
192 192 });
193 193
194 194 }
195 195
196 196 self.$rulesList.append(
197 197 self.addRule(
198 198 _gettext('Commit Authors are not allowed to be a reviewer.'))
199 199 )
200 200 }
201 201
202 202 return self.forbidReviewUsers
203 203 };
204 204
205 205 this.loadDefaultReviewers = function(sourceRepo, sourceRef, targetRepo, targetRef) {
206 206
207 207 if (self.currentRequest) {
208 208 // make sure we cleanup old running requests before triggering this
209 209 // again
210 210 self.currentRequest.abort();
211 211 }
212 212
213 213 $('.calculate-reviewers').show();
214 214 // reset reviewer members
215 215 self.$reviewMembers.empty();
216 216
217 217 prButtonLock(true, null, 'reviewers');
218 218 $('#user').hide(); // hide user autocomplete before load
219 219
220 220 if (sourceRef.length !== 3 || targetRef.length !== 3) {
221 221 // don't load defaults in case we're missing some refs...
222 222 $('.calculate-reviewers').hide();
223 223 return
224 224 }
225 225
226 226 var url = pyroutes.url('repo_default_reviewers_data',
227 227 {
228 228 'repo_name': templateContext.repo_name,
229 229 'source_repo': sourceRepo,
230 230 'source_ref': sourceRef[2],
231 231 'target_repo': targetRepo,
232 232 'target_ref': targetRef[2]
233 233 });
234 234
235 235 self.currentRequest = $.get(url)
236 236 .done(function(data) {
237 237 self.currentRequest = null;
238 238
239 239 // review rules
240 240 self.loadReviewRules(data);
241 241
242 242 for (var i = 0; i < data.reviewers.length; i++) {
243 243 var reviewer = data.reviewers[i];
244 244 self.addReviewMember(
245 245 reviewer, reviewer.reasons, reviewer.mandatory);
246 246 }
247 247 $('.calculate-reviewers').hide();
248 248 prButtonLock(false, null, 'reviewers');
249 249 $('#user').show(); // show user autocomplete after load
250 250 });
251 251 };
252 252
253 253 // check those, refactor
254 254 this.removeReviewMember = function(reviewer_id, mark_delete) {
255 255 var reviewer = $('#reviewer_{0}'.format(reviewer_id));
256 256
257 257 if(typeof(mark_delete) === undefined){
258 258 mark_delete = false;
259 259 }
260 260
261 261 if(mark_delete === true){
262 262 if (reviewer){
263 263 // now delete the input
264 264 $('#reviewer_{0} input'.format(reviewer_id)).remove();
265 265 // mark as to-delete
266 266 var obj = $('#reviewer_{0}_name'.format(reviewer_id));
267 267 obj.addClass('to-delete');
268 268 obj.css({"text-decoration":"line-through", "opacity": 0.5});
269 269 }
270 270 }
271 271 else{
272 272 $('#reviewer_{0}'.format(reviewer_id)).remove();
273 273 }
274 274 };
275 275 this.reviewMemberEntry = function() {
276 276
277 277 };
278 278 this.addReviewMember = function(reviewer_obj, reasons, mandatory) {
279 279 var members = self.$reviewMembers.get(0);
280 280 var id = reviewer_obj.user_id;
281 281 var username = reviewer_obj.username;
282 282
283 283 var reasons = reasons || [];
284 284 var mandatory = mandatory || false;
285 285
286 286 // register IDS to check if we don't have this ID already in
287 287 var currentIds = [];
288 288 var _els = self.$reviewMembers.find('li').toArray();
289 289 for (el in _els){
290 290 currentIds.push(_els[el].id)
291 291 }
292 292
293 293 var userAllowedReview = function(userId) {
294 294 var allowed = true;
295 295 $.each(self.forbidReviewUsers, function(index, member_data) {
296 296 if (parseInt(userId) === member_data['user_id']) {
297 297 allowed = false;
298 298 return false // breaks the loop
299 299 }
300 300 });
301 301 return allowed
302 302 };
303 303
304 304 var userAllowed = userAllowedReview(id);
305 305 if (!userAllowed){
306 306 alert(_gettext('User `{0}` not allowed to be a reviewer').format(username));
307 307 } else {
308 308 // only add if it's not there
309 309 var alreadyReviewer = currentIds.indexOf('reviewer_'+id) != -1;
310 310
311 311 if (alreadyReviewer) {
312 312 alert(_gettext('User `{0}` already in reviewers').format(username));
313 313 } else {
314 314 members.innerHTML += renderTemplate('reviewMemberEntry', {
315 315 'member': reviewer_obj,
316 316 'mandatory': mandatory,
317 317 'allowed_to_update': true,
318 318 'review_status': 'not_reviewed',
319 319 'review_status_label': _gettext('Not Reviewed'),
320 320 'reasons': reasons,
321 321 'create': true
322 322 });
323 tooltipActivate();
323 324 }
324 325 }
325 326
326 327 };
327 328
328 329 this.updateReviewers = function(repo_name, pull_request_id){
329 330 var postData = $('#reviewers input').serialize();
330 331 _updatePullRequest(repo_name, pull_request_id, postData);
331 332 };
332 333
333 334 };
334 335
335 336
336 337 var _updatePullRequest = function(repo_name, pull_request_id, postData) {
337 338 var url = pyroutes.url(
338 339 'pullrequest_update',
339 340 {"repo_name": repo_name, "pull_request_id": pull_request_id});
340 341 if (typeof postData === 'string' ) {
341 342 postData += '&csrf_token=' + CSRF_TOKEN;
342 343 } else {
343 344 postData.csrf_token = CSRF_TOKEN;
344 345 }
345 346 var success = function(o) {
346 347 window.location.reload();
347 348 };
348 349 ajaxPOST(url, postData, success);
349 350 };
350 351
351 352 /**
352 353 * PULL REQUEST update commits
353 354 */
354 355 var updateCommits = function(repo_name, pull_request_id) {
355 356 var postData = {
356 357 'update_commits': true};
357 358 _updatePullRequest(repo_name, pull_request_id, postData);
358 359 };
359 360
360 361
361 362 /**
362 363 * PULL REQUEST edit info
363 364 */
364 365 var editPullRequest = function(repo_name, pull_request_id, title, description, renderer) {
365 366 var url = pyroutes.url(
366 367 'pullrequest_update',
367 368 {"repo_name": repo_name, "pull_request_id": pull_request_id});
368 369
369 370 var postData = {
370 371 'title': title,
371 372 'description': description,
372 373 'description_renderer': renderer,
373 374 'edit_pull_request': true,
374 375 'csrf_token': CSRF_TOKEN
375 376 };
376 377 var success = function(o) {
377 378 window.location.reload();
378 379 };
379 380 ajaxPOST(url, postData, success);
380 381 };
381 382
382 383
383 384 /**
384 385 * Reviewer autocomplete
385 386 */
386 387 var ReviewerAutoComplete = function(inputId) {
387 388 $(inputId).autocomplete({
388 389 serviceUrl: pyroutes.url('user_autocomplete_data'),
389 390 minChars:2,
390 391 maxHeight:400,
391 392 deferRequestBy: 300, //miliseconds
392 393 showNoSuggestionNotice: true,
393 394 tabDisabled: true,
394 395 autoSelectFirst: true,
395 396 params: { user_id: templateContext.rhodecode_user.user_id, user_groups:true, user_groups_expand:true, skip_default_user:true },
396 397 formatResult: autocompleteFormatResult,
397 398 lookupFilter: autocompleteFilterResult,
398 399 onSelect: function(element, data) {
399 400 var mandatory = false;
400 401 var reasons = [_gettext('added manually by "{0}"').format(templateContext.rhodecode_user.username)];
401 402
402 403 // add whole user groups
403 404 if (data.value_type == 'user_group') {
404 405 reasons.push(_gettext('member of "{0}"').format(data.value_display));
405 406
406 407 $.each(data.members, function(index, member_data) {
407 408 var reviewer = member_data;
408 409 reviewer['user_id'] = member_data['id'];
409 410 reviewer['gravatar_link'] = member_data['icon_link'];
410 411 reviewer['user_link'] = member_data['profile_link'];
411 412 reviewer['rules'] = [];
412 413 reviewersController.addReviewMember(reviewer, reasons, mandatory);
413 414 })
414 415 }
415 416 // add single user
416 417 else {
417 418 var reviewer = data;
418 419 reviewer['user_id'] = data['id'];
419 420 reviewer['gravatar_link'] = data['icon_link'];
420 421 reviewer['user_link'] = data['profile_link'];
421 422 reviewer['rules'] = [];
422 423 reviewersController.addReviewMember(reviewer, reasons, mandatory);
423 424 }
424 425
425 426 $(inputId).val('');
426 427 }
427 428 });
428 429 };
429 430
430 431
431 432 VersionController = function () {
432 433 var self = this;
433 434 this.$verSource = $('input[name=ver_source]');
434 435 this.$verTarget = $('input[name=ver_target]');
435 436 this.$showVersionDiff = $('#show-version-diff');
436 437
437 438 this.adjustRadioSelectors = function (curNode) {
438 439 var getVal = function (item) {
439 440 if (item == 'latest') {
440 441 return Number.MAX_SAFE_INTEGER
441 442 }
442 443 else {
443 444 return parseInt(item)
444 445 }
445 446 };
446 447
447 448 var curVal = getVal($(curNode).val());
448 449 var cleared = false;
449 450
450 451 $.each(self.$verSource, function (index, value) {
451 452 var elVal = getVal($(value).val());
452 453
453 454 if (elVal > curVal) {
454 455 if ($(value).is(':checked')) {
455 456 cleared = true;
456 457 }
457 458 $(value).attr('disabled', 'disabled');
458 459 $(value).removeAttr('checked');
459 460 $(value).css({'opacity': 0.1});
460 461 }
461 462 else {
462 463 $(value).css({'opacity': 1});
463 464 $(value).removeAttr('disabled');
464 465 }
465 466 });
466 467
467 468 if (cleared) {
468 469 // if we unchecked an active, set the next one to same loc.
469 470 $(this.$verSource).filter('[value={0}]'.format(
470 471 curVal)).attr('checked', 'checked');
471 472 }
472 473
473 474 self.setLockAction(false,
474 475 $(curNode).data('verPos'),
475 476 $(this.$verSource).filter(':checked').data('verPos')
476 477 );
477 478 };
478 479
479 480
480 481 this.attachVersionListener = function () {
481 482 self.$verTarget.change(function (e) {
482 483 self.adjustRadioSelectors(this)
483 484 });
484 485 self.$verSource.change(function (e) {
485 486 self.adjustRadioSelectors(self.$verTarget.filter(':checked'))
486 487 });
487 488 };
488 489
489 490 this.init = function () {
490 491
491 492 var curNode = self.$verTarget.filter(':checked');
492 493 self.adjustRadioSelectors(curNode);
493 494 self.setLockAction(true);
494 495 self.attachVersionListener();
495 496
496 497 };
497 498
498 499 this.setLockAction = function (state, selectedVersion, otherVersion) {
499 500 var $showVersionDiff = this.$showVersionDiff;
500 501
501 502 if (state) {
502 503 $showVersionDiff.attr('disabled', 'disabled');
503 504 $showVersionDiff.addClass('disabled');
504 505 $showVersionDiff.html($showVersionDiff.data('labelTextLocked'));
505 506 }
506 507 else {
507 508 $showVersionDiff.removeAttr('disabled');
508 509 $showVersionDiff.removeClass('disabled');
509 510
510 511 if (selectedVersion == otherVersion) {
511 512 $showVersionDiff.html($showVersionDiff.data('labelTextShow'));
512 513 } else {
513 514 $showVersionDiff.html($showVersionDiff.data('labelTextDiff'));
514 515 }
515 516 }
516 517
517 518 };
518 519
519 520 this.showVersionDiff = function () {
520 521 var target = self.$verTarget.filter(':checked');
521 522 var source = self.$verSource.filter(':checked');
522 523
523 524 if (target.val() && source.val()) {
524 525 var params = {
525 526 'pull_request_id': templateContext.pull_request_data.pull_request_id,
526 527 'repo_name': templateContext.repo_name,
527 528 'version': target.val(),
528 529 'from_version': source.val()
529 530 };
530 531 window.location = pyroutes.url('pullrequest_show', params)
531 532 }
532 533
533 534 return false;
534 535 };
535 536
536 537 this.toggleVersionView = function (elem) {
537 538
538 539 if (this.$showVersionDiff.is(':visible')) {
539 540 $('.version-pr').hide();
540 541 this.$showVersionDiff.hide();
541 542 $(elem).html($(elem).data('toggleOn'))
542 543 } else {
543 544 $('.version-pr').show();
544 545 this.$showVersionDiff.show();
545 546 $(elem).html($(elem).data('toggleOff'))
546 547 }
547 548
548 549 return false
549 550 }
550 551
551 552 }; No newline at end of file
This diff has been collapsed as it changes many lines, (4369 lines changed) Show them Hide them
@@ -1,94 +1,4283 b''
1 // # Copyright (C) 2010-2019 RhodeCode GmbH
2 // #
3 // # This program is free software: you can redistribute it and/or modify
4 // # it under the terms of the GNU Affero General Public License, version 3
5 // # (only), as published by the Free Software Foundation.
6 // #
7 // # This program is distributed in the hope that it will be useful,
8 // # but WITHOUT ANY WARRANTY; without even the implied warranty of
9 // # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
10 // # GNU General Public License for more details.
11 // #
12 // # You should have received a copy of the GNU Affero General Public License
13 // # along with this program. If not, see <http://www.gnu.org/licenses/>.
14 // #
15 // # This program is dual-licensed. If you wish to learn more about the
16 // # RhodeCode Enterprise Edition, including its added features, Support services,
17 // # and proprietary license terms, please see https://rhodecode.com/licenses/
1 /**
2 * tooltipster http://iamceege.github.io/tooltipster/
3 * A rockin' custom tooltip jQuery plugin
4 * Developed by Caleb Jacob and Louis Ameline
5 * MIT license
6 */
7 (function (root, factory) {
8 if (typeof define === 'function' && define.amd) {
9 // AMD. Register as an anonymous module unless amdModuleId is set
10 define(["jquery"], function (a0) {
11 return (factory(a0));
12 });
13 } else if (typeof exports === 'object') {
14 // Node. Does not work with strict CommonJS, but
15 // only CommonJS-like environments that support module.exports,
16 // like Node.
17 module.exports = factory(require("jquery"));
18 } else {
19 factory(jQuery);
20 }
21 }(this, function ($) {
22
23 // This file will be UMDified by a build task.
24
25 var defaults = {
26 animation: 'fade',
27 animationDuration: 350,
28 content: null,
29 contentAsHTML: false,
30 contentCloning: false,
31 debug: true,
32 delay: 300,
33 delayTouch: [300, 500],
34 functionInit: null,
35 functionBefore: null,
36 functionReady: null,
37 functionAfter: null,
38 functionFormat: null,
39 IEmin: 6,
40 interactive: false,
41 multiple: false,
42 // will default to document.body, or must be an element positioned at (0, 0)
43 // in the document, typically like the very top views of an app.
44 parent: null,
45 plugins: ['sideTip'],
46 repositionOnScroll: false,
47 restoration: 'none',
48 selfDestruction: true,
49 theme: [],
50 timer: 0,
51 trackerInterval: 500,
52 trackOrigin: false,
53 trackTooltip: false,
54 trigger: 'hover',
55 triggerClose: {
56 click: false,
57 mouseleave: false,
58 originClick: false,
59 scroll: false,
60 tap: false,
61 touchleave: false
62 },
63 triggerOpen: {
64 click: false,
65 mouseenter: false,
66 tap: false,
67 touchstart: false
68 },
69 updateAnimation: 'rotate',
70 zIndex: 9999999
71 },
72 // we'll avoid using the 'window' global as a good practice but npm's
73 // jquery@<2.1.0 package actually requires a 'window' global, so not sure
74 // it's useful at all
75 win = (typeof window != 'undefined') ? window : null,
76 // env will be proxied by the core for plugins to have access its properties
77 env = {
78 // detect if this device can trigger touch events. Better have a false
79 // positive (unused listeners, that's ok) than a false negative.
80 // https://github.com/Modernizr/Modernizr/blob/master/feature-detects/touchevents.js
81 // http://stackoverflow.com/questions/4817029/whats-the-best-way-to-detect-a-touch-screen-device-using-javascript
82 hasTouchCapability: !!(
83 win
84 && ( 'ontouchstart' in win
85 || (win.DocumentTouch && win.document instanceof win.DocumentTouch)
86 || win.navigator.maxTouchPoints
87 )
88 ),
89 hasTransitions: transitionSupport(),
90 IE: false,
91 // don't set manually, it will be updated by a build task after the manifest
92 semVer: '4.2.7',
93 window: win
94 },
95 core = function() {
96
97 // core variables
98
99 // the core emitters
100 this.__$emitterPrivate = $({});
101 this.__$emitterPublic = $({});
102 this.__instancesLatestArr = [];
103 // collects plugin constructors
104 this.__plugins = {};
105 // proxy env variables for plugins who might use them
106 this._env = env;
107 };
108
109 // core methods
110 core.prototype = {
111
112 /**
113 * A function to proxy the public methods of an object onto another
114 *
115 * @param {object} constructor The constructor to bridge
116 * @param {object} obj The object that will get new methods (an instance or the core)
117 * @param {string} pluginName A plugin name for the console log message
118 * @return {core}
119 * @private
120 */
121 __bridge: function(constructor, obj, pluginName) {
122
123 // if it's not already bridged
124 if (!obj[pluginName]) {
125
126 var fn = function() {};
127 fn.prototype = constructor;
128
129 var pluginInstance = new fn();
130
131 // the _init method has to exist in instance constructors but might be missing
132 // in core constructors
133 if (pluginInstance.__init) {
134 pluginInstance.__init(obj);
135 }
136
137 $.each(constructor, function(methodName, fn) {
138
139 // don't proxy "private" methods, only "protected" and public ones
140 if (methodName.indexOf('__') != 0) {
141
142 // if the method does not exist yet
143 if (!obj[methodName]) {
144
145 obj[methodName] = function() {
146 return pluginInstance[methodName].apply(pluginInstance, Array.prototype.slice.apply(arguments));
147 };
148
149 // remember to which plugin this method corresponds (several plugins may
150 // have methods of the same name, we need to be sure)
151 obj[methodName].bridged = pluginInstance;
152 }
153 else if (defaults.debug) {
154
155 console.log('The '+ methodName +' method of the '+ pluginName
156 +' plugin conflicts with another plugin or native methods');
157 }
158 }
159 });
160
161 obj[pluginName] = pluginInstance;
162 }
163
164 return this;
165 },
166
167 /**
168 * For mockup in Node env if need be, for testing purposes
169 *
170 * @return {core}
171 * @private
172 */
173 __setWindow: function(window) {
174 env.window = window;
175 return this;
176 },
177
178 /**
179 * Returns a ruler, a tool to help measure the size of a tooltip under
180 * various settings. Meant for plugins
181 *
182 * @see Ruler
183 * @return {object} A Ruler instance
184 * @protected
185 */
186 _getRuler: function($tooltip) {
187 return new Ruler($tooltip);
188 },
189
190 /**
191 * For internal use by plugins, if needed
192 *
193 * @return {core}
194 * @protected
195 */
196 _off: function() {
197 this.__$emitterPrivate.off.apply(this.__$emitterPrivate, Array.prototype.slice.apply(arguments));
198 return this;
199 },
200
201 /**
202 * For internal use by plugins, if needed
203 *
204 * @return {core}
205 * @protected
206 */
207 _on: function() {
208 this.__$emitterPrivate.on.apply(this.__$emitterPrivate, Array.prototype.slice.apply(arguments));
209 return this;
210 },
211
212 /**
213 * For internal use by plugins, if needed
214 *
215 * @return {core}
216 * @protected
217 */
218 _one: function() {
219 this.__$emitterPrivate.one.apply(this.__$emitterPrivate, Array.prototype.slice.apply(arguments));
220 return this;
221 },
222
223 /**
224 * Returns (getter) or adds (setter) a plugin
225 *
226 * @param {string|object} plugin Provide a string (in the full form
227 * "namespace.name") to use as as getter, an object to use as a setter
228 * @return {object|core}
229 * @protected
230 */
231 _plugin: function(plugin) {
232
233 var self = this;
234
235 // getter
236 if (typeof plugin == 'string') {
237
238 var pluginName = plugin,
239 p = null;
240
241 // if the namespace is provided, it's easy to search
242 if (pluginName.indexOf('.') > 0) {
243 p = self.__plugins[pluginName];
244 }
245 // otherwise, return the first name that matches
246 else {
247 $.each(self.__plugins, function(i, plugin) {
248
249 if (plugin.name.substring(plugin.name.length - pluginName.length - 1) == '.'+ pluginName) {
250 p = plugin;
251 return false;
252 }
253 });
254 }
255
256 return p;
257 }
258 // setter
259 else {
260
261 // force namespaces
262 if (plugin.name.indexOf('.') < 0) {
263 throw new Error('Plugins must be namespaced');
264 }
265
266 self.__plugins[plugin.name] = plugin;
267
268 // if the plugin has core features
269 if (plugin.core) {
270
271 // bridge non-private methods onto the core to allow new core methods
272 self.__bridge(plugin.core, self, plugin.name);
273 }
274
275 return this;
276 }
277 },
278
279 /**
280 * Trigger events on the core emitters
281 *
282 * @returns {core}
283 * @protected
284 */
285 _trigger: function() {
286
287 var args = Array.prototype.slice.apply(arguments);
288
289 if (typeof args[0] == 'string') {
290 args[0] = { type: args[0] };
291 }
292
293 // note: the order of emitters matters
294 this.__$emitterPrivate.trigger.apply(this.__$emitterPrivate, args);
295 this.__$emitterPublic.trigger.apply(this.__$emitterPublic, args);
296
297 return this;
298 },
299
300 /**
301 * Returns instances of all tooltips in the page or an a given element
302 *
303 * @param {string|HTML object collection} selector optional Use this
304 * parameter to restrict the set of objects that will be inspected
305 * for the retrieval of instances. By default, all instances in the
306 * page are returned.
307 * @return {array} An array of instance objects
308 * @public
309 */
310 instances: function(selector) {
311
312 var instances = [],
313 sel = selector || '.tooltipstered';
314
315 $(sel).each(function() {
316
317 var $this = $(this),
318 ns = $this.data('tooltipster-ns');
319
320 if (ns) {
321
322 $.each(ns, function(i, namespace) {
323 instances.push($this.data(namespace));
324 });
325 }
326 });
327
328 return instances;
329 },
330
331 /**
332 * Returns the Tooltipster objects generated by the last initializing call
333 *
334 * @return {array} An array of instance objects
335 * @public
336 */
337 instancesLatest: function() {
338 return this.__instancesLatestArr;
339 },
340
341 /**
342 * For public use only, not to be used by plugins (use ::_off() instead)
343 *
344 * @return {core}
345 * @public
346 */
347 off: function() {
348 this.__$emitterPublic.off.apply(this.__$emitterPublic, Array.prototype.slice.apply(arguments));
349 return this;
350 },
351
352 /**
353 * For public use only, not to be used by plugins (use ::_on() instead)
354 *
355 * @return {core}
356 * @public
357 */
358 on: function() {
359 this.__$emitterPublic.on.apply(this.__$emitterPublic, Array.prototype.slice.apply(arguments));
360 return this;
361 },
362
363 /**
364 * For public use only, not to be used by plugins (use ::_one() instead)
365 *
366 * @return {core}
367 * @public
368 */
369 one: function() {
370 this.__$emitterPublic.one.apply(this.__$emitterPublic, Array.prototype.slice.apply(arguments));
371 return this;
372 },
373
374 /**
375 * Returns all HTML elements which have one or more tooltips
376 *
377 * @param {string} selector optional Use this to restrict the results
378 * to the descendants of an element
379 * @return {array} An array of HTML elements
380 * @public
381 */
382 origins: function(selector) {
383
384 var sel = selector ?
385 selector +' ' :
386 '';
387
388 return $(sel +'.tooltipstered').toArray();
389 },
390
391 /**
392 * Change default options for all future instances
393 *
394 * @param {object} d The options that should be made defaults
395 * @return {core}
396 * @public
397 */
398 setDefaults: function(d) {
399 $.extend(defaults, d);
400 return this;
401 },
402
403 /**
404 * For users to trigger their handlers on the public emitter
405 *
406 * @returns {core}
407 * @public
408 */
409 triggerHandler: function() {
410 this.__$emitterPublic.triggerHandler.apply(this.__$emitterPublic, Array.prototype.slice.apply(arguments));
411 return this;
412 }
413 };
414
415 // $.tooltipster will be used to call core methods
416 $.tooltipster = new core();
417
418 // the Tooltipster instance class (mind the capital T)
419 $.Tooltipster = function(element, options) {
420
421 // list of instance variables
422
423 // stack of custom callbacks provided as parameters to API methods
424 this.__callbacks = {
425 close: [],
426 open: []
427 };
428 // the schedule time of DOM removal
429 this.__closingTime;
430 // this will be the user content shown in the tooltip. A capital "C" is used
431 // because there is also a method called content()
432 this.__Content;
433 // for the size tracker
434 this.__contentBcr;
435 // to disable the tooltip after destruction
436 this.__destroyed = false;
437 // we can't emit directly on the instance because if a method with the same
438 // name as the event exists, it will be called by jQuery. Se we use a plain
439 // object as emitter. This emitter is for internal use by plugins,
440 // if needed.
441 this.__$emitterPrivate = $({});
442 // this emitter is for the user to listen to events without risking to mess
443 // with our internal listeners
444 this.__$emitterPublic = $({});
445 this.__enabled = true;
446 // the reference to the gc interval
447 this.__garbageCollector;
448 // various position and size data recomputed before each repositioning
449 this.__Geometry;
450 // the tooltip position, saved after each repositioning by a plugin
451 this.__lastPosition;
452 // a unique namespace per instance
453 this.__namespace = 'tooltipster-'+ Math.round(Math.random()*1000000);
454 this.__options;
455 // will be used to support origins in scrollable areas
456 this.__$originParents;
457 this.__pointerIsOverOrigin = false;
458 // to remove themes if needed
459 this.__previousThemes = [];
460 // the state can be either: appearing, stable, disappearing, closed
461 this.__state = 'closed';
462 // timeout references
463 this.__timeouts = {
464 close: [],
465 open: null
466 };
467 // store touch events to be able to detect emulated mouse events
468 this.__touchEvents = [];
469 // the reference to the tracker interval
470 this.__tracker = null;
471 // the element to which this tooltip is associated
472 this._$origin;
473 // this will be the tooltip element (jQuery wrapped HTML element).
474 // It's the job of a plugin to create it and append it to the DOM
475 this._$tooltip;
476
477 // launch
478 this.__init(element, options);
479 };
480
481 $.Tooltipster.prototype = {
482
483 /**
484 * @param origin
485 * @param options
486 * @private
487 */
488 __init: function(origin, options) {
489
490 var self = this;
491
492 self._$origin = $(origin);
493 self.__options = $.extend(true, {}, defaults, options);
494
495 // some options may need to be reformatted
496 self.__optionsFormat();
497
498 // don't run on old IE if asked no to
499 if ( !env.IE
500 || env.IE >= self.__options.IEmin
501 ) {
502
503 // note: the content is null (empty) by default and can stay that
504 // way if the plugin remains initialized but not fed any content. The
505 // tooltip will just not appear.
506
507 // let's save the initial value of the title attribute for later
508 // restoration if need be.
509 var initialTitle = null;
510
511 // it will already have been saved in case of multiple tooltips
512 if (self._$origin.data('tooltipster-initialTitle') === undefined) {
513
514 initialTitle = self._$origin.attr('title');
515
516 // we do not want initialTitle to be "undefined" because
517 // of how jQuery's .data() method works
518 if (initialTitle === undefined) initialTitle = null;
519
520 self._$origin.data('tooltipster-initialTitle', initialTitle);
521 }
522
523 // If content is provided in the options, it has precedence over the
524 // title attribute.
525 // Note: an empty string is considered content, only 'null' represents
526 // the absence of content.
527 // Also, an existing title="" attribute will result in an empty string
528 // content
529 if (self.__options.content !== null) {
530 self.__contentSet(self.__options.content);
531 }
532 else {
533
534 var selector = self._$origin.attr('data-tooltip-content'),
535 $el;
536
537 if (selector){
538 $el = $(selector);
539 }
540
541 if ($el && $el[0]) {
542 self.__contentSet($el.first());
543 }
544 else {
545 self.__contentSet(initialTitle);
546 }
547 }
548
549 self._$origin
550 // strip the title off of the element to prevent the default tooltips
551 // from popping up
552 .removeAttr('title')
553 // to be able to find all instances on the page later (upon window
554 // events in particular)
555 .addClass('tooltipstered');
556
557 // set listeners on the origin
558 self.__prepareOrigin();
559
560 // set the garbage collector
561 self.__prepareGC();
562
563 // init plugins
564 $.each(self.__options.plugins, function(i, pluginName) {
565 self._plug(pluginName);
566 });
567
568 // to detect swiping
569 if (env.hasTouchCapability) {
570 $(env.window.document.body).on('touchmove.'+ self.__namespace +'-triggerOpen', function(event) {
571 self._touchRecordEvent(event);
572 });
573 }
574
575 self
576 // prepare the tooltip when it gets created. This event must
577 // be fired by a plugin
578 ._on('created', function() {
579 self.__prepareTooltip();
580 })
581 // save position information when it's sent by a plugin
582 ._on('repositioned', function(e) {
583 self.__lastPosition = e.position;
584 });
585 }
586 else {
587 self.__options.disabled = true;
588 }
589 },
590
591 /**
592 * Insert the content into the appropriate HTML element of the tooltip
593 *
594 * @returns {self}
595 * @private
596 */
597 __contentInsert: function() {
598
599 var self = this,
600 $el = self._$tooltip.find('.tooltipster-content'),
601 formattedContent = self.__Content,
602 format = function(content) {
603 formattedContent = content;
604 };
605
606 self._trigger({
607 type: 'format',
608 content: self.__Content,
609 format: format
610 });
611
612 if (self.__options.functionFormat) {
613
614 formattedContent = self.__options.functionFormat.call(
615 self,
616 self,
617 { origin: self._$origin[0] },
618 self.__Content
619 );
620 }
621
622 if (typeof formattedContent === 'string' && !self.__options.contentAsHTML) {
623 $el.text(formattedContent);
624 }
625 else {
626 $el
627 .empty()
628 .append(formattedContent);
629 }
630
631 return self;
632 },
633
634 /**
635 * Save the content, cloning it beforehand if need be
636 *
637 * @param content
638 * @returns {self}
639 * @private
640 */
641 __contentSet: function(content) {
642
643 // clone if asked. Cloning the object makes sure that each instance has its
644 // own version of the content (in case a same object were provided for several
645 // instances)
646 // reminder: typeof null === object
647 if (content instanceof $ && this.__options.contentCloning) {
648 content = content.clone(true);
649 }
650
651 this.__Content = content;
652
653 this._trigger({
654 type: 'updated',
655 content: content
656 });
657
658 return this;
659 },
660
661 /**
662 * Error message about a method call made after destruction
663 *
664 * @private
665 */
666 __destroyError: function() {
667 throw new Error('This tooltip has been destroyed and cannot execute your method call.');
668 },
669
670 /**
671 * Gather all information about dimensions and available space,
672 * called before every repositioning
673 *
674 * @private
675 * @returns {object}
676 */
677 __geometry: function() {
678
679 var self = this,
680 $target = self._$origin,
681 originIsArea = self._$origin.is('area');
682
683 // if this._$origin is a map area, the target we'll need
684 // the dimensions of is actually the image using the map,
685 // not the area itself
686 if (originIsArea) {
687
688 var mapName = self._$origin.parent().attr('name');
689
690 $target = $('img[usemap="#'+ mapName +'"]');
691 }
692
693 var bcr = $target[0].getBoundingClientRect(),
694 $document = $(env.window.document),
695 $window = $(env.window),
696 $parent = $target,
697 // some useful properties of important elements
698 geo = {
699 // available space for the tooltip, see down below
700 available: {
701 document: null,
702 window: null
703 },
704 document: {
705 size: {
706 height: $document.height(),
707 width: $document.width()
708 }
709 },
710 window: {
711 scroll: {
712 // the second ones are for IE compatibility
713 left: env.window.scrollX || env.window.document.documentElement.scrollLeft,
714 top: env.window.scrollY || env.window.document.documentElement.scrollTop
715 },
716 size: {
717 height: $window.height(),
718 width: $window.width()
719 }
720 },
721 origin: {
722 // the origin has a fixed lineage if itself or one of its
723 // ancestors has a fixed position
724 fixedLineage: false,
725 // relative to the document
726 offset: {},
727 size: {
728 height: bcr.bottom - bcr.top,
729 width: bcr.right - bcr.left
730 },
731 usemapImage: originIsArea ? $target[0] : null,
732 // relative to the window
733 windowOffset: {
734 bottom: bcr.bottom,
735 left: bcr.left,
736 right: bcr.right,
737 top: bcr.top
738 }
739 }
740 },
741 geoFixed = false;
742
743 // if the element is a map area, some properties may need
744 // to be recalculated
745 if (originIsArea) {
746
747 var shape = self._$origin.attr('shape'),
748 coords = self._$origin.attr('coords');
749
750 if (coords) {
751
752 coords = coords.split(',');
753
754 $.map(coords, function(val, i) {
755 coords[i] = parseInt(val);
756 });
757 }
758
759 // if the image itself is the area, nothing more to do
760 if (shape != 'default') {
761
762 switch(shape) {
763
764 case 'circle':
765
766 var circleCenterLeft = coords[0],
767 circleCenterTop = coords[1],
768 circleRadius = coords[2],
769 areaTopOffset = circleCenterTop - circleRadius,
770 areaLeftOffset = circleCenterLeft - circleRadius;
771
772 geo.origin.size.height = circleRadius * 2;
773 geo.origin.size.width = geo.origin.size.height;
774
775 geo.origin.windowOffset.left += areaLeftOffset;
776 geo.origin.windowOffset.top += areaTopOffset;
777
778 break;
779
780 case 'rect':
781
782 var areaLeft = coords[0],
783 areaTop = coords[1],
784 areaRight = coords[2],
785 areaBottom = coords[3];
786
787 geo.origin.size.height = areaBottom - areaTop;
788 geo.origin.size.width = areaRight - areaLeft;
789
790 geo.origin.windowOffset.left += areaLeft;
791 geo.origin.windowOffset.top += areaTop;
792
793 break;
794
795 case 'poly':
796
797 var areaSmallestX = 0,
798 areaSmallestY = 0,
799 areaGreatestX = 0,
800 areaGreatestY = 0,
801 arrayAlternate = 'even';
802
803 for (var i = 0; i < coords.length; i++) {
804
805 var areaNumber = coords[i];
806
807 if (arrayAlternate == 'even') {
808
809 if (areaNumber > areaGreatestX) {
810
811 areaGreatestX = areaNumber;
812
813 if (i === 0) {
814 areaSmallestX = areaGreatestX;
815 }
816 }
817
818 if (areaNumber < areaSmallestX) {
819 areaSmallestX = areaNumber;
820 }
821
822 arrayAlternate = 'odd';
823 }
824 else {
825 if (areaNumber > areaGreatestY) {
826
827 areaGreatestY = areaNumber;
828
829 if (i == 1) {
830 areaSmallestY = areaGreatestY;
831 }
832 }
833
834 if (areaNumber < areaSmallestY) {
835 areaSmallestY = areaNumber;
836 }
837
838 arrayAlternate = 'even';
839 }
840 }
841
842 geo.origin.size.height = areaGreatestY - areaSmallestY;
843 geo.origin.size.width = areaGreatestX - areaSmallestX;
844
845 geo.origin.windowOffset.left += areaSmallestX;
846 geo.origin.windowOffset.top += areaSmallestY;
847
848 break;
849 }
850 }
851 }
852
853 // user callback through an event
854 var edit = function(r) {
855 geo.origin.size.height = r.height,
856 geo.origin.windowOffset.left = r.left,
857 geo.origin.windowOffset.top = r.top,
858 geo.origin.size.width = r.width
859 };
860
861 self._trigger({
862 type: 'geometry',
863 edit: edit,
864 geometry: {
865 height: geo.origin.size.height,
866 left: geo.origin.windowOffset.left,
867 top: geo.origin.windowOffset.top,
868 width: geo.origin.size.width
869 }
870 });
871
872 // calculate the remaining properties with what we got
873
874 geo.origin.windowOffset.right = geo.origin.windowOffset.left + geo.origin.size.width;
875 geo.origin.windowOffset.bottom = geo.origin.windowOffset.top + geo.origin.size.height;
876
877 geo.origin.offset.left = geo.origin.windowOffset.left + geo.window.scroll.left;
878 geo.origin.offset.top = geo.origin.windowOffset.top + geo.window.scroll.top;
879 geo.origin.offset.bottom = geo.origin.offset.top + geo.origin.size.height;
880 geo.origin.offset.right = geo.origin.offset.left + geo.origin.size.width;
881
882 // the space that is available to display the tooltip relatively to the document
883 geo.available.document = {
884 bottom: {
885 height: geo.document.size.height - geo.origin.offset.bottom,
886 width: geo.document.size.width
887 },
888 left: {
889 height: geo.document.size.height,
890 width: geo.origin.offset.left
891 },
892 right: {
893 height: geo.document.size.height,
894 width: geo.document.size.width - geo.origin.offset.right
895 },
896 top: {
897 height: geo.origin.offset.top,
898 width: geo.document.size.width
899 }
900 };
901
902 // the space that is available to display the tooltip relatively to the viewport
903 // (the resulting values may be negative if the origin overflows the viewport)
904 geo.available.window = {
905 bottom: {
906 // the inner max is here to make sure the available height is no bigger
907 // than the viewport height (when the origin is off screen at the top).
908 // The outer max just makes sure that the height is not negative (when
909 // the origin overflows at the bottom).
910 height: Math.max(geo.window.size.height - Math.max(geo.origin.windowOffset.bottom, 0), 0),
911 width: geo.window.size.width
912 },
913 left: {
914 height: geo.window.size.height,
915 width: Math.max(geo.origin.windowOffset.left, 0)
916 },
917 right: {
918 height: geo.window.size.height,
919 width: Math.max(geo.window.size.width - Math.max(geo.origin.windowOffset.right, 0), 0)
920 },
921 top: {
922 height: Math.max(geo.origin.windowOffset.top, 0),
923 width: geo.window.size.width
924 }
925 };
926
927 while ($parent[0].tagName.toLowerCase() != 'html') {
928
929 if ($parent.css('position') == 'fixed') {
930 geo.origin.fixedLineage = true;
931 break;
932 }
933
934 $parent = $parent.parent();
935 }
936
937 return geo;
938 },
939
940 /**
941 * Some options may need to be formated before being used
942 *
943 * @returns {self}
944 * @private
945 */
946 __optionsFormat: function() {
947
948 if (typeof this.__options.animationDuration == 'number') {
949 this.__options.animationDuration = [this.__options.animationDuration, this.__options.animationDuration];
950 }
951
952 if (typeof this.__options.delay == 'number') {
953 this.__options.delay = [this.__options.delay, this.__options.delay];
954 }
955
956 if (typeof this.__options.delayTouch == 'number') {
957 this.__options.delayTouch = [this.__options.delayTouch, this.__options.delayTouch];
958 }
959
960 if (typeof this.__options.theme == 'string') {
961 this.__options.theme = [this.__options.theme];
962 }
963
964 // determine the future parent
965 if (this.__options.parent === null) {
966 this.__options.parent = $(env.window.document.body);
967 }
968 else if (typeof this.__options.parent == 'string') {
969 this.__options.parent = $(this.__options.parent);
970 }
971
972 if (this.__options.trigger == 'hover') {
973
974 this.__options.triggerOpen = {
975 mouseenter: true,
976 touchstart: true
977 };
978
979 this.__options.triggerClose = {
980 mouseleave: true,
981 originClick: true,
982 touchleave: true
983 };
984 }
985 else if (this.__options.trigger == 'click') {
986
987 this.__options.triggerOpen = {
988 click: true,
989 tap: true
990 };
991
992 this.__options.triggerClose = {
993 click: true,
994 tap: true
995 };
996 }
997
998 // for the plugins
999 this._trigger('options');
1000
1001 return this;
1002 },
1003
1004 /**
1005 * Schedules or cancels the garbage collector task
1006 *
1007 * @returns {self}
1008 * @private
1009 */
1010 __prepareGC: function() {
1011
1012 var self = this;
1013
1014 // in case the selfDestruction option has been changed by a method call
1015 if (self.__options.selfDestruction) {
1016
1017 // the GC task
1018 self.__garbageCollector = setInterval(function() {
1019
1020 var now = new Date().getTime();
1021
1022 // forget the old events
1023 self.__touchEvents = $.grep(self.__touchEvents, function(event, i) {
1024 // 1 minute
1025 return now - event.time > 60000;
1026 });
1027
1028 // auto-destruct if the origin is gone
1029 if (!bodyContains(self._$origin)) {
1030
1031 self.close(function(){
1032 self.destroy();
1033 });
1034 }
1035 }, 20000);
1036 }
1037 else {
1038 clearInterval(self.__garbageCollector);
1039 }
1040
1041 return self;
1042 },
1043
1044 /**
1045 * Sets listeners on the origin if the open triggers require them.
1046 * Unlike the listeners set at opening time, these ones
1047 * remain even when the tooltip is closed. It has been made a
1048 * separate method so it can be called when the triggers are
1049 * changed in the options. Closing is handled in _open()
1050 * because of the bindings that may be needed on the tooltip
1051 * itself
1052 *
1053 * @returns {self}
1054 * @private
1055 */
1056 __prepareOrigin: function() {
1057
1058 var self = this;
1059
1060 // in case we're resetting the triggers
1061 self._$origin.off('.'+ self.__namespace +'-triggerOpen');
1062
1063 // if the device is touch capable, even if only mouse triggers
1064 // are asked, we need to listen to touch events to know if the mouse
1065 // events are actually emulated (so we can ignore them)
1066 if (env.hasTouchCapability) {
1067
1068 self._$origin.on(
1069 'touchstart.'+ self.__namespace +'-triggerOpen ' +
1070 'touchend.'+ self.__namespace +'-triggerOpen ' +
1071 'touchcancel.'+ self.__namespace +'-triggerOpen',
1072 function(event){
1073 self._touchRecordEvent(event);
1074 }
1075 );
1076 }
1077
1078 // mouse click and touch tap work the same way
1079 if ( self.__options.triggerOpen.click
1080 || (self.__options.triggerOpen.tap && env.hasTouchCapability)
1081 ) {
1082
1083 var eventNames = '';
1084 if (self.__options.triggerOpen.click) {
1085 eventNames += 'click.'+ self.__namespace +'-triggerOpen ';
1086 }
1087 if (self.__options.triggerOpen.tap && env.hasTouchCapability) {
1088 eventNames += 'touchend.'+ self.__namespace +'-triggerOpen';
1089 }
1090
1091 self._$origin.on(eventNames, function(event) {
1092 if (self._touchIsMeaningfulEvent(event)) {
1093 self._open(event);
1094 }
1095 });
1096 }
1097
1098 // mouseenter and touch start work the same way
1099 if ( self.__options.triggerOpen.mouseenter
1100 || (self.__options.triggerOpen.touchstart && env.hasTouchCapability)
1101 ) {
1102
1103 var eventNames = '';
1104 if (self.__options.triggerOpen.mouseenter) {
1105 eventNames += 'mouseenter.'+ self.__namespace +'-triggerOpen ';
1106 }
1107 if (self.__options.triggerOpen.touchstart && env.hasTouchCapability) {
1108 eventNames += 'touchstart.'+ self.__namespace +'-triggerOpen';
1109 }
1110
1111 self._$origin.on(eventNames, function(event) {
1112 if ( self._touchIsTouchEvent(event)
1113 || !self._touchIsEmulatedEvent(event)
1114 ) {
1115 self.__pointerIsOverOrigin = true;
1116 self._openShortly(event);
1117 }
1118 });
1119 }
1120
1121 // info for the mouseleave/touchleave close triggers when they use a delay
1122 if ( self.__options.triggerClose.mouseleave
1123 || (self.__options.triggerClose.touchleave && env.hasTouchCapability)
1124 ) {
1125
1126 var eventNames = '';
1127 if (self.__options.triggerClose.mouseleave) {
1128 eventNames += 'mouseleave.'+ self.__namespace +'-triggerOpen ';
1129 }
1130 if (self.__options.triggerClose.touchleave && env.hasTouchCapability) {
1131 eventNames += 'touchend.'+ self.__namespace +'-triggerOpen touchcancel.'+ self.__namespace +'-triggerOpen';
1132 }
1133
1134 self._$origin.on(eventNames, function(event) {
1135
1136 if (self._touchIsMeaningfulEvent(event)) {
1137 self.__pointerIsOverOrigin = false;
1138 }
1139 });
1140 }
1141
1142 return self;
1143 },
1144
1145 /**
1146 * Do the things that need to be done only once after the tooltip
1147 * HTML element it has been created. It has been made a separate
1148 * method so it can be called when options are changed. Remember
1149 * that the tooltip may actually exist in the DOM before it is
1150 * opened, and present after it has been closed: it's the display
1151 * plugin that takes care of handling it.
1152 *
1153 * @returns {self}
1154 * @private
1155 */
1156 __prepareTooltip: function() {
1157
1158 var self = this,
1159 p = self.__options.interactive ? 'auto' : '';
1160
1161 // this will be useful to know quickly if the tooltip is in
1162 // the DOM or not
1163 self._$tooltip
1164 .attr('id', self.__namespace)
1165 .css({
1166 // pointer events
1167 'pointer-events': p,
1168 zIndex: self.__options.zIndex
1169 });
1170
1171 // themes
1172 // remove the old ones and add the new ones
1173 $.each(self.__previousThemes, function(i, theme) {
1174 self._$tooltip.removeClass(theme);
1175 });
1176 $.each(self.__options.theme, function(i, theme) {
1177 self._$tooltip.addClass(theme);
1178 });
1179
1180 self.__previousThemes = $.merge([], self.__options.theme);
1181
1182 return self;
1183 },
1184
1185 /**
1186 * Handles the scroll on any of the parents of the origin (when the
1187 * tooltip is open)
1188 *
1189 * @param {object} event
1190 * @returns {self}
1191 * @private
1192 */
1193 __scrollHandler: function(event) {
1194
1195 var self = this;
1196
1197 if (self.__options.triggerClose.scroll) {
1198 self._close(event);
1199 }
1200 else {
1201
1202 // if the origin or tooltip have been removed: do nothing, the tracker will
1203 // take care of it later
1204 if (bodyContains(self._$origin) && bodyContains(self._$tooltip)) {
1205
1206 var geo = null;
1207
1208 // if the scroll happened on the window
1209 if (event.target === env.window.document) {
1210
1211 // if the origin has a fixed lineage, window scroll will have no
1212 // effect on its position nor on the position of the tooltip
1213 if (!self.__Geometry.origin.fixedLineage) {
1214
1215 // we don't need to do anything unless repositionOnScroll is true
1216 // because the tooltip will already have moved with the window
1217 // (and of course with the origin)
1218 if (self.__options.repositionOnScroll) {
1219 self.reposition(event);
1220 }
1221 }
1222 }
1223 // if the scroll happened on another parent of the tooltip, it means
1224 // that it's in a scrollable area and now needs to have its position
1225 // adjusted or recomputed, depending ont the repositionOnScroll
1226 // option. Also, if the origin is partly hidden due to a parent that
1227 // hides its overflow, we'll just hide (not close) the tooltip.
1228 else {
1229
1230 geo = self.__geometry();
1231
1232 var overflows = false;
1233
1234 // a fixed position origin is not affected by the overflow hiding
1235 // of a parent
1236 if (self._$origin.css('position') != 'fixed') {
1237
1238 self.__$originParents.each(function(i, el) {
1239
1240 var $el = $(el),
1241 overflowX = $el.css('overflow-x'),
1242 overflowY = $el.css('overflow-y');
1243
1244 if (overflowX != 'visible' || overflowY != 'visible') {
1245
1246 var bcr = el.getBoundingClientRect();
1247
1248 if (overflowX != 'visible') {
1249
1250 if ( geo.origin.windowOffset.left < bcr.left
1251 || geo.origin.windowOffset.right > bcr.right
1252 ) {
1253 overflows = true;
1254 return false;
1255 }
1256 }
1257
1258 if (overflowY != 'visible') {
1259
1260 if ( geo.origin.windowOffset.top < bcr.top
1261 || geo.origin.windowOffset.bottom > bcr.bottom
1262 ) {
1263 overflows = true;
1264 return false;
1265 }
1266 }
1267 }
1268
1269 // no need to go further if fixed, for the same reason as above
1270 if ($el.css('position') == 'fixed') {
1271 return false;
1272 }
1273 });
1274 }
1275
1276 if (overflows) {
1277 self._$tooltip.css('visibility', 'hidden');
1278 }
1279 else {
1280
1281 self._$tooltip.css('visibility', 'visible');
1282
1283 // reposition
1284 if (self.__options.repositionOnScroll) {
1285 self.reposition(event);
1286 }
1287 // or just adjust offset
1288 else {
1289
1290 // we have to use offset and not windowOffset because this way,
1291 // only the scroll distance of the scrollable areas are taken into
1292 // account (the scrolltop value of the main window must be
1293 // ignored since the tooltip already moves with it)
1294 var offsetLeft = geo.origin.offset.left - self.__Geometry.origin.offset.left,
1295 offsetTop = geo.origin.offset.top - self.__Geometry.origin.offset.top;
1296
1297 // add the offset to the position initially computed by the display plugin
1298 self._$tooltip.css({
1299 left: self.__lastPosition.coord.left + offsetLeft,
1300 top: self.__lastPosition.coord.top + offsetTop
1301 });
1302 }
1303 }
1304 }
1305
1306 self._trigger({
1307 type: 'scroll',
1308 event: event,
1309 geo: geo
1310 });
1311 }
1312 }
1313
1314 return self;
1315 },
1316
1317 /**
1318 * Changes the state of the tooltip
1319 *
1320 * @param {string} state
1321 * @returns {self}
1322 * @private
1323 */
1324 __stateSet: function(state) {
1325
1326 this.__state = state;
1327
1328 this._trigger({
1329 type: 'state',
1330 state: state
1331 });
1332
1333 return this;
1334 },
1335
1336 /**
1337 * Clear appearance timeouts
1338 *
1339 * @returns {self}
1340 * @private
1341 */
1342 __timeoutsClear: function() {
1343
1344 // there is only one possible open timeout: the delayed opening
1345 // when the mouseenter/touchstart open triggers are used
1346 clearTimeout(this.__timeouts.open);
1347 this.__timeouts.open = null;
1348
1349 // ... but several close timeouts: the delayed closing when the
1350 // mouseleave close trigger is used and the timer option
1351 $.each(this.__timeouts.close, function(i, timeout) {
1352 clearTimeout(timeout);
1353 });
1354 this.__timeouts.close = [];
1355
1356 return this;
1357 },
1358
1359 /**
1360 * Start the tracker that will make checks at regular intervals
1361 *
1362 * @returns {self}
1363 * @private
1364 */
1365 __trackerStart: function() {
1366
1367 var self = this,
1368 $content = self._$tooltip.find('.tooltipster-content');
1369
1370 // get the initial content size
1371 if (self.__options.trackTooltip) {
1372 self.__contentBcr = $content[0].getBoundingClientRect();
1373 }
1374
1375 self.__tracker = setInterval(function() {
1376
1377 // if the origin or tooltip elements have been removed.
1378 // Note: we could destroy the instance now if the origin has
1379 // been removed but we'll leave that task to our garbage collector
1380 if (!bodyContains(self._$origin) || !bodyContains(self._$tooltip)) {
1381 self._close();
1382 }
1383 // if everything is alright
1384 else {
1385
1386 // compare the former and current positions of the origin to reposition
1387 // the tooltip if need be
1388 if (self.__options.trackOrigin) {
1389
1390 var g = self.__geometry(),
1391 identical = false;
1392
1393 // compare size first (a change requires repositioning too)
1394 if (areEqual(g.origin.size, self.__Geometry.origin.size)) {
1395
1396 // for elements that have a fixed lineage (see __geometry()), we track the
1397 // top and left properties (relative to window)
1398 if (self.__Geometry.origin.fixedLineage) {
1399 if (areEqual(g.origin.windowOffset, self.__Geometry.origin.windowOffset)) {
1400 identical = true;
1401 }
1402 }
1403 // otherwise, track total offset (relative to document)
1404 else {
1405 if (areEqual(g.origin.offset, self.__Geometry.origin.offset)) {
1406 identical = true;
1407 }
1408 }
1409 }
1410
1411 if (!identical) {
1412
1413 // close the tooltip when using the mouseleave close trigger
1414 // (see https://github.com/iamceege/tooltipster/pull/253)
1415 if (self.__options.triggerClose.mouseleave) {
1416 self._close();
1417 }
1418 else {
1419 self.reposition();
1420 }
1421 }
1422 }
1423
1424 if (self.__options.trackTooltip) {
1425
1426 var currentBcr = $content[0].getBoundingClientRect();
1427
1428 if ( currentBcr.height !== self.__contentBcr.height
1429 || currentBcr.width !== self.__contentBcr.width
1430 ) {
1431 self.reposition();
1432 self.__contentBcr = currentBcr;
1433 }
1434 }
1435 }
1436 }, self.__options.trackerInterval);
1437
1438 return self;
1439 },
1440
1441 /**
1442 * Closes the tooltip (after the closing delay)
1443 *
1444 * @param event
1445 * @param callback
1446 * @param force Set to true to override a potential refusal of the user's function
1447 * @returns {self}
1448 * @protected
1449 */
1450 _close: function(event, callback, force) {
1451
1452 var self = this,
1453 ok = true;
1454
1455 self._trigger({
1456 type: 'close',
1457 event: event,
1458 stop: function() {
1459 ok = false;
1460 }
1461 });
1462
1463 // a destroying tooltip (force == true) may not refuse to close
1464 if (ok || force) {
1465
1466 // save the method custom callback and cancel any open method custom callbacks
1467 if (callback) self.__callbacks.close.push(callback);
1468 self.__callbacks.open = [];
1469
1470 // clear open/close timeouts
1471 self.__timeoutsClear();
1472
1473 var finishCallbacks = function() {
1474
1475 // trigger any close method custom callbacks and reset them
1476 $.each(self.__callbacks.close, function(i,c) {
1477 c.call(self, self, {
1478 event: event,
1479 origin: self._$origin[0]
1480 });
1481 });
1482
1483 self.__callbacks.close = [];
1484 };
1485
1486 if (self.__state != 'closed') {
1487
1488 var necessary = true,
1489 d = new Date(),
1490 now = d.getTime(),
1491 newClosingTime = now + self.__options.animationDuration[1];
1492
1493 // the tooltip may already already be disappearing, but if a new
1494 // call to close() is made after the animationDuration was changed
1495 // to 0 (for example), we ought to actually close it sooner than
1496 // previously scheduled. In that case it should be noted that the
1497 // browser will not adapt the animation duration to the new
1498 // animationDuration that was set after the start of the closing
1499 // animation.
1500 // Note: the same thing could be considered at opening, but is not
1501 // really useful since the tooltip is actually opened immediately
1502 // upon a call to _open(). Since it would not make the opening
1503 // animation finish sooner, its sole impact would be to trigger the
1504 // state event and the open callbacks sooner than the actual end of
1505 // the opening animation, which is not great.
1506 if (self.__state == 'disappearing') {
1507
1508 if ( newClosingTime > self.__closingTime
1509 // in case closing is actually overdue because the script
1510 // execution was suspended. See #679
1511 && self.__options.animationDuration[1] > 0
1512 ) {
1513 necessary = false;
1514 }
1515 }
1516
1517 if (necessary) {
1518
1519 self.__closingTime = newClosingTime;
1520
1521 if (self.__state != 'disappearing') {
1522 self.__stateSet('disappearing');
1523 }
1524
1525 var finish = function() {
1526
1527 // stop the tracker
1528 clearInterval(self.__tracker);
1529
1530 // a "beforeClose" option has been asked several times but would
1531 // probably useless since the content element is still accessible
1532 // via ::content(), and because people can always use listeners
1533 // inside their content to track what's going on. For the sake of
1534 // simplicity, this has been denied. Bur for the rare people who
1535 // really need the option (for old browsers or for the case where
1536 // detaching the content is actually destructive, for file or
1537 // password inputs for example), this event will do the work.
1538 self._trigger({
1539 type: 'closing',
1540 event: event
1541 });
1542
1543 // unbind listeners which are no longer needed
1544
1545 self._$tooltip
1546 .off('.'+ self.__namespace +'-triggerClose')
1547 .removeClass('tooltipster-dying');
1548
1549 // orientationchange, scroll and resize listeners
1550 $(env.window).off('.'+ self.__namespace +'-triggerClose');
1551
1552 // scroll listeners
1553 self.__$originParents.each(function(i, el) {
1554 $(el).off('scroll.'+ self.__namespace +'-triggerClose');
1555 });
1556 // clear the array to prevent memory leaks
1557 self.__$originParents = null;
1558
1559 $(env.window.document.body).off('.'+ self.__namespace +'-triggerClose');
1560
1561 self._$origin.off('.'+ self.__namespace +'-triggerClose');
1562
1563 self._off('dismissable');
1564
1565 // a plugin that would like to remove the tooltip from the
1566 // DOM when closed should bind on this
1567 self.__stateSet('closed');
1568
1569 // trigger event
1570 self._trigger({
1571 type: 'after',
1572 event: event
1573 });
1574
1575 // call our constructor custom callback function
1576 if (self.__options.functionAfter) {
1577 self.__options.functionAfter.call(self, self, {
1578 event: event,
1579 origin: self._$origin[0]
1580 });
1581 }
1582
1583 // call our method custom callbacks functions
1584 finishCallbacks();
1585 };
1586
1587 if (env.hasTransitions) {
1588
1589 self._$tooltip.css({
1590 '-moz-animation-duration': self.__options.animationDuration[1] + 'ms',
1591 '-ms-animation-duration': self.__options.animationDuration[1] + 'ms',
1592 '-o-animation-duration': self.__options.animationDuration[1] + 'ms',
1593 '-webkit-animation-duration': self.__options.animationDuration[1] + 'ms',
1594 'animation-duration': self.__options.animationDuration[1] + 'ms',
1595 'transition-duration': self.__options.animationDuration[1] + 'ms'
1596 });
1597
1598 self._$tooltip
1599 // clear both potential open and close tasks
1600 .clearQueue()
1601 .removeClass('tooltipster-show')
1602 // for transitions only
1603 .addClass('tooltipster-dying');
1604
1605 if (self.__options.animationDuration[1] > 0) {
1606 self._$tooltip.delay(self.__options.animationDuration[1]);
1607 }
1608
1609 self._$tooltip.queue(finish);
1610 }
1611 else {
1612
1613 self._$tooltip
1614 .stop()
1615 .fadeOut(self.__options.animationDuration[1], finish);
1616 }
1617 }
1618 }
1619 // if the tooltip is already closed, we still need to trigger
1620 // the method custom callbacks
1621 else {
1622 finishCallbacks();
1623 }
1624 }
1625
1626 return self;
1627 },
1628
1629 /**
1630 * For internal use by plugins, if needed
1631 *
1632 * @returns {self}
1633 * @protected
1634 */
1635 _off: function() {
1636 this.__$emitterPrivate.off.apply(this.__$emitterPrivate, Array.prototype.slice.apply(arguments));
1637 return this;
1638 },
1639
1640 /**
1641 * For internal use by plugins, if needed
1642 *
1643 * @returns {self}
1644 * @protected
1645 */
1646 _on: function() {
1647 this.__$emitterPrivate.on.apply(this.__$emitterPrivate, Array.prototype.slice.apply(arguments));
1648 return this;
1649 },
1650
1651 /**
1652 * For internal use by plugins, if needed
1653 *
1654 * @returns {self}
1655 * @protected
1656 */
1657 _one: function() {
1658 this.__$emitterPrivate.one.apply(this.__$emitterPrivate, Array.prototype.slice.apply(arguments));
1659 return this;
1660 },
1661
1662 /**
1663 * Opens the tooltip right away.
1664 *
1665 * @param event
1666 * @param callback Will be called when the opening animation is over
1667 * @returns {self}
1668 * @protected
1669 */
1670 _open: function(event, callback) {
1671
1672 var self = this;
1673
1674 // if the destruction process has not begun and if this was not
1675 // triggered by an unwanted emulated click event
1676 if (!self.__destroying) {
1677
1678 // check that the origin is still in the DOM
1679 if ( bodyContains(self._$origin)
1680 // if the tooltip is enabled
1681 && self.__enabled
1682 ) {
1683
1684 var ok = true;
1685
1686 // if the tooltip is not open yet, we need to call functionBefore.
1687 // otherwise we can jst go on
1688 if (self.__state == 'closed') {
1689
1690 // trigger an event. The event.stop function allows the callback
1691 // to prevent the opening of the tooltip
1692 self._trigger({
1693 type: 'before',
1694 event: event,
1695 stop: function() {
1696 ok = false;
1697 }
1698 });
1699
1700 if (ok && self.__options.functionBefore) {
1701
1702 // call our custom function before continuing
1703 ok = self.__options.functionBefore.call(self, self, {
1704 event: event,
1705 origin: self._$origin[0]
1706 });
1707 }
1708 }
1709
1710 if (ok !== false) {
1711
1712 // if there is some content
1713 if (self.__Content !== null) {
1714
1715 // save the method callback and cancel close method callbacks
1716 if (callback) {
1717 self.__callbacks.open.push(callback);
1718 }
1719 self.__callbacks.close = [];
1720
1721 // get rid of any appearance timeouts
1722 self.__timeoutsClear();
1723
1724 var extraTime,
1725 finish = function() {
1726
1727 if (self.__state != 'stable') {
1728 self.__stateSet('stable');
1729 }
1730
1731 // trigger any open method custom callbacks and reset them
1732 $.each(self.__callbacks.open, function(i,c) {
1733 c.call(self, self, {
1734 origin: self._$origin[0],
1735 tooltip: self._$tooltip[0]
1736 });
1737 });
1738
1739 self.__callbacks.open = [];
1740 };
1741
1742 // if the tooltip is already open
1743 if (self.__state !== 'closed') {
1744
1745 // the timer (if any) will start (or restart) right now
1746 extraTime = 0;
1747
1748 // if it was disappearing, cancel that
1749 if (self.__state === 'disappearing') {
1750
1751 self.__stateSet('appearing');
1752
1753 if (env.hasTransitions) {
1754
1755 self._$tooltip
1756 .clearQueue()
1757 .removeClass('tooltipster-dying')
1758 .addClass('tooltipster-show');
1759
1760 if (self.__options.animationDuration[0] > 0) {
1761 self._$tooltip.delay(self.__options.animationDuration[0]);
1762 }
1763
1764 self._$tooltip.queue(finish);
1765 }
1766 else {
1767 // in case the tooltip was currently fading out, bring it back
1768 // to life
1769 self._$tooltip
1770 .stop()
1771 .fadeIn(finish);
1772 }
1773 }
1774 // if the tooltip is already open, we still need to trigger the method
1775 // custom callback
1776 else if (self.__state == 'stable') {
1777 finish();
1778 }
1779 }
1780 // if the tooltip isn't already open, open it
1781 else {
1782
1783 // a plugin must bind on this and store the tooltip in this._$tooltip
1784 self.__stateSet('appearing');
1785
1786 // the timer (if any) will start when the tooltip has fully appeared
1787 // after its transition
1788 extraTime = self.__options.animationDuration[0];
1789
1790 // insert the content inside the tooltip
1791 self.__contentInsert();
1792
1793 // reposition the tooltip and attach to the DOM
1794 self.reposition(event, true);
1795
1796 // animate in the tooltip. If the display plugin wants no css
1797 // animations, it may override the animation option with a
1798 // dummy value that will produce no effect
1799 if (env.hasTransitions) {
1800
1801 // note: there seems to be an issue with start animations which
1802 // are randomly not played on fast devices in both Chrome and FF,
1803 // couldn't find a way to solve it yet. It seems that applying
1804 // the classes before appending to the DOM helps a little, but
1805 // it messes up some CSS transitions. The issue almost never
1806 // happens when delay[0]==0 though
1807 self._$tooltip
1808 .addClass('tooltipster-'+ self.__options.animation)
1809 .addClass('tooltipster-initial')
1810 .css({
1811 '-moz-animation-duration': self.__options.animationDuration[0] + 'ms',
1812 '-ms-animation-duration': self.__options.animationDuration[0] + 'ms',
1813 '-o-animation-duration': self.__options.animationDuration[0] + 'ms',
1814 '-webkit-animation-duration': self.__options.animationDuration[0] + 'ms',
1815 'animation-duration': self.__options.animationDuration[0] + 'ms',
1816 'transition-duration': self.__options.animationDuration[0] + 'ms'
1817 });
1818
1819 setTimeout(
1820 function() {
1821
1822 // a quick hover may have already triggered a mouseleave
1823 if (self.__state != 'closed') {
1824
1825 self._$tooltip
1826 .addClass('tooltipster-show')
1827 .removeClass('tooltipster-initial');
1828
1829 if (self.__options.animationDuration[0] > 0) {
1830 self._$tooltip.delay(self.__options.animationDuration[0]);
1831 }
1832
1833 self._$tooltip.queue(finish);
1834 }
1835 },
1836 0
1837 );
1838 }
1839 else {
1840
1841 // old browsers will have to live with this
1842 self._$tooltip
1843 .css('display', 'none')
1844 .fadeIn(self.__options.animationDuration[0], finish);
1845 }
1846
1847 // checks if the origin is removed while the tooltip is open
1848 self.__trackerStart();
1849
1850 // NOTE: the listeners below have a '-triggerClose' namespace
1851 // because we'll remove them when the tooltip closes (unlike
1852 // the '-triggerOpen' listeners). So some of them are actually
1853 // not about close triggers, rather about positioning.
1854
1855 $(env.window)
1856 // reposition on resize
1857 .on('resize.'+ self.__namespace +'-triggerClose', function(e) {
1858
1859 var $ae = $(document.activeElement);
1860
1861 // reposition only if the resize event was not triggered upon the opening
1862 // of a virtual keyboard due to an input field being focused within the tooltip
1863 // (otherwise the repositioning would lose the focus)
1864 if ( (!$ae.is('input') && !$ae.is('textarea'))
1865 || !$.contains(self._$tooltip[0], $ae[0])
1866 ) {
1867 self.reposition(e);
1868 }
1869 })
1870 // same as below for parents
1871 .on('scroll.'+ self.__namespace +'-triggerClose', function(e) {
1872 self.__scrollHandler(e);
1873 });
1874
1875 self.__$originParents = self._$origin.parents();
1876
1877 // scrolling may require the tooltip to be moved or even
1878 // repositioned in some cases
1879 self.__$originParents.each(function(i, parent) {
1880
1881 $(parent).on('scroll.'+ self.__namespace +'-triggerClose', function(e) {
1882 self.__scrollHandler(e);
1883 });
1884 });
1885
1886 if ( self.__options.triggerClose.mouseleave
1887 || (self.__options.triggerClose.touchleave && env.hasTouchCapability)
1888 ) {
1889
1890 // we use an event to allow users/plugins to control when the mouseleave/touchleave
1891 // close triggers will come to action. It allows to have more triggering elements
1892 // than just the origin and the tooltip for example, or to cancel/delay the closing,
1893 // or to make the tooltip interactive even if it wasn't when it was open, etc.
1894 self._on('dismissable', function(event) {
1895
1896 if (event.dismissable) {
1897
1898 if (event.delay) {
1899
1900 timeout = setTimeout(function() {
1901 // event.event may be undefined
1902 self._close(event.event);
1903 }, event.delay);
1904
1905 self.__timeouts.close.push(timeout);
1906 }
1907 else {
1908 self._close(event);
1909 }
1910 }
1911 else {
1912 clearTimeout(timeout);
1913 }
1914 });
1915
1916 // now set the listeners that will trigger 'dismissable' events
1917 var $elements = self._$origin,
1918 eventNamesIn = '',
1919 eventNamesOut = '',
1920 timeout = null;
1921
1922 // if we have to allow interaction, bind on the tooltip too
1923 if (self.__options.interactive) {
1924 $elements = $elements.add(self._$tooltip);
1925 }
1926
1927 if (self.__options.triggerClose.mouseleave) {
1928 eventNamesIn += 'mouseenter.'+ self.__namespace +'-triggerClose ';
1929 eventNamesOut += 'mouseleave.'+ self.__namespace +'-triggerClose ';
1930 }
1931 if (self.__options.triggerClose.touchleave && env.hasTouchCapability) {
1932 eventNamesIn += 'touchstart.'+ self.__namespace +'-triggerClose';
1933 eventNamesOut += 'touchend.'+ self.__namespace +'-triggerClose touchcancel.'+ self.__namespace +'-triggerClose';
1934 }
1935
1936 $elements
1937 // close after some time spent outside of the elements
1938 .on(eventNamesOut, function(event) {
1939
1940 // it's ok if the touch gesture ended up to be a swipe,
1941 // it's still a "touch leave" situation
1942 if ( self._touchIsTouchEvent(event)
1943 || !self._touchIsEmulatedEvent(event)
1944 ) {
1945
1946 var delay = (event.type == 'mouseleave') ?
1947 self.__options.delay :
1948 self.__options.delayTouch;
1949
1950 self._trigger({
1951 delay: delay[1],
1952 dismissable: true,
1953 event: event,
1954 type: 'dismissable'
1955 });
1956 }
1957 })
1958 // suspend the mouseleave timeout when the pointer comes back
1959 // over the elements
1960 .on(eventNamesIn, function(event) {
1961
1962 // it's also ok if the touch event is a swipe gesture
1963 if ( self._touchIsTouchEvent(event)
1964 || !self._touchIsEmulatedEvent(event)
1965 ) {
1966 self._trigger({
1967 dismissable: false,
1968 event: event,
1969 type: 'dismissable'
1970 });
1971 }
1972 });
1973 }
1974
1975 // close the tooltip when the origin gets a mouse click (common behavior of
1976 // native tooltips)
1977 if (self.__options.triggerClose.originClick) {
1978
1979 self._$origin.on('click.'+ self.__namespace + '-triggerClose', function(event) {
1980
1981 // we could actually let a tap trigger this but this feature just
1982 // does not make sense on touch devices
1983 if ( !self._touchIsTouchEvent(event)
1984 && !self._touchIsEmulatedEvent(event)
1985 ) {
1986 self._close(event);
1987 }
1988 });
1989 }
1990
1991 // set the same bindings for click and touch on the body to close the tooltip
1992 if ( self.__options.triggerClose.click
1993 || (self.__options.triggerClose.tap && env.hasTouchCapability)
1994 ) {
1995
1996 // don't set right away since the click/tap event which triggered this method
1997 // (if it was a click/tap) is going to bubble up to the body, we don't want it
1998 // to close the tooltip immediately after it opened
1999 setTimeout(function() {
2000
2001 if (self.__state != 'closed') {
2002
2003 var eventNames = '',
2004 $body = $(env.window.document.body);
2005
2006 if (self.__options.triggerClose.click) {
2007 eventNames += 'click.'+ self.__namespace +'-triggerClose ';
2008 }
2009 if (self.__options.triggerClose.tap && env.hasTouchCapability) {
2010 eventNames += 'touchend.'+ self.__namespace +'-triggerClose';
2011 }
2012
2013 $body.on(eventNames, function(event) {
2014
2015 if (self._touchIsMeaningfulEvent(event)) {
2016
2017 self._touchRecordEvent(event);
2018
2019 if (!self.__options.interactive || !$.contains(self._$tooltip[0], event.target)) {
2020 self._close(event);
2021 }
2022 }
2023 });
2024
2025 // needed to detect and ignore swiping
2026 if (self.__options.triggerClose.tap && env.hasTouchCapability) {
2027
2028 $body.on('touchstart.'+ self.__namespace +'-triggerClose', function(event) {
2029 self._touchRecordEvent(event);
2030 });
2031 }
2032 }
2033 }, 0);
2034 }
2035
2036 self._trigger('ready');
2037
2038 // call our custom callback
2039 if (self.__options.functionReady) {
2040 self.__options.functionReady.call(self, self, {
2041 origin: self._$origin[0],
2042 tooltip: self._$tooltip[0]
2043 });
2044 }
2045 }
2046
2047 // if we have a timer set, let the countdown begin
2048 if (self.__options.timer > 0) {
2049
2050 var timeout = setTimeout(function() {
2051 self._close();
2052 }, self.__options.timer + extraTime);
2053
2054 self.__timeouts.close.push(timeout);
2055 }
2056 }
2057 }
2058 }
2059 }
2060
2061 return self;
2062 },
2063
2064 /**
2065 * When using the mouseenter/touchstart open triggers, this function will
2066 * schedule the opening of the tooltip after the delay, if there is one
2067 *
2068 * @param event
2069 * @returns {self}
2070 * @protected
2071 */
2072 _openShortly: function(event) {
2073
2074 var self = this,
2075 ok = true;
2076
2077 if (self.__state != 'stable' && self.__state != 'appearing') {
2078
2079 // if a timeout is not already running
2080 if (!self.__timeouts.open) {
2081
2082 self._trigger({
2083 type: 'start',
2084 event: event,
2085 stop: function() {
2086 ok = false;
2087 }
2088 });
2089
2090 if (ok) {
2091
2092 var delay = (event.type.indexOf('touch') == 0) ?
2093 self.__options.delayTouch :
2094 self.__options.delay;
2095
2096 if (delay[0]) {
2097
2098 self.__timeouts.open = setTimeout(function() {
2099
2100 self.__timeouts.open = null;
2101
2102 // open only if the pointer (mouse or touch) is still over the origin.
2103 // The check on the "meaningful event" can only be made here, after some
2104 // time has passed (to know if the touch was a swipe or not)
2105 if (self.__pointerIsOverOrigin && self._touchIsMeaningfulEvent(event)) {
2106
2107 // signal that we go on
2108 self._trigger('startend');
2109
2110 self._open(event);
2111 }
2112 else {
2113 // signal that we cancel
2114 self._trigger('startcancel');
2115 }
2116 }, delay[0]);
2117 }
2118 else {
2119 // signal that we go on
2120 self._trigger('startend');
2121
2122 self._open(event);
2123 }
2124 }
2125 }
2126 }
2127
2128 return self;
2129 },
2130
2131 /**
2132 * Meant for plugins to get their options
2133 *
2134 * @param {string} pluginName The name of the plugin that asks for its options
2135 * @param {object} defaultOptions The default options of the plugin
2136 * @returns {object} The options
2137 * @protected
2138 */
2139 _optionsExtract: function(pluginName, defaultOptions) {
2140
2141 var self = this,
2142 options = $.extend(true, {}, defaultOptions);
2143
2144 // if the plugin options were isolated in a property named after the
2145 // plugin, use them (prevents conflicts with other plugins)
2146 var pluginOptions = self.__options[pluginName];
2147
2148 // if not, try to get them as regular options
2149 if (!pluginOptions){
2150
2151 pluginOptions = {};
2152
2153 $.each(defaultOptions, function(optionName, value) {
2154
2155 var o = self.__options[optionName];
2156
2157 if (o !== undefined) {
2158 pluginOptions[optionName] = o;
2159 }
2160 });
2161 }
2162
2163 // let's merge the default options and the ones that were provided. We'd want
2164 // to do a deep copy but not let jQuery merge arrays, so we'll do a shallow
2165 // extend on two levels, that will be enough if options are not more than 1
2166 // level deep
2167 $.each(options, function(optionName, value) {
2168
2169 if (pluginOptions[optionName] !== undefined) {
2170
2171 if (( typeof value == 'object'
2172 && !(value instanceof Array)
2173 && value != null
2174 )
2175 &&
2176 ( typeof pluginOptions[optionName] == 'object'
2177 && !(pluginOptions[optionName] instanceof Array)
2178 && pluginOptions[optionName] != null
2179 )
2180 ) {
2181 $.extend(options[optionName], pluginOptions[optionName]);
2182 }
2183 else {
2184 options[optionName] = pluginOptions[optionName];
2185 }
2186 }
2187 });
2188
2189 return options;
2190 },
2191
2192 /**
2193 * Used at instantiation of the plugin, or afterwards by plugins that activate themselves
2194 * on existing instances
2195 *
2196 * @param {object} pluginName
2197 * @returns {self}
2198 * @protected
2199 */
2200 _plug: function(pluginName) {
2201
2202 var plugin = $.tooltipster._plugin(pluginName);
2203
2204 if (plugin) {
2205
2206 // if there is a constructor for instances
2207 if (plugin.instance) {
2208
2209 // proxy non-private methods on the instance to allow new instance methods
2210 $.tooltipster.__bridge(plugin.instance, this, plugin.name);
2211 }
2212 }
2213 else {
2214 throw new Error('The "'+ pluginName +'" plugin is not defined');
2215 }
2216
2217 return this;
2218 },
2219
2220 /**
2221 * This will return true if the event is a mouse event which was
2222 * emulated by the browser after a touch event. This allows us to
2223 * really dissociate mouse and touch triggers.
2224 *
2225 * There is a margin of error if a real mouse event is fired right
2226 * after (within the delay shown below) a touch event on the same
2227 * element, but hopefully it should not happen often.
2228 *
2229 * @returns {boolean}
2230 * @protected
2231 */
2232 _touchIsEmulatedEvent: function(event) {
2233
2234 var isEmulated = false,
2235 now = new Date().getTime();
2236
2237 for (var i = this.__touchEvents.length - 1; i >= 0; i--) {
2238
2239 var e = this.__touchEvents[i];
2240
2241 // delay, in milliseconds. It's supposed to be 300ms in
2242 // most browsers (350ms on iOS) to allow a double tap but
2243 // can be less (check out FastClick for more info)
2244 if (now - e.time < 500) {
2245
2246 if (e.target === event.target) {
2247 isEmulated = true;
2248 }
2249 }
2250 else {
2251 break;
2252 }
2253 }
2254
2255 return isEmulated;
2256 },
2257
2258 /**
2259 * Returns false if the event was an emulated mouse event or
2260 * a touch event involved in a swipe gesture.
2261 *
2262 * @param {object} event
2263 * @returns {boolean}
2264 * @protected
2265 */
2266 _touchIsMeaningfulEvent: function(event) {
2267 return (
2268 (this._touchIsTouchEvent(event) && !this._touchSwiped(event.target))
2269 || (!this._touchIsTouchEvent(event) && !this._touchIsEmulatedEvent(event))
2270 );
2271 },
2272
2273 /**
2274 * Checks if an event is a touch event
2275 *
2276 * @param {object} event
2277 * @returns {boolean}
2278 * @protected
2279 */
2280 _touchIsTouchEvent: function(event){
2281 return event.type.indexOf('touch') == 0;
2282 },
2283
2284 /**
2285 * Store touch events for a while to detect swiping and emulated mouse events
2286 *
2287 * @param {object} event
2288 * @returns {self}
2289 * @protected
2290 */
2291 _touchRecordEvent: function(event) {
2292
2293 if (this._touchIsTouchEvent(event)) {
2294 event.time = new Date().getTime();
2295 this.__touchEvents.push(event);
2296 }
2297
2298 return this;
2299 },
2300
2301 /**
2302 * Returns true if a swipe happened after the last touchstart event fired on
2303 * event.target.
2304 *
2305 * We need to differentiate a swipe from a tap before we let the event open
2306 * or close the tooltip. A swipe is when a touchmove (scroll) event happens
2307 * on the body between the touchstart and the touchend events of an element.
2308 *
2309 * @param {object} target The HTML element that may have triggered the swipe
2310 * @returns {boolean}
2311 * @protected
2312 */
2313 _touchSwiped: function(target) {
2314
2315 var swiped = false;
2316
2317 for (var i = this.__touchEvents.length - 1; i >= 0; i--) {
2318
2319 var e = this.__touchEvents[i];
2320
2321 if (e.type == 'touchmove') {
2322 swiped = true;
2323 break;
2324 }
2325 else if (
2326 e.type == 'touchstart'
2327 && target === e.target
2328 ) {
2329 break;
2330 }
2331 }
2332
2333 return swiped;
2334 },
2335
2336 /**
2337 * Triggers an event on the instance emitters
2338 *
2339 * @returns {self}
2340 * @protected
2341 */
2342 _trigger: function() {
2343
2344 var args = Array.prototype.slice.apply(arguments);
2345
2346 if (typeof args[0] == 'string') {
2347 args[0] = { type: args[0] };
2348 }
2349
2350 // add properties to the event
2351 args[0].instance = this;
2352 args[0].origin = this._$origin ? this._$origin[0] : null;
2353 args[0].tooltip = this._$tooltip ? this._$tooltip[0] : null;
2354
2355 // note: the order of emitters matters
2356 this.__$emitterPrivate.trigger.apply(this.__$emitterPrivate, args);
2357 $.tooltipster._trigger.apply($.tooltipster, args);
2358 this.__$emitterPublic.trigger.apply(this.__$emitterPublic, args);
2359
2360 return this;
2361 },
2362
2363 /**
2364 * Deactivate a plugin on this instance
2365 *
2366 * @returns {self}
2367 * @protected
2368 */
2369 _unplug: function(pluginName) {
2370
2371 var self = this;
2372
2373 // if the plugin has been activated on this instance
2374 if (self[pluginName]) {
2375
2376 var plugin = $.tooltipster._plugin(pluginName);
2377
2378 // if there is a constructor for instances
2379 if (plugin.instance) {
2380
2381 // unbridge
2382 $.each(plugin.instance, function(methodName, fn) {
2383
2384 // if the method exists (privates methods do not) and comes indeed from
2385 // this plugin (may be missing or come from a conflicting plugin).
2386 if ( self[methodName]
2387 && self[methodName].bridged === self[pluginName]
2388 ) {
2389 delete self[methodName];
2390 }
2391 });
2392 }
2393
2394 // destroy the plugin
2395 if (self[pluginName].__destroy) {
2396 self[pluginName].__destroy();
2397 }
2398
2399 // remove the reference to the plugin instance
2400 delete self[pluginName];
2401 }
2402
2403 return self;
2404 },
2405
2406 /**
2407 * @see self::_close
2408 * @returns {self}
2409 * @public
2410 */
2411 close: function(callback) {
2412
2413 if (!this.__destroyed) {
2414 this._close(null, callback);
2415 }
2416 else {
2417 this.__destroyError();
2418 }
2419
2420 return this;
2421 },
2422
2423 /**
2424 * Sets or gets the content of the tooltip
2425 *
2426 * @returns {mixed|self}
2427 * @public
2428 */
2429 content: function(content) {
2430
2431 var self = this;
2432
2433 // getter method
2434 if (content === undefined) {
2435 return self.__Content;
2436 }
2437 // setter method
2438 else {
2439
2440 if (!self.__destroyed) {
2441
2442 // change the content
2443 self.__contentSet(content);
2444
2445 if (self.__Content !== null) {
2446
2447 // update the tooltip if it is open
2448 if (self.__state !== 'closed') {
2449
2450 // reset the content in the tooltip
2451 self.__contentInsert();
2452
2453 // reposition and resize the tooltip
2454 self.reposition();
2455
2456 // if we want to play a little animation showing the content changed
2457 if (self.__options.updateAnimation) {
2458
2459 if (env.hasTransitions) {
2460
2461 // keep the reference in the local scope
2462 var animation = self.__options.updateAnimation;
2463
2464 self._$tooltip.addClass('tooltipster-update-'+ animation);
2465
2466 // remove the class after a while. The actual duration of the
2467 // update animation may be shorter, it's set in the CSS rules
2468 setTimeout(function() {
2469
2470 if (self.__state != 'closed') {
2471
2472 self._$tooltip.removeClass('tooltipster-update-'+ animation);
2473 }
2474 }, 1000);
2475 }
2476 else {
2477 self._$tooltip.fadeTo(200, 0.5, function() {
2478 if (self.__state != 'closed') {
2479 self._$tooltip.fadeTo(200, 1);
2480 }
2481 });
2482 }
2483 }
2484 }
2485 }
2486 else {
2487 self._close();
2488 }
2489 }
2490 else {
2491 self.__destroyError();
2492 }
2493
2494 return self;
2495 }
2496 },
2497
2498 /**
2499 * Destroys the tooltip
2500 *
2501 * @returns {self}
2502 * @public
2503 */
2504 destroy: function() {
2505
2506 var self = this;
2507
2508 if (!self.__destroyed) {
2509
2510 if(self.__state != 'closed'){
2511
2512 // no closing delay
2513 self.option('animationDuration', 0)
2514 // force closing
2515 ._close(null, null, true);
2516 }
2517 else {
2518 // there might be an open timeout still running
2519 self.__timeoutsClear();
2520 }
2521
2522 // send event
2523 self._trigger('destroy');
2524
2525 self.__destroyed = true;
2526
2527 self._$origin
2528 .removeData(self.__namespace)
2529 // remove the open trigger listeners
2530 .off('.'+ self.__namespace +'-triggerOpen');
2531
2532 // remove the touch listener
2533 $(env.window.document.body).off('.' + self.__namespace +'-triggerOpen');
2534
2535 var ns = self._$origin.data('tooltipster-ns');
2536
2537 // if the origin has been removed from DOM, its data may
2538 // well have been destroyed in the process and there would
2539 // be nothing to clean up or restore
2540 if (ns) {
2541
2542 // if there are no more tooltips on this element
2543 if (ns.length === 1) {
2544
2545 // optional restoration of a title attribute
2546 var title = null;
2547 if (self.__options.restoration == 'previous') {
2548 title = self._$origin.data('tooltipster-initialTitle');
2549 }
2550 else if (self.__options.restoration == 'current') {
2551
2552 // old school technique to stringify when outerHTML is not supported
2553 title = (typeof self.__Content == 'string') ?
2554 self.__Content :
2555 $('<div></div>').append(self.__Content).html();
2556 }
2557
2558 if (title) {
2559 self._$origin.attr('title', title);
2560 }
2561
2562 // final cleaning
2563
2564 self._$origin.removeClass('tooltipstered');
2565
2566 self._$origin
2567 .removeData('tooltipster-ns')
2568 .removeData('tooltipster-initialTitle');
2569 }
2570 else {
2571 // remove the instance namespace from the list of namespaces of
2572 // tooltips present on the element
2573 ns = $.grep(ns, function(el, i) {
2574 return el !== self.__namespace;
2575 });
2576 self._$origin.data('tooltipster-ns', ns);
2577 }
2578 }
2579
2580 // last event
2581 self._trigger('destroyed');
2582
2583 // unbind private and public event listeners
2584 self._off();
2585 self.off();
2586
2587 // remove external references, just in case
2588 self.__Content = null;
2589 self.__$emitterPrivate = null;
2590 self.__$emitterPublic = null;
2591 self.__options.parent = null;
2592 self._$origin = null;
2593 self._$tooltip = null;
2594
2595 // make sure the object is no longer referenced in there to prevent
2596 // memory leaks
2597 $.tooltipster.__instancesLatestArr = $.grep($.tooltipster.__instancesLatestArr, function(el, i) {
2598 return self !== el;
2599 });
2600
2601 clearInterval(self.__garbageCollector);
2602 }
2603 else {
2604 self.__destroyError();
2605 }
2606
2607 // we return the scope rather than true so that the call to
2608 // .tooltipster('destroy') actually returns the matched elements
2609 // and applies to all of them
2610 return self;
2611 },
2612
2613 /**
2614 * Disables the tooltip
2615 *
2616 * @returns {self}
2617 * @public
2618 */
2619 disable: function() {
2620
2621 if (!this.__destroyed) {
2622
2623 // close first, in case the tooltip would not disappear on
2624 // its own (no close trigger)
2625 this._close();
2626 this.__enabled = false;
2627
2628 return this;
2629 }
2630 else {
2631 this.__destroyError();
2632 }
2633
2634 return this;
2635 },
2636
2637 /**
2638 * Returns the HTML element of the origin
2639 *
2640 * @returns {self}
2641 * @public
2642 */
2643 elementOrigin: function() {
2644
2645 if (!this.__destroyed) {
2646 return this._$origin[0];
2647 }
2648 else {
2649 this.__destroyError();
2650 }
2651 },
2652
2653 /**
2654 * Returns the HTML element of the tooltip
2655 *
2656 * @returns {self}
2657 * @public
2658 */
2659 elementTooltip: function() {
2660 return this._$tooltip ? this._$tooltip[0] : null;
2661 },
2662
2663 /**
2664 * Enables the tooltip
2665 *
2666 * @returns {self}
2667 * @public
2668 */
2669 enable: function() {
2670 this.__enabled = true;
2671 return this;
2672 },
2673
2674 /**
2675 * Alias, deprecated in 4.0.0
2676 *
2677 * @param {function} callback
2678 * @returns {self}
2679 * @public
2680 */
2681 hide: function(callback) {
2682 return this.close(callback);
2683 },
2684
2685 /**
2686 * Returns the instance
2687 *
2688 * @returns {self}
2689 * @public
2690 */
2691 instance: function() {
2692 return this;
2693 },
2694
2695 /**
2696 * For public use only, not to be used by plugins (use ::_off() instead)
2697 *
2698 * @returns {self}
2699 * @public
2700 */
2701 off: function() {
2702
2703 if (!this.__destroyed) {
2704 this.__$emitterPublic.off.apply(this.__$emitterPublic, Array.prototype.slice.apply(arguments));
2705 }
2706
2707 return this;
2708 },
2709
2710 /**
2711 * For public use only, not to be used by plugins (use ::_on() instead)
2712 *
2713 * @returns {self}
2714 * @public
2715 */
2716 on: function() {
2717
2718 if (!this.__destroyed) {
2719 this.__$emitterPublic.on.apply(this.__$emitterPublic, Array.prototype.slice.apply(arguments));
2720 }
2721 else {
2722 this.__destroyError();
2723 }
2724
2725 return this;
2726 },
2727
2728 /**
2729 * For public use only, not to be used by plugins
2730 *
2731 * @returns {self}
2732 * @public
2733 */
2734 one: function() {
2735
2736 if (!this.__destroyed) {
2737 this.__$emitterPublic.one.apply(this.__$emitterPublic, Array.prototype.slice.apply(arguments));
2738 }
2739 else {
2740 this.__destroyError();
2741 }
2742
2743 return this;
2744 },
2745
2746 /**
2747 * @see self::_open
2748 * @returns {self}
2749 * @public
2750 */
2751 open: function(callback) {
2752
2753 if (!this.__destroyed) {
2754 this._open(null, callback);
2755 }
2756 else {
2757 this.__destroyError();
2758 }
2759
2760 return this;
2761 },
2762
2763 /**
2764 * Get or set options. For internal use and advanced users only.
2765 *
2766 * @param {string} o Option name
2767 * @param {mixed} val optional A new value for the option
2768 * @return {mixed|self} If val is omitted, the value of the option
2769 * is returned, otherwise the instance itself is returned
2770 * @public
2771 */
2772 option: function(o, val) {
2773
2774 // getter
2775 if (val === undefined) {
2776 return this.__options[o];
2777 }
2778 // setter
2779 else {
2780
2781 if (!this.__destroyed) {
2782
2783 // change value
2784 this.__options[o] = val;
2785
2786 // format
2787 this.__optionsFormat();
2788
2789 // re-prepare the triggers if needed
2790 if ($.inArray(o, ['trigger', 'triggerClose', 'triggerOpen']) >= 0) {
2791 this.__prepareOrigin();
2792 }
2793
2794 if (o === 'selfDestruction') {
2795 this.__prepareGC();
2796 }
2797 }
2798 else {
2799 this.__destroyError();
2800 }
2801
2802 return this;
2803 }
2804 },
2805
2806 /**
2807 * This method is in charge of setting the position and size properties of the tooltip.
2808 * All the hard work is delegated to the display plugin.
2809 * Note: The tooltip may be detached from the DOM at the moment the method is called
2810 * but must be attached by the end of the method call.
2811 *
2812 * @param {object} event For internal use only. Defined if an event such as
2813 * window resizing triggered the repositioning
2814 * @param {boolean} tooltipIsDetached For internal use only. Set this to true if you
2815 * know that the tooltip not being in the DOM is not an issue (typically when the
2816 * tooltip element has just been created but has not been added to the DOM yet).
2817 * @returns {self}
2818 * @public
2819 */
2820 reposition: function(event, tooltipIsDetached) {
2821
2822 var self = this;
2823
2824 if (!self.__destroyed) {
2825
2826 // if the tooltip is still open and the origin is still in the DOM
2827 if (self.__state != 'closed' && bodyContains(self._$origin)) {
2828
2829 // if the tooltip has not been removed from DOM manually (or if it
2830 // has been detached on purpose)
2831 if (tooltipIsDetached || bodyContains(self._$tooltip)) {
2832
2833 if (!tooltipIsDetached) {
2834 // detach in case the tooltip overflows the window and adds
2835 // scrollbars to it, so __geometry can be accurate
2836 self._$tooltip.detach();
2837 }
2838
2839 // refresh the geometry object before passing it as a helper
2840 self.__Geometry = self.__geometry();
2841
2842 // let a plugin fo the rest
2843 self._trigger({
2844 type: 'reposition',
2845 event: event,
2846 helper: {
2847 geo: self.__Geometry
2848 }
2849 });
2850 }
2851 }
2852 }
2853 else {
2854 self.__destroyError();
2855 }
2856
2857 return self;
2858 },
2859
2860 /**
2861 * Alias, deprecated in 4.0.0
2862 *
2863 * @param callback
2864 * @returns {self}
2865 * @public
2866 */
2867 show: function(callback) {
2868 return this.open(callback);
2869 },
2870
2871 /**
2872 * Returns some properties about the instance
2873 *
2874 * @returns {object}
2875 * @public
2876 */
2877 status: function() {
2878
2879 return {
2880 destroyed: this.__destroyed,
2881 enabled: this.__enabled,
2882 open: this.__state !== 'closed',
2883 state: this.__state
2884 };
2885 },
2886
2887 /**
2888 * For public use only, not to be used by plugins
2889 *
2890 * @returns {self}
2891 * @public
2892 */
2893 triggerHandler: function() {
2894
2895 if (!this.__destroyed) {
2896 this.__$emitterPublic.triggerHandler.apply(this.__$emitterPublic, Array.prototype.slice.apply(arguments));
2897 }
2898 else {
2899 this.__destroyError();
2900 }
2901
2902 return this;
2903 }
2904 };
2905
2906 $.fn.tooltipster = function() {
2907
2908 // for using in closures
2909 var args = Array.prototype.slice.apply(arguments),
2910 // common mistake: an HTML element can't be in several tooltips at the same time
2911 contentCloningWarning = 'You are using a single HTML element as content for several tooltips. You probably want to set the contentCloning option to TRUE.';
2912
2913 // this happens with $(sel).tooltipster(...) when $(sel) does not match anything
2914 if (this.length === 0) {
2915
2916 // still chainable
2917 return this;
2918 }
2919 // this happens when calling $(sel).tooltipster('methodName or options')
2920 // where $(sel) matches one or more elements
2921 else {
2922
2923 // method calls
2924 if (typeof args[0] === 'string') {
2925
2926 var v = '#*$~&';
2927
2928 this.each(function() {
2929
2930 // retrieve the namepaces of the tooltip(s) that exist on that element.
2931 // We will interact with the first tooltip only.
2932 var ns = $(this).data('tooltipster-ns'),
2933 // self represents the instance of the first tooltipster plugin
2934 // associated to the current HTML object of the loop
2935 self = ns ? $(this).data(ns[0]) : null;
2936
2937 // if the current element holds a tooltipster instance
2938 if (self) {
2939
2940 if (typeof self[args[0]] === 'function') {
2941
2942 if ( this.length > 1
2943 && args[0] == 'content'
2944 && ( args[1] instanceof $
2945 || (typeof args[1] == 'object' && args[1] != null && args[1].tagName)
2946 )
2947 && !self.__options.contentCloning
2948 && self.__options.debug
2949 ) {
2950 console.log(contentCloningWarning);
2951 }
2952
2953 // note : args[1] and args[2] may not be defined
2954 var resp = self[args[0]](args[1], args[2]);
2955 }
2956 else {
2957 throw new Error('Unknown method "'+ args[0] +'"');
2958 }
2959
2960 // if the function returned anything other than the instance
2961 // itself (which implies chaining, except for the `instance` method)
2962 if (resp !== self || args[0] === 'instance') {
2963
2964 v = resp;
2965
2966 // return false to stop .each iteration on the first element
2967 // matched by the selector
2968 return false;
2969 }
2970 }
2971 else {
2972 throw new Error('You called Tooltipster\'s "'+ args[0] +'" method on an uninitialized element');
2973 }
2974 });
2975
2976 return (v !== '#*$~&') ? v : this;
2977 }
2978 // first argument is undefined or an object: the tooltip is initializing
2979 else {
2980
2981 // reset the array of last initialized objects
2982 $.tooltipster.__instancesLatestArr = [];
2983
2984 // is there a defined value for the multiple option in the options object ?
2985 var multipleIsSet = args[0] && args[0].multiple !== undefined,
2986 // if the multiple option is set to true, or if it's not defined but
2987 // set to true in the defaults
2988 multiple = (multipleIsSet && args[0].multiple) || (!multipleIsSet && defaults.multiple),
2989 // same for content
2990 contentIsSet = args[0] && args[0].content !== undefined,
2991 content = (contentIsSet && args[0].content) || (!contentIsSet && defaults.content),
2992 // same for contentCloning
2993 contentCloningIsSet = args[0] && args[0].contentCloning !== undefined,
2994 contentCloning =
2995 (contentCloningIsSet && args[0].contentCloning)
2996 || (!contentCloningIsSet && defaults.contentCloning),
2997 // same for debug
2998 debugIsSet = args[0] && args[0].debug !== undefined,
2999 debug = (debugIsSet && args[0].debug) || (!debugIsSet && defaults.debug);
3000
3001 if ( this.length > 1
3002 && ( content instanceof $
3003 || (typeof content == 'object' && content != null && content.tagName)
3004 )
3005 && !contentCloning
3006 && debug
3007 ) {
3008 console.log(contentCloningWarning);
3009 }
3010
3011 // create a tooltipster instance for each element if it doesn't
3012 // already have one or if the multiple option is set, and attach the
3013 // object to it
3014 this.each(function() {
3015
3016 var go = false,
3017 $this = $(this),
3018 ns = $this.data('tooltipster-ns'),
3019 obj = null;
3020
3021 if (!ns) {
3022 go = true;
3023 }
3024 else if (multiple) {
3025 go = true;
3026 }
3027 else if (debug) {
3028 console.log('Tooltipster: one or more tooltips are already attached to the element below. Ignoring.');
3029 console.log(this);
3030 }
3031
3032 if (go) {
3033 obj = new $.Tooltipster(this, args[0]);
3034
3035 // save the reference of the new instance
3036 if (!ns) ns = [];
3037 ns.push(obj.__namespace);
3038 $this.data('tooltipster-ns', ns);
3039
3040 // save the instance itself
3041 $this.data(obj.__namespace, obj);
3042
3043 // call our constructor custom function.
3044 // we do this here and not in ::init() because we wanted
3045 // the object to be saved in $this.data before triggering
3046 // it
3047 if (obj.__options.functionInit) {
3048 obj.__options.functionInit.call(obj, obj, {
3049 origin: this
3050 });
3051 }
3052
3053 // and now the event, for the plugins and core emitter
3054 obj._trigger('init');
3055 }
3056
3057 $.tooltipster.__instancesLatestArr.push(obj);
3058 });
3059
3060 return this;
3061 }
3062 }
3063 };
3064
3065 // Utilities
18 3066
19 3067 /**
20 * TOOLTIP IMPL.
3068 * A class to check if a tooltip can fit in given dimensions
3069 *
3070 * @param {object} $tooltip The jQuery wrapped tooltip element, or a clone of it
3071 */
3072 function Ruler($tooltip) {
3073
3074 // list of instance variables
3075
3076 this.$container;
3077 this.constraints = null;
3078 this.__$tooltip;
3079
3080 this.__init($tooltip);
3081 }
3082
3083 Ruler.prototype = {
3084
3085 /**
3086 * Move the tooltip into an invisible div that does not allow overflow to make
3087 * size tests. Note: the tooltip may or may not be attached to the DOM at the
3088 * moment this method is called, it does not matter.
3089 *
3090 * @param {object} $tooltip The object to test. May be just a clone of the
3091 * actual tooltip.
3092 * @private
3093 */
3094 __init: function($tooltip) {
3095
3096 this.__$tooltip = $tooltip;
3097
3098 this.__$tooltip
3099 .css({
3100 // for some reason we have to specify top and left 0
3101 left: 0,
3102 // any overflow will be ignored while measuring
3103 overflow: 'hidden',
3104 // positions at (0,0) without the div using 100% of the available width
3105 position: 'absolute',
3106 top: 0
3107 })
3108 // overflow must be auto during the test. We re-set this in case
3109 // it were modified by the user
3110 .find('.tooltipster-content')
3111 .css('overflow', 'auto');
3112
3113 this.$container = $('<div class="tooltipster-ruler"></div>')
3114 .append(this.__$tooltip)
3115 .appendTo(env.window.document.body);
3116 },
3117
3118 /**
3119 * Force the browser to redraw (re-render) the tooltip immediately. This is required
3120 * when you changed some CSS properties and need to make something with it
3121 * immediately, without waiting for the browser to redraw at the end of instructions.
3122 *
3123 * @see http://stackoverflow.com/questions/3485365/how-can-i-force-webkit-to-redraw-repaint-to-propagate-style-changes
3124 * @private
3125 */
3126 __forceRedraw: function() {
3127
3128 // note: this would work but for Webkit only
3129 //this.__$tooltip.close();
3130 //this.__$tooltip[0].offsetHeight;
3131 //this.__$tooltip.open();
3132
3133 // works in FF too
3134 var $p = this.__$tooltip.parent();
3135 this.__$tooltip.detach();
3136 this.__$tooltip.appendTo($p);
3137 },
3138
3139 /**
3140 * Set maximum dimensions for the tooltip. A call to ::measure afterwards
3141 * will tell us if the content overflows or if it's ok
3142 *
3143 * @param {int} width
3144 * @param {int} height
3145 * @return {Ruler}
3146 * @public
3147 */
3148 constrain: function(width, height) {
3149
3150 this.constraints = {
3151 width: width,
3152 height: height
3153 };
3154
3155 this.__$tooltip.css({
3156 // we disable display:flex, otherwise the content would overflow without
3157 // creating horizontal scrolling (which we need to detect).
3158 display: 'block',
3159 // reset any previous height
3160 height: '',
3161 // we'll check if horizontal scrolling occurs
3162 overflow: 'auto',
3163 // we'll set the width and see what height is generated and if there
3164 // is horizontal overflow
3165 width: width
3166 });
3167
3168 return this;
3169 },
3170
3171 /**
3172 * Reset the tooltip content overflow and remove the test container
3173 *
3174 * @returns {Ruler}
3175 * @public
3176 */
3177 destroy: function() {
3178
3179 // in case the element was not a clone
3180 this.__$tooltip
3181 .detach()
3182 .find('.tooltipster-content')
3183 .css({
3184 // reset to CSS value
3185 display: '',
3186 overflow: ''
3187 });
3188
3189 this.$container.remove();
3190 },
3191
3192 /**
3193 * Removes any constraints
3194 *
3195 * @returns {Ruler}
3196 * @public
3197 */
3198 free: function() {
3199
3200 this.constraints = null;
3201
3202 // reset to natural size
3203 this.__$tooltip.css({
3204 display: '',
3205 height: '',
3206 overflow: 'visible',
3207 width: ''
3208 });
3209
3210 return this;
3211 },
3212
3213 /**
3214 * Returns the size of the tooltip. When constraints are applied, also returns
3215 * whether the tooltip fits in the provided dimensions.
3216 * The idea is to see if the new height is small enough and if the content does
3217 * not overflow horizontally.
3218 *
3219 * @param {int} width
3220 * @param {int} height
3221 * @returns {object} An object with a bool `fits` property and a `size` property
3222 * @public
3223 */
3224 measure: function() {
3225
3226 this.__forceRedraw();
3227
3228 var tooltipBcr = this.__$tooltip[0].getBoundingClientRect(),
3229 result = { size: {
3230 // bcr.width/height are not defined in IE8- but in this
3231 // case, bcr.right/bottom will have the same value
3232 // except in iOS 8+ where tooltipBcr.bottom/right are wrong
3233 // after scrolling for reasons yet to be determined.
3234 // tooltipBcr.top/left might not be 0, see issue #514
3235 height: tooltipBcr.height || (tooltipBcr.bottom - tooltipBcr.top),
3236 width: tooltipBcr.width || (tooltipBcr.right - tooltipBcr.left)
3237 }};
3238
3239 if (this.constraints) {
3240
3241 // note: we used to use offsetWidth instead of boundingRectClient but
3242 // it returned rounded values, causing issues with sub-pixel layouts.
3243
3244 // note2: noticed that the bcrWidth of text content of a div was once
3245 // greater than the bcrWidth of its container by 1px, causing the final
3246 // tooltip box to be too small for its content. However, evaluating
3247 // their widths one against the other (below) surprisingly returned
3248 // equality. Happened only once in Chrome 48, was not able to reproduce
3249 // => just having fun with float position values...
3250
3251 var $content = this.__$tooltip.find('.tooltipster-content'),
3252 height = this.__$tooltip.outerHeight(),
3253 contentBcr = $content[0].getBoundingClientRect(),
3254 fits = {
3255 height: height <= this.constraints.height,
3256 width: (
3257 // this condition accounts for min-width property that
3258 // may apply
3259 tooltipBcr.width <= this.constraints.width
3260 // the -1 is here because scrollWidth actually returns
3261 // a rounded value, and may be greater than bcr.width if
3262 // it was rounded up. This may cause an issue for contents
3263 // which actually really overflow by 1px or so, but that
3264 // should be rare. Not sure how to solve this efficiently.
3265 // See http://blogs.msdn.com/b/ie/archive/2012/02/17/sub-pixel-rendering-and-the-css-object-model.aspx
3266 && contentBcr.width >= $content[0].scrollWidth - 1
3267 )
3268 };
3269
3270 result.fits = fits.height && fits.width;
3271 }
3272
3273 // old versions of IE get the width wrong for some reason and it causes
3274 // the text to be broken to a new line, so we round it up. If the width
3275 // is the width of the screen though, we can assume it is accurate.
3276 if ( env.IE
3277 && env.IE <= 11
3278 && result.size.width !== env.window.document.documentElement.clientWidth
3279 ) {
3280 result.size.width = Math.ceil(result.size.width) + 1;
3281 }
3282
3283 return result;
3284 }
3285 };
3286
3287 // quick & dirty compare function, not bijective nor multidimensional
3288 function areEqual(a,b) {
3289 var same = true;
3290 $.each(a, function(i, _) {
3291 if (b[i] === undefined || a[i] !== b[i]) {
3292 same = false;
3293 return false;
3294 }
3295 });
3296 return same;
3297 }
3298
3299 /**
3300 * A fast function to check if an element is still in the DOM. It
3301 * tries to use an id as ids are indexed by the browser, or falls
3302 * back to jQuery's `contains` method. May fail if two elements
3303 * have the same id, but so be it
3304 *
3305 * @param {object} $obj A jQuery-wrapped HTML element
3306 * @return {boolean}
21 3307 */
22
23 var TTIP = {};
24
25 TTIP.main = {
26 offset: [15,15],
27 maxWidth: 600,
28
29 setDeferredListeners: function(){
30 $('body').on('mouseover', '.tooltip', yt.show_tip);
31 $('body').on('mousemove', '.tooltip', yt.move_tip);
32 $('body').on('mouseout', '.tooltip', yt.close_tip);
33 },
34
35 init: function(){
36 $('#tip-box').remove();
37 yt.tipBox = document.createElement('div');
38 document.body.appendChild(yt.tipBox);
39 yt.tipBox.id = 'tip-box';
40
41 $(yt.tipBox).hide();
42 $(yt.tipBox).css('position', 'absolute');
43 if(yt.maxWidth !== null){
44 $(yt.tipBox).css('max-width', yt.maxWidth+'px');
45 }
46 yt.setDeferredListeners();
47 },
48
49 show_tip: function(e, el){
50 e.stopPropagation();
51 e.preventDefault();
52 var el = e.data || e.currentTarget || el;
53 if(el.tagName.toLowerCase() === 'img'){
54 yt.tipText = el.alt ? el.alt : '';
55 } else {
56 yt.tipText = el.title ? el.title : '';
57 }
58
59 if(yt.tipText !== ''){
60 // save org title
61 $(el).attr('tt_title', yt.tipText);
62 // reset title to not show org tooltips
63 $(el).attr('title', '');
64
65 yt.tipBox.innerHTML = yt.tipText;
66 $(yt.tipBox).show();
67 }
68 },
69
70 move_tip: function(e, el){
71 e.stopPropagation();
72 e.preventDefault();
73 var el = e.data || e.currentTarget || el;
74 var movePos = [e.pageX, e.pageY];
75 $(yt.tipBox).css('top', (movePos[1] + yt.offset[1]) + 'px')
76 $(yt.tipBox).css('left', (movePos[0] + yt.offset[0]) + 'px')
77 },
78
79 close_tip: function(e, el){
80 e.stopPropagation();
81 e.preventDefault();
82 var el = e.data || e.currentTarget || el;
83 $(yt.tipBox).hide();
84 $(el).attr('title', $(el).attr('tt_title'));
85 $('#tip-box').hide();
86 }
87 };
88
89 // activate tooltips
90 yt = TTIP.main;
91 if ($(document).data('activated-tooltips') !== '1'){
92 $(document).ready(yt.init);
93 $(document).data('activated-tooltips', '1');
3308 function bodyContains($obj) {
3309 var id = $obj.attr('id'),
3310 el = id ? env.window.document.getElementById(id) : null;
3311 // must also check that the element with the id is the one we want
3312 return el ? el === $obj[0] : $.contains(env.window.document.body, $obj[0]);
3313 }
3314
3315 // detect IE versions for dirty fixes
3316 var uA = navigator.userAgent.toLowerCase();
3317 if (uA.indexOf('msie') != -1) env.IE = parseInt(uA.split('msie')[1]);
3318 else if (uA.toLowerCase().indexOf('trident') !== -1 && uA.indexOf(' rv:11') !== -1) env.IE = 11;
3319 else if (uA.toLowerCase().indexOf('edge/') != -1) env.IE = parseInt(uA.toLowerCase().split('edge/')[1]);
3320
3321 // detecting support for CSS transitions
3322 function transitionSupport() {
3323
3324 // env.window is not defined yet when this is called
3325 if (!win) return false;
3326
3327 var b = win.document.body || win.document.documentElement,
3328 s = b.style,
3329 p = 'transition',
3330 v = ['Moz', 'Webkit', 'Khtml', 'O', 'ms'];
3331
3332 if (typeof s[p] == 'string') { return true; }
3333
3334 p = p.charAt(0).toUpperCase() + p.substr(1);
3335 for (var i=0; i<v.length; i++) {
3336 if (typeof s[v[i] + p] == 'string') { return true; }
3337 }
3338 return false;
94 3339 }
3340
3341 // we'll return jQuery for plugins not to have to declare it as a dependency,
3342 // but it's done by a build task since it should be included only once at the
3343 // end when we concatenate the main file with a plugin
3344 // sideTip is Tooltipster's default plugin.
3345 // This file will be UMDified by a build task.
3346
3347 var pluginName = 'tooltipster.sideTip';
3348
3349 $.tooltipster._plugin({
3350 name: pluginName,
3351 instance: {
3352 /**
3353 * Defaults are provided as a function for an easy override by inheritance
3354 *
3355 * @return {object} An object with the defaults options
3356 * @private
3357 */
3358 __defaults: function() {
3359
3360 return {
3361 // if the tooltip should display an arrow that points to the origin
3362 arrow: true,
3363 // the distance in pixels between the tooltip and the origin
3364 distance: 6,
3365 // allows to easily change the position of the tooltip
3366 functionPosition: null,
3367 maxWidth: null,
3368 // used to accomodate the arrow of tooltip if there is one.
3369 // First to make sure that the arrow target is not too close
3370 // to the edge of the tooltip, so the arrow does not overflow
3371 // the tooltip. Secondly when we reposition the tooltip to
3372 // make sure that it's positioned in such a way that the arrow is
3373 // still pointing at the target (and not a few pixels beyond it).
3374 // It should be equal to or greater than half the width of
3375 // the arrow (by width we mean the size of the side which touches
3376 // the side of the tooltip).
3377 minIntersection: 16,
3378 minWidth: 0,
3379 // deprecated in 4.0.0. Listed for _optionsExtract to pick it up
3380 position: null,
3381 side: 'top',
3382 // set to false to position the tooltip relatively to the document rather
3383 // than the window when we open it
3384 viewportAware: true
3385 };
3386 },
3387
3388 /**
3389 * Run once: at instantiation of the plugin
3390 *
3391 * @param {object} instance The tooltipster object that instantiated this plugin
3392 * @private
3393 */
3394 __init: function(instance) {
3395
3396 var self = this;
3397
3398 // list of instance variables
3399
3400 self.__instance = instance;
3401 self.__namespace = 'tooltipster-sideTip-'+ Math.round(Math.random()*1000000);
3402 self.__previousState = 'closed';
3403 self.__options;
3404
3405 // initial formatting
3406 self.__optionsFormat();
3407
3408 self.__instance._on('state.'+ self.__namespace, function(event) {
3409
3410 if (event.state == 'closed') {
3411 self.__close();
3412 }
3413 else if (event.state == 'appearing' && self.__previousState == 'closed') {
3414 self.__create();
3415 }
3416
3417 self.__previousState = event.state;
3418 });
3419
3420 // reformat every time the options are changed
3421 self.__instance._on('options.'+ self.__namespace, function() {
3422 self.__optionsFormat();
3423 });
3424
3425 self.__instance._on('reposition.'+ self.__namespace, function(e) {
3426 self.__reposition(e.event, e.helper);
3427 });
3428 },
3429
3430 /**
3431 * Called when the tooltip has closed
3432 *
3433 * @private
3434 */
3435 __close: function() {
3436
3437 // detach our content object first, so the next jQuery's remove()
3438 // call does not unbind its event handlers
3439 if (this.__instance.content() instanceof $) {
3440 this.__instance.content().detach();
3441 }
3442
3443 // remove the tooltip from the DOM
3444 this.__instance._$tooltip.remove();
3445 this.__instance._$tooltip = null;
3446 },
3447
3448 /**
3449 * Creates the HTML element of the tooltip.
3450 *
3451 * @private
3452 */
3453 __create: function() {
3454
3455 // note: we wrap with a .tooltipster-box div to be able to set a margin on it
3456 // (.tooltipster-base must not have one)
3457 var $html = $(
3458 '<div class="tooltipster-base tooltipster-sidetip">' +
3459 '<div class="tooltipster-box">' +
3460 '<div class="tooltipster-content"></div>' +
3461 '</div>' +
3462 '<div class="tooltipster-arrow">' +
3463 '<div class="tooltipster-arrow-uncropped">' +
3464 '<div class="tooltipster-arrow-border"></div>' +
3465 '<div class="tooltipster-arrow-background"></div>' +
3466 '</div>' +
3467 '</div>' +
3468 '</div>'
3469 );
3470
3471 // hide arrow if asked
3472 if (!this.__options.arrow) {
3473 $html
3474 .find('.tooltipster-box')
3475 .css('margin', 0)
3476 .end()
3477 .find('.tooltipster-arrow')
3478 .hide();
3479 }
3480
3481 // apply min/max width if asked
3482 if (this.__options.minWidth) {
3483 $html.css('min-width', this.__options.minWidth + 'px');
3484 }
3485 if (this.__options.maxWidth) {
3486 $html.css('max-width', this.__options.maxWidth + 'px');
3487 }
3488
3489 this.__instance._$tooltip = $html;
3490
3491 // tell the instance that the tooltip element has been created
3492 this.__instance._trigger('created');
3493 },
3494
3495 /**
3496 * Used when the plugin is to be unplugged
3497 *
3498 * @private
3499 */
3500 __destroy: function() {
3501 this.__instance._off('.'+ self.__namespace);
3502 },
3503
3504 /**
3505 * (Re)compute this.__options from the options declared to the instance
3506 *
3507 * @private
3508 */
3509 __optionsFormat: function() {
3510
3511 var self = this;
3512
3513 // get the options
3514 self.__options = self.__instance._optionsExtract(pluginName, self.__defaults());
3515
3516 // for backward compatibility, deprecated in v4.0.0
3517 if (self.__options.position) {
3518 self.__options.side = self.__options.position;
3519 }
3520
3521 // options formatting
3522
3523 // format distance as a four-cell array if it ain't one yet and then make
3524 // it an object with top/bottom/left/right properties
3525 if (typeof self.__options.distance != 'object') {
3526 self.__options.distance = [self.__options.distance];
3527 }
3528 if (self.__options.distance.length < 4) {
3529
3530 if (self.__options.distance[1] === undefined) self.__options.distance[1] = self.__options.distance[0];
3531 if (self.__options.distance[2] === undefined) self.__options.distance[2] = self.__options.distance[0];
3532 if (self.__options.distance[3] === undefined) self.__options.distance[3] = self.__options.distance[1];
3533
3534 self.__options.distance = {
3535 top: self.__options.distance[0],
3536 right: self.__options.distance[1],
3537 bottom: self.__options.distance[2],
3538 left: self.__options.distance[3]
3539 };
3540 }
3541
3542 // let's transform:
3543 // 'top' into ['top', 'bottom', 'right', 'left']
3544 // 'right' into ['right', 'left', 'top', 'bottom']
3545 // 'bottom' into ['bottom', 'top', 'right', 'left']
3546 // 'left' into ['left', 'right', 'top', 'bottom']
3547 if (typeof self.__options.side == 'string') {
3548
3549 var opposites = {
3550 'top': 'bottom',
3551 'right': 'left',
3552 'bottom': 'top',
3553 'left': 'right'
3554 };
3555
3556 self.__options.side = [self.__options.side, opposites[self.__options.side]];
3557
3558 if (self.__options.side[0] == 'left' || self.__options.side[0] == 'right') {
3559 self.__options.side.push('top', 'bottom');
3560 }
3561 else {
3562 self.__options.side.push('right', 'left');
3563 }
3564 }
3565
3566 // misc
3567 // disable the arrow in IE6 unless the arrow option was explicitly set to true
3568 if ( $.tooltipster._env.IE === 6
3569 && self.__options.arrow !== true
3570 ) {
3571 self.__options.arrow = false;
3572 }
3573 },
3574
3575 /**
3576 * This method must compute and set the positioning properties of the
3577 * tooltip (left, top, width, height, etc.). It must also make sure the
3578 * tooltip is eventually appended to its parent (since the element may be
3579 * detached from the DOM at the moment the method is called).
3580 *
3581 * We'll evaluate positioning scenarios to find which side can contain the
3582 * tooltip in the best way. We'll consider things relatively to the window
3583 * (unless the user asks not to), then to the document (if need be, or if the
3584 * user explicitly requires the tests to run on the document). For each
3585 * scenario, measures are taken, allowing us to know how well the tooltip
3586 * is going to fit. After that, a sorting function will let us know what
3587 * the best scenario is (we also allow the user to choose his favorite
3588 * scenario by using an event).
3589 *
3590 * @param {object} helper An object that contains variables that plugin
3591 * creators may find useful (see below)
3592 * @param {object} helper.geo An object with many layout properties
3593 * about objects of interest (window, document, origin). This should help
3594 * plugin users compute the optimal position of the tooltip
3595 * @private
3596 */
3597 __reposition: function(event, helper) {
3598
3599 var self = this,
3600 finalResult,
3601 // to know where to put the tooltip, we need to know on which point
3602 // of the x or y axis we should center it. That coordinate is the target
3603 targets = self.__targetFind(helper),
3604 testResults = [];
3605
3606 // make sure the tooltip is detached while we make tests on a clone
3607 self.__instance._$tooltip.detach();
3608
3609 // we could actually provide the original element to the Ruler and
3610 // not a clone, but it just feels right to keep it out of the
3611 // machinery.
3612 var $clone = self.__instance._$tooltip.clone(),
3613 // start position tests session
3614 ruler = $.tooltipster._getRuler($clone),
3615 satisfied = false,
3616 animation = self.__instance.option('animation');
3617
3618 // an animation class could contain properties that distort the size
3619 if (animation) {
3620 $clone.removeClass('tooltipster-'+ animation);
3621 }
3622
3623 // start evaluating scenarios
3624 $.each(['window', 'document'], function(i, container) {
3625
3626 var takeTest = null;
3627
3628 // let the user decide to keep on testing or not
3629 self.__instance._trigger({
3630 container: container,
3631 helper: helper,
3632 satisfied: satisfied,
3633 takeTest: function(bool) {
3634 takeTest = bool;
3635 },
3636 results: testResults,
3637 type: 'positionTest'
3638 });
3639
3640 if ( takeTest == true
3641 || ( takeTest != false
3642 && satisfied == false
3643 // skip the window scenarios if asked. If they are reintegrated by
3644 // the callback of the positionTest event, they will have to be
3645 // excluded using the callback of positionTested
3646 && (container != 'window' || self.__options.viewportAware)
3647 )
3648 ) {
3649
3650 // for each allowed side
3651 for (var i=0; i < self.__options.side.length; i++) {
3652
3653 var distance = {
3654 horizontal: 0,
3655 vertical: 0
3656 },
3657 side = self.__options.side[i];
3658
3659 if (side == 'top' || side == 'bottom') {
3660 distance.vertical = self.__options.distance[side];
3661 }
3662 else {
3663 distance.horizontal = self.__options.distance[side];
3664 }
3665
3666 // this may have an effect on the size of the tooltip if there are css
3667 // rules for the arrow or something else
3668 self.__sideChange($clone, side);
3669
3670 $.each(['natural', 'constrained'], function(i, mode) {
3671
3672 takeTest = null;
3673
3674 // emit an event on the instance
3675 self.__instance._trigger({
3676 container: container,
3677 event: event,
3678 helper: helper,
3679 mode: mode,
3680 results: testResults,
3681 satisfied: satisfied,
3682 side: side,
3683 takeTest: function(bool) {
3684 takeTest = bool;
3685 },
3686 type: 'positionTest'
3687 });
3688
3689 if ( takeTest == true
3690 || ( takeTest != false
3691 && satisfied == false
3692 )
3693 ) {
3694
3695 var testResult = {
3696 container: container,
3697 // we let the distance as an object here, it can make things a little easier
3698 // during the user's calculations at positionTest/positionTested
3699 distance: distance,
3700 // whether the tooltip can fit in the size of the viewport (does not mean
3701 // that we'll be able to make it initially entirely visible, see 'whole')
3702 fits: null,
3703 mode: mode,
3704 outerSize: null,
3705 side: side,
3706 size: null,
3707 target: targets[side],
3708 // check if the origin has enough surface on screen for the tooltip to
3709 // aim at it without overflowing the viewport (this is due to the thickness
3710 // of the arrow represented by the minIntersection length).
3711 // If not, the tooltip will have to be partly or entirely off screen in
3712 // order to stay docked to the origin. This value will stay null when the
3713 // container is the document, as it is not relevant
3714 whole: null
3715 };
3716
3717 // get the size of the tooltip with or without size constraints
3718 var rulerConfigured = (mode == 'natural') ?
3719 ruler.free() :
3720 ruler.constrain(
3721 helper.geo.available[container][side].width - distance.horizontal,
3722 helper.geo.available[container][side].height - distance.vertical
3723 ),
3724 rulerResults = rulerConfigured.measure();
3725
3726 testResult.size = rulerResults.size;
3727 testResult.outerSize = {
3728 height: rulerResults.size.height + distance.vertical,
3729 width: rulerResults.size.width + distance.horizontal
3730 };
3731
3732 if (mode == 'natural') {
3733
3734 if( helper.geo.available[container][side].width >= testResult.outerSize.width
3735 && helper.geo.available[container][side].height >= testResult.outerSize.height
3736 ) {
3737 testResult.fits = true;
3738 }
3739 else {
3740 testResult.fits = false;
3741 }
3742 }
3743 else {
3744 testResult.fits = rulerResults.fits;
3745 }
3746
3747 if (container == 'window') {
3748
3749 if (!testResult.fits) {
3750 testResult.whole = false;
3751 }
3752 else {
3753 if (side == 'top' || side == 'bottom') {
3754
3755 testResult.whole = (
3756 helper.geo.origin.windowOffset.right >= self.__options.minIntersection
3757 && helper.geo.window.size.width - helper.geo.origin.windowOffset.left >= self.__options.minIntersection
3758 );
3759 }
3760 else {
3761 testResult.whole = (
3762 helper.geo.origin.windowOffset.bottom >= self.__options.minIntersection
3763 && helper.geo.window.size.height - helper.geo.origin.windowOffset.top >= self.__options.minIntersection
3764 );
3765 }
3766 }
3767 }
3768
3769 testResults.push(testResult);
3770
3771 // we don't need to compute more positions if we have one fully on screen
3772 if (testResult.whole) {
3773 satisfied = true;
3774 }
3775 else {
3776 // don't run the constrained test unless the natural width was greater
3777 // than the available width, otherwise it's pointless as we know it
3778 // wouldn't fit either
3779 if ( testResult.mode == 'natural'
3780 && ( testResult.fits
3781 || testResult.size.width <= helper.geo.available[container][side].width
3782 )
3783 ) {
3784 return false;
3785 }
3786 }
3787 }
3788 });
3789 }
3790 }
3791 });
3792
3793 // the user may eliminate the unwanted scenarios from testResults, but he's
3794 // not supposed to alter them at this point. functionPosition and the
3795 // position event serve that purpose.
3796 self.__instance._trigger({
3797 edit: function(r) {
3798 testResults = r;
3799 },
3800 event: event,
3801 helper: helper,
3802 results: testResults,
3803 type: 'positionTested'
3804 });
3805
3806 /**
3807 * Sort the scenarios to find the favorite one.
3808 *
3809 * The favorite scenario is when we can fully display the tooltip on screen,
3810 * even if it means that the middle of the tooltip is no longer centered on
3811 * the middle of the origin (when the origin is near the edge of the screen
3812 * or even partly off screen). We want the tooltip on the preferred side,
3813 * even if it means that we have to use a constrained size rather than a
3814 * natural one (as long as it fits). When the origin is off screen at the top
3815 * the tooltip will be positioned at the bottom (if allowed), if the origin
3816 * is off screen on the right, it will be positioned on the left, etc.
3817 * If there are no scenarios where the tooltip can fit on screen, or if the
3818 * user does not want the tooltip to fit on screen (viewportAware == false),
3819 * we fall back to the scenarios relative to the document.
3820 *
3821 * When the tooltip is bigger than the viewport in either dimension, we stop
3822 * looking at the window scenarios and consider the document scenarios only,
3823 * with the same logic to find on which side it would fit best.
3824 *
3825 * If the tooltip cannot fit the document on any side, we force it at the
3826 * bottom, so at least the user can scroll to see it.
3827 */
3828 testResults.sort(function(a, b) {
3829
3830 // best if it's whole (the tooltip fits and adapts to the viewport)
3831 if (a.whole && !b.whole) {
3832 return -1;
3833 }
3834 else if (!a.whole && b.whole) {
3835 return 1;
3836 }
3837 else if (a.whole && b.whole) {
3838
3839 var ai = self.__options.side.indexOf(a.side),
3840 bi = self.__options.side.indexOf(b.side);
3841
3842 // use the user's sides fallback array
3843 if (ai < bi) {
3844 return -1;
3845 }
3846 else if (ai > bi) {
3847 return 1;
3848 }
3849 else {
3850 // will be used if the user forced the tests to continue
3851 return a.mode == 'natural' ? -1 : 1;
3852 }
3853 }
3854 else {
3855
3856 // better if it fits
3857 if (a.fits && !b.fits) {
3858 return -1;
3859 }
3860 else if (!a.fits && b.fits) {
3861 return 1;
3862 }
3863 else if (a.fits && b.fits) {
3864
3865 var ai = self.__options.side.indexOf(a.side),
3866 bi = self.__options.side.indexOf(b.side);
3867
3868 // use the user's sides fallback array
3869 if (ai < bi) {
3870 return -1;
3871 }
3872 else if (ai > bi) {
3873 return 1;
3874 }
3875 else {
3876 // will be used if the user forced the tests to continue
3877 return a.mode == 'natural' ? -1 : 1;
3878 }
3879 }
3880 else {
3881
3882 // if everything failed, this will give a preference to the case where
3883 // the tooltip overflows the document at the bottom
3884 if ( a.container == 'document'
3885 && a.side == 'bottom'
3886 && a.mode == 'natural'
3887 ) {
3888 return -1;
3889 }
3890 else {
3891 return 1;
3892 }
3893 }
3894 }
3895 });
3896
3897 finalResult = testResults[0];
3898
3899
3900 // now let's find the coordinates of the tooltip relatively to the window
3901 finalResult.coord = {};
3902
3903 switch (finalResult.side) {
3904
3905 case 'left':
3906 case 'right':
3907 finalResult.coord.top = Math.floor(finalResult.target - finalResult.size.height / 2);
3908 break;
3909
3910 case 'bottom':
3911 case 'top':
3912 finalResult.coord.left = Math.floor(finalResult.target - finalResult.size.width / 2);
3913 break;
3914 }
3915
3916 switch (finalResult.side) {
3917
3918 case 'left':
3919 finalResult.coord.left = helper.geo.origin.windowOffset.left - finalResult.outerSize.width;
3920 break;
3921
3922 case 'right':
3923 finalResult.coord.left = helper.geo.origin.windowOffset.right + finalResult.distance.horizontal;
3924 break;
3925
3926 case 'top':
3927 finalResult.coord.top = helper.geo.origin.windowOffset.top - finalResult.outerSize.height;
3928 break;
3929
3930 case 'bottom':
3931 finalResult.coord.top = helper.geo.origin.windowOffset.bottom + finalResult.distance.vertical;
3932 break;
3933 }
3934
3935 // if the tooltip can potentially be contained within the viewport dimensions
3936 // and that we are asked to make it fit on screen
3937 if (finalResult.container == 'window') {
3938
3939 // if the tooltip overflows the viewport, we'll move it accordingly (then it will
3940 // not be centered on the middle of the origin anymore). We only move horizontally
3941 // for top and bottom tooltips and vice versa.
3942 if (finalResult.side == 'top' || finalResult.side == 'bottom') {
3943
3944 // if there is an overflow on the left
3945 if (finalResult.coord.left < 0) {
3946
3947 // prevent the overflow unless the origin itself gets off screen (minus the
3948 // margin needed to keep the arrow pointing at the target)
3949 if (helper.geo.origin.windowOffset.right - this.__options.minIntersection >= 0) {
3950 finalResult.coord.left = 0;
3951 }
3952 else {
3953 finalResult.coord.left = helper.geo.origin.windowOffset.right - this.__options.minIntersection - 1;
3954 }
3955 }
3956 // or an overflow on the right
3957 else if (finalResult.coord.left > helper.geo.window.size.width - finalResult.size.width) {
3958
3959 if (helper.geo.origin.windowOffset.left + this.__options.minIntersection <= helper.geo.window.size.width) {
3960 finalResult.coord.left = helper.geo.window.size.width - finalResult.size.width;
3961 }
3962 else {
3963 finalResult.coord.left = helper.geo.origin.windowOffset.left + this.__options.minIntersection + 1 - finalResult.size.width;
3964 }
3965 }
3966 }
3967 else {
3968
3969 // overflow at the top
3970 if (finalResult.coord.top < 0) {
3971
3972 if (helper.geo.origin.windowOffset.bottom - this.__options.minIntersection >= 0) {
3973 finalResult.coord.top = 0;
3974 }
3975 else {
3976 finalResult.coord.top = helper.geo.origin.windowOffset.bottom - this.__options.minIntersection - 1;
3977 }
3978 }
3979 // or at the bottom
3980 else if (finalResult.coord.top > helper.geo.window.size.height - finalResult.size.height) {
3981
3982 if (helper.geo.origin.windowOffset.top + this.__options.minIntersection <= helper.geo.window.size.height) {
3983 finalResult.coord.top = helper.geo.window.size.height - finalResult.size.height;
3984 }
3985 else {
3986 finalResult.coord.top = helper.geo.origin.windowOffset.top + this.__options.minIntersection + 1 - finalResult.size.height;
3987 }
3988 }
3989 }
3990 }
3991 else {
3992
3993 // there might be overflow here too but it's easier to handle. If there has
3994 // to be an overflow, we'll make sure it's on the right side of the screen
3995 // (because the browser will extend the document size if there is an overflow
3996 // on the right, but not on the left). The sort function above has already
3997 // made sure that a bottom document overflow is preferred to a top overflow,
3998 // so we don't have to care about it.
3999
4000 // if there is an overflow on the right
4001 if (finalResult.coord.left > helper.geo.window.size.width - finalResult.size.width) {
4002
4003 // this may actually create on overflow on the left but we'll fix it in a sec
4004 finalResult.coord.left = helper.geo.window.size.width - finalResult.size.width;
4005 }
4006
4007 // if there is an overflow on the left
4008 if (finalResult.coord.left < 0) {
4009
4010 // don't care if it overflows the right after that, we made our best
4011 finalResult.coord.left = 0;
4012 }
4013 }
4014
4015
4016 // submit the positioning proposal to the user function which may choose to change
4017 // the side, size and/or the coordinates
4018
4019 // first, set the rules that corresponds to the proposed side: it may change
4020 // the size of the tooltip, and the custom functionPosition may want to detect the
4021 // size of something before making a decision. So let's make things easier for the
4022 // implementor
4023 self.__sideChange($clone, finalResult.side);
4024
4025 // add some variables to the helper
4026 helper.tooltipClone = $clone[0];
4027 helper.tooltipParent = self.__instance.option('parent').parent[0];
4028 // move informative values to the helper
4029 helper.mode = finalResult.mode;
4030 helper.whole = finalResult.whole;
4031 // add some variables to the helper for the functionPosition callback (these
4032 // will also be added to the event fired by self.__instance._trigger but that's
4033 // ok, we're just being consistent)
4034 helper.origin = self.__instance._$origin[0];
4035 helper.tooltip = self.__instance._$tooltip[0];
4036
4037 // leave only the actionable values in there for functionPosition
4038 delete finalResult.container;
4039 delete finalResult.fits;
4040 delete finalResult.mode;
4041 delete finalResult.outerSize;
4042 delete finalResult.whole;
4043
4044 // keep only the distance on the relevant side, for clarity
4045 finalResult.distance = finalResult.distance.horizontal || finalResult.distance.vertical;
4046
4047 // beginners may not be comfortable with the concept of editing the object
4048 // passed by reference, so we provide an edit function and pass a clone
4049 var finalResultClone = $.extend(true, {}, finalResult);
4050
4051 // emit an event on the instance
4052 self.__instance._trigger({
4053 edit: function(result) {
4054 finalResult = result;
4055 },
4056 event: event,
4057 helper: helper,
4058 position: finalResultClone,
4059 type: 'position'
4060 });
4061
4062 if (self.__options.functionPosition) {
4063
4064 var result = self.__options.functionPosition.call(self, self.__instance, helper, finalResultClone);
4065
4066 if (result) finalResult = result;
4067 }
4068
4069 // end the positioning tests session (the user might have had a
4070 // use for it during the position event, now it's over)
4071 ruler.destroy();
4072
4073 // compute the position of the target relatively to the tooltip root
4074 // element so we can place the arrow and make the needed adjustments
4075 var arrowCoord,
4076 maxVal;
4077
4078 if (finalResult.side == 'top' || finalResult.side == 'bottom') {
4079
4080 arrowCoord = {
4081 prop: 'left',
4082 val: finalResult.target - finalResult.coord.left
4083 };
4084 maxVal = finalResult.size.width - this.__options.minIntersection;
4085 }
4086 else {
4087
4088 arrowCoord = {
4089 prop: 'top',
4090 val: finalResult.target - finalResult.coord.top
4091 };
4092 maxVal = finalResult.size.height - this.__options.minIntersection;
4093 }
4094
4095 // cannot lie beyond the boundaries of the tooltip, minus the
4096 // arrow margin
4097 if (arrowCoord.val < this.__options.minIntersection) {
4098 arrowCoord.val = this.__options.minIntersection;
4099 }
4100 else if (arrowCoord.val > maxVal) {
4101 arrowCoord.val = maxVal;
4102 }
4103
4104 var originParentOffset;
4105
4106 // let's convert the window-relative coordinates into coordinates relative to the
4107 // future positioned parent that the tooltip will be appended to
4108 if (helper.geo.origin.fixedLineage) {
4109
4110 // same as windowOffset when the position is fixed
4111 originParentOffset = helper.geo.origin.windowOffset;
4112 }
4113 else {
4114
4115 // this assumes that the parent of the tooltip is located at
4116 // (0, 0) in the document, typically like when the parent is
4117 // <body>.
4118 // If we ever allow other types of parent, .tooltipster-ruler
4119 // will have to be appended to the parent to inherit css style
4120 // values that affect the display of the text and such.
4121 originParentOffset = {
4122 left: helper.geo.origin.windowOffset.left + helper.geo.window.scroll.left,
4123 top: helper.geo.origin.windowOffset.top + helper.geo.window.scroll.top
4124 };
4125 }
4126
4127 finalResult.coord = {
4128 left: originParentOffset.left + (finalResult.coord.left - helper.geo.origin.windowOffset.left),
4129 top: originParentOffset.top + (finalResult.coord.top - helper.geo.origin.windowOffset.top)
4130 };
4131
4132 // set position values on the original tooltip element
4133
4134 self.__sideChange(self.__instance._$tooltip, finalResult.side);
4135
4136 if (helper.geo.origin.fixedLineage) {
4137 self.__instance._$tooltip
4138 .css('position', 'fixed');
4139 }
4140 else {
4141 // CSS default
4142 self.__instance._$tooltip
4143 .css('position', '');
4144 }
4145
4146 self.__instance._$tooltip
4147 .css({
4148 left: finalResult.coord.left,
4149 top: finalResult.coord.top,
4150 // we need to set a size even if the tooltip is in its natural size
4151 // because when the tooltip is positioned beyond the width of the body
4152 // (which is by default the width of the window; it will happen when
4153 // you scroll the window horizontally to get to the origin), its text
4154 // content will otherwise break lines at each word to keep up with the
4155 // body overflow strategy.
4156 height: finalResult.size.height,
4157 width: finalResult.size.width
4158 })
4159 .find('.tooltipster-arrow')
4160 .css({
4161 'left': '',
4162 'top': ''
4163 })
4164 .css(arrowCoord.prop, arrowCoord.val);
4165
4166 // append the tooltip HTML element to its parent
4167 self.__instance._$tooltip.appendTo(self.__instance.option('parent'));
4168
4169 self.__instance._trigger({
4170 type: 'repositioned',
4171 event: event,
4172 position: finalResult
4173 });
4174 },
4175
4176 /**
4177 * Make whatever modifications are needed when the side is changed. This has
4178 * been made an independant method for easy inheritance in custom plugins based
4179 * on this default plugin.
4180 *
4181 * @param {object} $obj
4182 * @param {string} side
4183 * @private
4184 */
4185 __sideChange: function($obj, side) {
4186
4187 $obj
4188 .removeClass('tooltipster-bottom')
4189 .removeClass('tooltipster-left')
4190 .removeClass('tooltipster-right')
4191 .removeClass('tooltipster-top')
4192 .addClass('tooltipster-'+ side);
4193 },
4194
4195 /**
4196 * Returns the target that the tooltip should aim at for a given side.
4197 * The calculated value is a distance from the edge of the window
4198 * (left edge for top/bottom sides, top edge for left/right side). The
4199 * tooltip will be centered on that position and the arrow will be
4200 * positioned there (as much as possible).
4201 *
4202 * @param {object} helper
4203 * @return {integer}
4204 * @private
4205 */
4206 __targetFind: function(helper) {
4207
4208 var target = {},
4209 rects = this.__instance._$origin[0].getClientRects();
4210
4211 // these lines fix a Chrome bug (issue #491)
4212 if (rects.length > 1) {
4213 var opacity = this.__instance._$origin.css('opacity');
4214 if(opacity == 1) {
4215 this.__instance._$origin.css('opacity', 0.99);
4216 rects = this.__instance._$origin[0].getClientRects();
4217 this.__instance._$origin.css('opacity', 1);
4218 }
4219 }
4220
4221 // by default, the target will be the middle of the origin
4222 if (rects.length < 2) {
4223
4224 target.top = Math.floor(helper.geo.origin.windowOffset.left + (helper.geo.origin.size.width / 2));
4225 target.bottom = target.top;
4226
4227 target.left = Math.floor(helper.geo.origin.windowOffset.top + (helper.geo.origin.size.height / 2));
4228 target.right = target.left;
4229 }
4230 // if multiple client rects exist, the element may be text split
4231 // up into multiple lines and the middle of the origin may not be
4232 // best option anymore. We need to choose the best target client rect
4233 else {
4234
4235 // top: the first
4236 var targetRect = rects[0];
4237 target.top = Math.floor(targetRect.left + (targetRect.right - targetRect.left) / 2);
4238
4239 // right: the middle line, rounded down in case there is an even
4240 // number of lines (looks more centered => check out the
4241 // demo with 4 split lines)
4242 if (rects.length > 2) {
4243 targetRect = rects[Math.ceil(rects.length / 2) - 1];
4244 }
4245 else {
4246 targetRect = rects[0];
4247 }
4248 target.right = Math.floor(targetRect.top + (targetRect.bottom - targetRect.top) / 2);
4249
4250 // bottom: the last
4251 targetRect = rects[rects.length - 1];
4252 target.bottom = Math.floor(targetRect.left + (targetRect.right - targetRect.left) / 2);
4253
4254 // left: the middle line, rounded up
4255 if (rects.length > 2) {
4256 targetRect = rects[Math.ceil((rects.length + 1) / 2) - 1];
4257 }
4258 else {
4259 targetRect = rects[rects.length - 1];
4260 }
4261
4262 target.left = Math.floor(targetRect.top + (targetRect.bottom - targetRect.top) / 2);
4263 }
4264
4265 return target;
4266 }
4267 }
4268 });
4269
4270 /* a build task will add "return $;" here */
4271 return $;
4272
4273 }));
4274
4275 //
4276 // $(document).ready(function() {
4277 // $('.tooltip-hovercard').tooltipster({
4278 // theme: 'tooltipster-shadow',
4279 // animation: 'fade',
4280 // delay: 100,
4281 // contentCloning: true,
4282 //
4283 // }); No newline at end of file
@@ -1,137 +1,139 b''
1 1 ## -*- coding: utf-8 -*-
2 2 <%inherit file="/base/base.mako"/>
3 3
4 4 <%def name="title()">
5 5 %if c.show_private:
6 6 ${_('Private Gists for user {}').format(c.rhodecode_user.username)}
7 7 %elif c.show_public:
8 8 ${_('Public Gists for user {}').format(c.rhodecode_user.username)}
9 9 %else:
10 10 ${_('Public Gists')}
11 11 %endif
12 12 %if c.rhodecode_name:
13 13 &middot; ${h.branding(c.rhodecode_name)}
14 14 %endif
15 15 </%def>
16 16
17 17 <%def name="breadcrumbs_links()"></%def>
18 18
19 19 <%def name="menu_bar_nav()">
20 20 ${self.menu_items(active='gists')}
21 21 </%def>
22 22
23 23 <%def name="main()">
24 24
25 25 <div class="box">
26 26 <div class="title">
27 27
28 28 <ul class="button-links">
29 29 % if c.is_super_admin:
30 30 <li class="btn ${('active' if c.active=='all' else '')}"><a href="${h.route_path('gists_show', _query={'all': 1})}">${_('All gists')}</a></li>
31 31 %endif
32 32 <li class="btn ${('active' if c.active=='public' else '')}"><a href="${h.route_path('gists_show')}">${_('All public')}</a></li>
33 33 %if c.rhodecode_user.username != h.DEFAULT_USER:
34 34 <li class="btn ${('active' if c.active=='my_all' else '')}"><a href="${h.route_path('gists_show', _query={'public':1, 'private': 1})}">${_('My gists')}</a></li>
35 35 <li class="btn ${('active' if c.active=='my_private' else '')}"><a href="${h.route_path('gists_show', _query={'private': 1})}">${_('My private')}</a></li>
36 36 <li class="btn ${('active' if c.active=='my_public' else '')}"><a href="${h.route_path('gists_show', _query={'public': 1})}">${_('My public')}</a></li>
37 37 %endif
38 38 </ul>
39 39
40 40 <div class="grid-quick-filter">
41 41 <ul class="grid-filter-box">
42 42 <li class="grid-filter-box-icon">
43 43 <i class="icon-search"></i>
44 44 </li>
45 45 <li class="grid-filter-box-input">
46 46 <input class="q_filter_box" id="q_filter" size="15" type="text" name="filter" placeholder="${_('quick filter...')}" value=""/>
47 47 </li>
48 48 </ul>
49 49 </div>
50 50
51 51 </div>
52 52
53 53 <div class="main-content-full-width">
54 54 <div id="repos_list_wrap">
55 55 <table id="gist_list_table" class="display"></table>
56 56 </div>
57 57 </div>
58 58
59 59 </div>
60 60
61 61 <script type="text/javascript">
62 62 $(document).ready(function() {
63 63
64 64 var get_datatable_count = function(){
65 65 var api = $('#gist_list_table').dataTable().api();
66 66 $('#gists_count').text(api.page.info().recordsDisplay);
67 67 };
68 68
69 69
70 70 // custom filter that filters by access_id, description or author
71 71 $.fn.dataTable.ext.search.push(
72 72 function( settings, data, dataIndex ) {
73 73 var query = $('#q_filter').val();
74 74 var author = data[0].strip();
75 75 var access_id = data[2].strip();
76 76 var description = data[3].strip();
77 77
78 78 var query_str = (access_id + " " + author + " " + description).toLowerCase();
79 79
80 80 if(query_str.indexOf(query.toLowerCase()) !== -1){
81 81 return true;
82 82 }
83 83 return false;
84 84 }
85 85 );
86 86
87 87 // gists list
88 88 $('#gist_list_table').DataTable({
89 89 data: ${c.data|n},
90 90 dom: 'rtp',
91 91 pageLength: ${c.visual.dashboard_items},
92 92 order: [[ 4, "desc" ]],
93 93 columns: [
94 94 { data: {"_": "author",
95 95 "sort": "author_raw"}, title: "${_("Author")}", width: "250px", className: "td-user" },
96 96 { data: {"_": "type",
97 97 "sort": "type"}, title: "${_("Type")}", width: "70px", className: "td-tags" },
98 98 { data: {"_": "access_id",
99 99 "sort": "access_id"}, title: "${_("Name")}", width:"150px", className: "td-componentname" },
100 100 { data: {"_": "description",
101 101 "sort": "description"}, title: "${_("Description")}", width: "250px", className: "td-description" },
102 102 { data: {"_": "created_on",
103 103 "sort": "created_on_raw"}, title: "${_("Created on")}", className: "td-time" },
104 104 { data: {"_": "expires",
105 105 "sort": "expires"}, title: "${_("Expires")}", className: "td-exp" }
106 106 ],
107 107 language: {
108 108 paginate: DEFAULT_GRID_PAGINATION,
109 109 emptyTable: _gettext("No gists available yet.")
110 110 },
111 111 "initComplete": function( settings, json ) {
112 112 timeagoActivate();
113 tooltipActivate();
113 114 get_datatable_count();
114 115 }
115 116 });
116 117
117 118 // update the counter when things change
118 119 $('#gist_list_table').on('draw.dt', function() {
119 120 timeagoActivate();
121 tooltipActivate();
120 122 get_datatable_count();
121 123 });
122 124
123 125 // filter, filter both grids
124 126 $('#q_filter').on( 'keyup', function () {
125 127 var repo_api = $('#gist_list_table').dataTable().api();
126 128 repo_api
127 129 .draw();
128 130 });
129 131
130 132 // refilter table if page load via back button
131 133 $("#q_filter").trigger('keyup');
132 134
133 135 });
134 136
135 137 </script>
136 138 </%def>
137 139
@@ -1,111 +1,111 b''
1 1 ## -*- coding: utf-8 -*-
2 2 <%inherit file="/base/base.mako"/>
3 3
4 4 <%def name="robots()">
5 5 %if c.gist.gist_type != 'public':
6 6 <meta name="robots" content="noindex, nofollow">
7 7 %else:
8 8 ${parent.robots()}
9 9 %endif
10 10 </%def>
11 11
12 12 <%def name="title()">
13 13 ${_('Gist')} &middot; ${c.gist.gist_access_id}
14 14 %if c.rhodecode_name:
15 15 &middot; ${h.branding(c.rhodecode_name)}
16 16 %endif
17 17 </%def>
18 18
19 19 <%def name="breadcrumbs_links()">
20 20 ${_('Gist')} &middot; ${c.gist.gist_access_id}
21 21 </%def>
22 22
23 23 <%def name="menu_bar_nav()">
24 24 ${self.menu_items(active='gists')}
25 25 </%def>
26 26
27 27 <%def name="main()">
28 28 <div class="box">
29 29 <!-- box / title -->
30 30 <div class="title">
31 31 ${self.breadcrumbs()}
32 32 </div>
33 33
34 34 <div class="table">
35 35 <div id="files_data">
36 36 <div id="codeblock" class="codeblock">
37 37 <div class="code-header">
38 38 <div class="gist_url">
39 39 <code>
40 40 ${c.gist.gist_url()} <span class="icon-clipboard clipboard-action" data-clipboard-text="${c.gist.gist_url()}" title="${_('Copy the url')}"></span>
41 41 </code>
42 42 </div>
43 43 <div class="stats">
44 44 %if c.is_super_admin or c.gist.gist_owner == c.rhodecode_user.user_id:
45 45 <div class="remove_gist">
46 46 ${h.secure_form(h.route_path('gist_delete', gist_id=c.gist.gist_access_id), request=request)}
47 47 ${h.submit('remove_gist', _('Delete'),class_="btn btn-mini btn-danger",onclick="return confirm('"+_('Confirm to delete this Gist')+"');")}
48 48 ${h.end_form()}
49 49 </div>
50 50 %endif
51 51 <div class="buttons">
52 52 ## only owner should see that
53 53 <a href="#copySource" onclick="return false;" class="btn btn-mini icon-clipboard clipboard-action" data-clipboard-text="${c.files[0].content}">${_('Copy content')}</a>
54 54
55 55 %if c.is_super_admin or c.gist.gist_owner == c.rhodecode_user.user_id:
56 56 ${h.link_to(_('Edit'), h.route_path('gist_edit', gist_id=c.gist.gist_access_id), class_="btn btn-mini")}
57 57 %endif
58 58 ${h.link_to(_('Show as Raw'), h.route_path('gist_show_formatted', gist_id=c.gist.gist_access_id, revision='tip', format='raw'), class_="btn btn-mini")}
59 59 </div>
60 60 <div class="left" >
61 61 %if c.gist.gist_type != 'public':
62 62 <span class="tag tag-ok disabled">${_('Private Gist')}</span>
63 63 %endif
64 64 <span> ${c.gist.gist_description}</span>
65 65 <span>${_('Expires')}:
66 66 %if c.gist.gist_expires == -1:
67 67 ${_('never')}
68 68 %else:
69 69 ${h.age_component(h.time_to_utcdatetime(c.gist.gist_expires))}
70 70 %endif
71 71 </span>
72 72
73 73 </div>
74 74 </div>
75 75
76 76 <div class="author">
77 77 <div title="${h.tooltip(c.file_last_commit.author)}">
78 ${self.gravatar_with_user(c.file_last_commit.author, 16)} - ${_('created')} ${h.age_component(c.file_last_commit.date)}
78 ${self.gravatar_with_user(c.file_last_commit.author, 16, tooltip=True)} - ${_('created')} ${h.age_component(c.file_last_commit.date)}
79 79 </div>
80 80
81 81 </div>
82 82 <div class="commit">${h.urlify_commit_message(c.file_last_commit.message, None)}</div>
83 83 </div>
84 84
85 85 ## iterate over the files
86 86 % for file in c.files:
87 87 <% renderer = c.render and h.renderer_from_filename(file.path, exclude=['.txt', '.TXT'])%>
88 88 <!--
89 89 <div id="${h.FID('G', file.path)}" class="stats" >
90 90 <a href="${c.gist.gist_url()}">¶</a>
91 91 <b >${file.path}</b>
92 92 <div>
93 93 ${h.link_to(_('Show as raw'), h.route_path('gist_show_formatted_path', gist_id=c.gist.gist_access_id, revision=file.commit.raw_id, format='raw', f_path=file.path), class_="btn btn-mini")}
94 94 </div>
95 95 </div>
96 96 -->
97 97 <div class="code-body textarea text-area editor">
98 98 %if renderer:
99 99 ${h.render(file.content, renderer=renderer)}
100 100 %else:
101 101 ${h.pygmentize(file,linenos=True,anchorlinenos=True,lineanchors='L',cssclass="code-highlight")}
102 102 %endif
103 103 </div>
104 104 %endfor
105 105 </div>
106 106 </div>
107 107 </div>
108 108
109 109
110 110 </div>
111 111 </%def>
@@ -1,93 +1,94 b''
1 1 <%namespace name="base" file="/base/base.mako"/>
2 2
3 3 <div class="panel panel-default">
4 4 <div class="panel-body">
5 5 %if c.closed:
6 6 ${h.checkbox('show_closed',checked="checked", label=_('Show Closed Pull Requests'))}
7 7 %else:
8 8 ${h.checkbox('show_closed',label=_('Show Closed Pull Requests'))}
9 9 %endif
10 10 </div>
11 11 </div>
12 12
13 13 <div class="panel panel-default">
14 14 <div class="panel-heading">
15 15 <h3 class="panel-title">${_('Pull Requests You Participate In')}</h3>
16 16 </div>
17 17 <div class="panel-body panel-body-min-height">
18 18 <table id="pull_request_list_table" class="display"></table>
19 19 </div>
20 20 </div>
21 21
22 22 <script type="text/javascript">
23 23 $(document).ready(function() {
24 24
25 25 $('#show_closed').on('click', function(e){
26 26 if($(this).is(":checked")){
27 27 window.location = "${h.route_path('my_account_pullrequests', _query={'pr_show_closed':1})}";
28 28 }
29 29 else{
30 30 window.location = "${h.route_path('my_account_pullrequests')}";
31 31 }
32 32 });
33 33
34 34 var $pullRequestListTable = $('#pull_request_list_table');
35 35
36 36 // participating object list
37 37 $pullRequestListTable.DataTable({
38 38 processing: true,
39 39 serverSide: true,
40 40 ajax: {
41 41 "url": "${h.route_path('my_account_pullrequests_data')}",
42 42 "data": function (d) {
43 43 d.closed = "${c.closed}";
44 44 }
45 45 },
46 46 dom: 'rtp',
47 47 pageLength: ${c.visual.dashboard_items},
48 48 order: [[ 2, "desc" ]],
49 49 columns: [
50 50 { data: {"_": "status",
51 51 "sort": "status"}, title: "", className: "td-status", orderable: false},
52 52 { data: {"_": "target_repo",
53 53 "sort": "target_repo"}, title: "${_('Target Repo')}", className: "td-targetrepo", orderable: false},
54 54 { data: {"_": "name",
55 55 "sort": "name_raw"}, title: "${_('Name')}", className: "td-componentname", "type": "num" },
56 56 { data: {"_": "author",
57 57 "sort": "author_raw"}, title: "${_('Author')}", className: "td-user", orderable: false },
58 58 { data: {"_": "title",
59 59 "sort": "title"}, title: "${_('Title')}", className: "td-description" },
60 60 { data: {"_": "comments",
61 61 "sort": "comments_raw"}, title: "", className: "td-comments", orderable: false},
62 62 { data: {"_": "updated_on",
63 63 "sort": "updated_on_raw"}, title: "${_('Last Update')}", className: "td-time" }
64 64 ],
65 65 language: {
66 66 paginate: DEFAULT_GRID_PAGINATION,
67 67 sProcessing: _gettext('loading...'),
68 68 emptyTable: _gettext("There are currently no open pull requests requiring your participation.")
69 69 },
70 70 "drawCallback": function( settings, json ) {
71 71 timeagoActivate();
72 tooltipActivate();
72 73 },
73 74 "createdRow": function ( row, data, index ) {
74 75 if (data['closed']) {
75 76 $(row).addClass('closed');
76 77 }
77 78 if (data['owned']) {
78 79 $(row).addClass('owned');
79 80 }
80 81 if (data['state'] !== 'created') {
81 82 $(row).addClass('state-' + data['state']);
82 83 }
83 84 }
84 85 });
85 86 $pullRequestListTable.on('xhr.dt', function(e, settings, json, xhr){
86 87 $pullRequestListTable.css('opacity', 1);
87 88 });
88 89
89 90 $pullRequestListTable.on('preXhr.dt', function(e, settings, data){
90 91 $pullRequestListTable.css('opacity', 0.3);
91 92 });
92 93 });
93 94 </script>
@@ -1,71 +1,71 b''
1 1 <%namespace name="base" file="/base/base.mako"/>
2 2
3 3 <%
4 4 source_repo_id = c.repo_group.changeset_cache.get('source_repo_id')
5 5
6 6 elems = [
7 7 (_('Repository Group ID'), c.repo_group.group_id, '', ''),
8 (_('Owner'), lambda:base.gravatar_with_user(c.repo_group.user.email), '', ''),
8 (_('Owner'), lambda:base.gravatar_with_user(c.repo_group.user.email, tooltip=True), '', ''),
9 9 (_('Created on'), h.format_date(c.repo_group.created_on), '', ''),
10 10 (_('Updated on'), h.format_date(c.repo_group.updated_on), '', ''),
11 11 (_('Cached Commit date'), (c.repo_group.changeset_cache.get('date')), '', ''),
12 12 (_('Cached Commit repo_id'), (h.link_to_if(source_repo_id, source_repo_id, h.route_path('repo_summary', repo_name='_{}'.format(source_repo_id)))), '', ''),
13 13
14 14 (_('Is Personal Group'), c.repo_group.personal or False, '', ''),
15 15
16 16 (_('Total repositories'), c.repo_group.repositories_recursive_count, '', ''),
17 17 (_('Top level repositories'), c.repo_group.repositories.count(), '', c.repo_group.repositories.all()),
18 18
19 19 (_('Children groups'), c.repo_group.children.count(), '', c.repo_group.children.all()),
20 20 ]
21 21 %>
22 22
23 23 <div class="panel panel-default">
24 24 <div class="panel-heading">
25 25 <h3 class="panel-title">${_('Repository Group Advanced: {}').format(c.repo_group.name)}</h3>
26 26 </div>
27 27 <div class="panel-body">
28 28 ${base.dt_info_panel(elems)}
29 29 </div>
30 30
31 31 </div>
32 32
33 33 <div class="panel panel-danger">
34 34 <div class="panel-heading">
35 35 <h3 class="panel-title">${_('Delete repository group')}</h3>
36 36 </div>
37 37 <div class="panel-body">
38 38 ${h.secure_form(h.route_path('edit_repo_group_advanced_delete', repo_group_name=c.repo_group.group_name), request=request)}
39 39 <table class="display">
40 40
41 41 <tr>
42 42 <td>
43 43 ${_ungettext('This repository group includes %s children repository group.', 'This repository group includes %s children repository groups.', c.repo_group.children.count()) % c.repo_group.children.count()}
44 44 </td>
45 45 <td>
46 46 </td>
47 47 <td>
48 48 </td>
49 49 </tr>
50 50 <tr>
51 51 <td>
52 52 ${_ungettext('This repository group includes %s repository.', 'This repository group includes %s repositories.', c.repo_group.repositories_recursive_count) % c.repo_group.repositories_recursive_count}
53 53 </td>
54 54 <td>
55 55 </td>
56 56 <td>
57 57 </td>
58 58 </tr>
59 59
60 60 </table>
61 61 <div style="margin: 0 0 20px 0" class="fake-space"></div>
62 62
63 63 <button class="btn btn-small btn-danger" type="submit"
64 64 onclick="return confirm('${_('Confirm to delete this group: %s') % (c.repo_group.group_name)}');">
65 65 ${_('Delete this repository group')}
66 66 </button>
67 67 ${h.end_form()}
68 68 </div>
69 69 </div>
70 70
71 71
@@ -1,219 +1,220 b''
1 1 <%namespace name="base" file="/base/base.mako"/>
2 2
3 3 <div class="panel panel-default">
4 4 <div class="panel-heading">
5 5 <h3 class="panel-title">${_('Repository Group Permissions: {}').format(c.repo_group.name)}</h3>
6 6 </div>
7 7 <div class="panel-body">
8 8 ${h.secure_form(h.route_path('edit_repo_group_perms_update', repo_group_name=c.repo_group.group_name), request=request)}
9 9 <table id="permissions_manage" class="rctable permissions">
10 10 <tr>
11 11 <th class="td-radio">${_('None')}</th>
12 12 <th class="td-radio">${_('Read')}</th>
13 13 <th class="td-radio">${_('Write')}</th>
14 14 <th class="td-radio">${_('Admin')}</th>
15 15 <th class="td-owner">${_('User/User Group')}</th>
16 16 <th class="td-action"></th>
17 17 <th class="td-action"></th>
18 18 </tr>
19 19 ## USERS
20 20 %for _user in c.repo_group.permissions():
21 21 ## super admin/owner row
22 22 %if getattr(_user, 'admin_row', None) or getattr(_user, 'owner_row', None):
23 23 <tr class="perm_admin_row">
24 24 <td class="td-radio">${h.radio('admin_perm_%s' % _user.user_id,'repository.none', disabled="disabled")}</td>
25 25 <td class="td-radio">${h.radio('admin_perm_%s' % _user.user_id,'repository.read', disabled="disabled")}</td>
26 26 <td class="td-radio">${h.radio('admin_perm_%s' % _user.user_id,'repository.write', disabled="disabled")}</td>
27 27 <td class="td-radio">${h.radio('admin_perm_%s' % _user.user_id,'repository.admin', 'repository.admin', disabled="disabled")}</td>
28 28 <td class="td-user">
29 ${base.gravatar(_user.email, 16)}
29 ${base.gravatar(_user.email, 16, user=_user, tooltip=True)}
30 30 ${h.link_to_user(_user.username)}
31 31 %if getattr(_user, 'admin_row', None):
32 32 (${_('super admin')})
33 33 %endif
34 34 %if getattr(_user, 'owner_row', None):
35 35 (${_('owner')})
36 36 %endif
37 37 </td>
38 38 <td></td>
39 39 <td class="quick_repo_menu">
40 40 % if c.rhodecode_user.is_admin:
41 41 <i class="icon-more"></i>
42 42 <div class="menu_items_container" style="display: none;">
43 43 <ul class="menu_items">
44 44 <li>
45 45 ${h.link_to('show permissions', h.route_path('edit_user_perms_summary', user_id=_user.user_id, _anchor='repositories-groups-permissions'))}
46 46 </li>
47 47 </ul>
48 48 </div>
49 49 % endif
50 50 </td>
51 51 </tr>
52 52 %else:
53 53 <tr>
54 54 ##forbid revoking permission from yourself, except if you're an super admin
55 55 %if c.rhodecode_user.user_id != _user.user_id or c.rhodecode_user.is_admin:
56 56 <td class="td-radio">${h.radio('u_perm_%s' % _user.user_id,'group.none', checked=_user.permission=='group.none')}</td>
57 57 <td class="td-radio">${h.radio('u_perm_%s' % _user.user_id,'group.read', checked=_user.permission=='group.read')}</td>
58 58 <td class="td-radio">${h.radio('u_perm_%s' % _user.user_id,'group.write', checked=_user.permission=='group.write')}</td>
59 59 <td class="td-radio">${h.radio('u_perm_%s' % _user.user_id,'group.admin', checked=_user.permission=='group.admin')}</td>
60 60 <td class="td-user">
61 ${base.gravatar(_user.email, 16)}
61 ${base.gravatar(_user.email, 16, user=_user, tooltip=True)}
62 62 <span class="user">
63 63 % if _user.username == h.DEFAULT_USER:
64 64 ${h.DEFAULT_USER} <span class="user-perm-help-text"> - ${_('permission for all other users')}</span>
65 65 % else:
66 66 ${h.link_to_user(_user.username)}
67 67 %if getattr(_user, 'duplicate_perm', None):
68 68 (${_('inactive duplicate')})
69 69 %endif
70 70 % endif
71 71 </span>
72 72 </td>
73 73 <td class="td-action">
74 74 %if _user.username != h.DEFAULT_USER:
75 75 <span class="btn btn-link btn-danger revoke_perm"
76 76 member="${_user.user_id}" member_type="user">
77 77 ${_('Remove')}
78 78 </span>
79 79 %endif
80 80 </td>
81 81 <td class="quick_repo_menu">
82 82 % if c.rhodecode_user.is_admin:
83 83 <i class="icon-more"></i>
84 84 <div class="menu_items_container" style="display: none;">
85 85 <ul class="menu_items">
86 86 <li>
87 87 % if _user.username == h.DEFAULT_USER:
88 88 ${h.link_to('show permissions', h.route_path('admin_permissions_overview', _anchor='repositories-groups-permissions'))}
89 89 % else:
90 90 ${h.link_to('show permissions', h.route_path('edit_user_perms_summary', user_id=_user.user_id, _anchor='repositories-groups-permissions'))}
91 91 % endif
92 92 </li>
93 93 </ul>
94 94 </div>
95 95 % endif
96 96 </td>
97 97 %else:
98 98 ## special case for currently logged-in user permissions, we make sure he cannot take his own permissions
99 99 <td class="td-radio">${h.radio('u_perm_%s' % _user.user_id,'group.none', disabled="disabled")}</td>
100 100 <td class="td-radio">${h.radio('u_perm_%s' % _user.user_id,'group.read', disabled="disabled")}</td>
101 101 <td class="td-radio">${h.radio('u_perm_%s' % _user.user_id,'group.write', disabled="disabled")}</td>
102 102 <td class="td-radio">${h.radio('u_perm_%s' % _user.user_id,'group.admin', disabled="disabled")}</td>
103 103 <td class="td-user">
104 ${base.gravatar(_user.email, 16)}
104 ${base.gravatar(_user.email, 16, user=_user, tooltip=True)}
105 105 <span class="user">
106 106 % if _user.username == h.DEFAULT_USER:
107 107 ${h.DEFAULT_USER} <span class="user-perm-help-text"> - ${_('permission for all other users')}</span>
108 108 % else:
109 109 ${h.link_to_user(_user.username)}
110 110 %if getattr(_user, 'duplicate_perm', None):
111 111 (${_('inactive duplicate')})
112 112 %endif
113 113 % endif
114 114 <span class="user-perm-help-text">(${_('delegated admin')})</span>
115 115 </span>
116 116 </td>
117 117 <td></td>
118 118 <td class="quick_repo_menu">
119 119 % if c.rhodecode_user.is_admin:
120 120 <i class="icon-more"></i>
121 121 <div class="menu_items_container" style="display: none;">
122 122 <ul class="menu_items">
123 123 <li>
124 124 ${h.link_to('show permissions', h.route_path('edit_user_perms_summary', user_id=_user.user_id, _anchor='repositories-groups-permissions'))}
125 125 </li>
126 126 </ul>
127 127 </div>
128 128 % endif
129 129 </td>
130 130 %endif
131 131 </tr>
132 132 %endif
133 133 %endfor
134 134
135 135 ## USER GROUPS
136 136 %for _user_group in c.repo_group.permission_user_groups(with_members=True):
137 137 <tr id="id${id(_user_group.users_group_name)}">
138 138 <td class="td-radio">${h.radio('g_perm_%s' % _user_group.users_group_id,'group.none', checked=_user_group.permission=='group.none')}</td>
139 139 <td class="td-radio">${h.radio('g_perm_%s' % _user_group.users_group_id,'group.read', checked=_user_group.permission=='group.read')}</td>
140 140 <td class="td-radio">${h.radio('g_perm_%s' % _user_group.users_group_id,'group.write', checked=_user_group.permission=='group.write')}</td>
141 141 <td class="td-radio">${h.radio('g_perm_%s' % _user_group.users_group_id,'group.admin', checked=_user_group.permission=='group.admin')}</td>
142 142 <td class="td-componentname">
143 <i class="icon-user-group"></i>
143 ${base.user_group_icon(_user_group, tooltip=True)}
144
144 145 %if c.is_super_admin:
145 146 <a href="${h.route_path('edit_user_group',user_group_id=_user_group.users_group_id)}">
146 147 ${_user_group.users_group_name}
147 148 </a>
148 149 %else:
149 150 ${h.link_to_group(_user_group.users_group_name)}
150 151 %endif
151 152 (${_('members')}: ${len(_user_group.members)})
152 153 </td>
153 154 <td class="td-action">
154 155 <span class="btn btn-link btn-danger revoke_perm"
155 156 member="${_user_group.users_group_id}" member_type="user_group">
156 157 ${_('Remove')}
157 158 </span>
158 159 </td>
159 160 <td class="quick_repo_menu">
160 161 % if c.rhodecode_user.is_admin:
161 162 <i class="icon-more"></i>
162 163 <div class="menu_items_container" style="display: none;">
163 164 <ul class="menu_items">
164 165 <li>
165 166 ${h.link_to('show permissions', h.route_path('edit_user_group_perms_summary', user_group_id=_user_group.users_group_id, _anchor='repositories-groups-permissions'))}
166 167 </li>
167 168 </ul>
168 169 </div>
169 170 % endif
170 171 </td>
171 172 </tr>
172 173 %endfor
173 174
174 175 <tr class="new_members" id="add_perm_input"></tr>
175 176 <tr>
176 177 <td></td>
177 178 <td></td>
178 179 <td></td>
179 180 <td></td>
180 181 <td></td>
181 182 <td>
182 183 <span id="add_perm" class="link">
183 184 ${_('Add user/user group')}
184 185 </span>
185 186 </td>
186 187 <td></td>
187 188 </tr>
188 189 </table>
189 190
190 191 <div class="fields">
191 192 <div class="field">
192 193 <div class="label label-radio">
193 194 ${_('Apply to children')}:
194 195 </div>
195 196 <div class="radios">
196 197 ${h.radio('recursive', 'none', label=_('None'), checked="checked")}
197 198 ${h.radio('recursive', 'groups', label=_('Repository Groups'))}
198 199 ${h.radio('recursive', 'repos', label=_('Repositories'))}
199 200 ${h.radio('recursive', 'all', label=_('Both'))}
200 201 <span class="help-block">${_('Set or revoke permissions to selected types of children of this group, including non-private repositories and other groups if chosen.')}</span>
201 202 </div>
202 203 </div>
203 204 </div>
204 205 <div class="buttons">
205 206 ${h.submit('save',_('Save'),class_="btn btn-primary")}
206 207 ${h.reset('reset',_('Reset'),class_="btn btn-danger")}
207 208 </div>
208 209 ${h.end_form()}
209 210 </div>
210 211 </div>
211 212 <script type="text/javascript">
212 213 $('#add_perm').on('click', function(e){
213 214 addNewPermInput($(this), 'group');
214 215 });
215 216 $('.revoke_perm').on('click', function(e){
216 217 markRevokePermInput($(this), 'group');
217 218 });
218 219 quick_repo_menu();
219 220 </script>
@@ -1,291 +1,291 b''
1 1 <%namespace name="base" file="/base/base.mako"/>
2 2
3 3 <%
4 4 elems = [
5 5 (_('Repository ID'), c.rhodecode_db_repo.repo_id, '', ''),
6 (_('Owner'), lambda:base.gravatar_with_user(c.rhodecode_db_repo.user.email), '', ''),
6 (_('Owner'), lambda:base.gravatar_with_user(c.rhodecode_db_repo.user.email, tooltip=True), '', ''),
7 7 (_('Created on'), h.format_date(c.rhodecode_db_repo.created_on), '', ''),
8 8 (_('Updated on'), h.format_date(c.rhodecode_db_repo.updated_on), '', ''),
9 9 (_('Cached Commit id'), lambda: h.link_to(c.rhodecode_db_repo.changeset_cache.get('short_id'), h.route_path('repo_commit',repo_name=c.repo_name,commit_id=c.rhodecode_db_repo.changeset_cache.get('raw_id'))), '', ''),
10 10 (_('Cached Commit date'), c.rhodecode_db_repo.changeset_cache.get('date'), '', ''),
11 11 (_('Attached scoped tokens'), len(c.rhodecode_db_repo.scoped_tokens), '', [x.user for x in c.rhodecode_db_repo.scoped_tokens]),
12 12 (_('Pull requests source'), len(c.rhodecode_db_repo.pull_requests_source), '', ['pr_id:{}, repo:{}'.format(x.pull_request_id,x.source_repo.repo_name) for x in c.rhodecode_db_repo.pull_requests_source]),
13 13 (_('Pull requests target'), len(c.rhodecode_db_repo.pull_requests_target), '', ['pr_id:{}, repo:{}'.format(x.pull_request_id,x.target_repo.repo_name) for x in c.rhodecode_db_repo.pull_requests_target]),
14 14 (_('Attached Artifacts'), len(c.rhodecode_db_repo.artifacts), '', ''),
15 15 ]
16 16 %>
17 17
18 18 <div class="panel panel-default">
19 19 <div class="panel-heading" id="advanced-info" >
20 20 <h3 class="panel-title">${_('Repository: %s') % c.rhodecode_db_repo.repo_name} <a class="permalink" href="#advanced-info"></a></h3>
21 21 </div>
22 22 <div class="panel-body">
23 23 ${base.dt_info_panel(elems)}
24 24 </div>
25 25 </div>
26 26
27 27
28 28 <div class="panel panel-default">
29 29 <div class="panel-heading" id="advanced-fork">
30 30 <h3 class="panel-title">${_('Fork Reference')} <a class="permalink" href="#advanced-fork"></a></h3>
31 31 </div>
32 32 <div class="panel-body">
33 33 ${h.secure_form(h.route_path('edit_repo_advanced_fork', repo_name=c.rhodecode_db_repo.repo_name), request=request)}
34 34
35 35 % if c.rhodecode_db_repo.fork:
36 36 <div class="panel-body-title-text">${h.literal(_('This repository is a fork of %(repo_link)s') % {'repo_link': h.link_to_if(c.has_origin_repo_read_perm,c.rhodecode_db_repo.fork.repo_name, h.route_path('repo_summary', repo_name=c.rhodecode_db_repo.fork.repo_name))})}
37 37 | <button class="btn btn-link btn-danger" type="submit">Remove fork reference</button></div>
38 38 % endif
39 39
40 40 <div class="field">
41 41 ${h.hidden('id_fork_of')}
42 42 ${h.submit('set_as_fork_%s' % c.rhodecode_db_repo.repo_name,_('Set'),class_="btn btn-small",)}
43 43 </div>
44 44 <div class="field">
45 45 <span class="help-block">${_('Manually set this repository as a fork of another from the list')}</span>
46 46 </div>
47 47 ${h.end_form()}
48 48 </div>
49 49 </div>
50 50
51 51
52 52 <div class="panel panel-default">
53 53 <div class="panel-heading" id="advanced-journal">
54 54 <h3 class="panel-title">${_('Public Journal Visibility')} <a class="permalink" href="#advanced-journal"></a></h3>
55 55 </div>
56 56 <div class="panel-body">
57 57 ${h.secure_form(h.route_path('edit_repo_advanced_journal', repo_name=c.rhodecode_db_repo.repo_name), request=request)}
58 58 <div class="field">
59 59 %if c.in_public_journal:
60 60 <button class="btn btn-small" type="submit">
61 61 ${_('Remove from Public Journal')}
62 62 </button>
63 63 %else:
64 64 <button class="btn btn-small" type="submit">
65 65 ${_('Add to Public Journal')}
66 66 </button>
67 67 %endif
68 68 </div>
69 69 <div class="field" >
70 70 <span class="help-block">${_('All actions made on this repository will be visible to everyone following the public journal.')}</span>
71 71 </div>
72 72 ${h.end_form()}
73 73 </div>
74 74 </div>
75 75
76 76
77 77 <div class="panel panel-default">
78 78 <div class="panel-heading" id="advanced-locking">
79 79 <h3 class="panel-title">${_('Locking state')} <a class="permalink" href="#advanced-locking"></a></h3>
80 80 </div>
81 81 <div class="panel-body">
82 82 ${h.secure_form(h.route_path('edit_repo_advanced_locking', repo_name=c.rhodecode_db_repo.repo_name), request=request)}
83 83
84 84 %if c.rhodecode_db_repo.locked[0]:
85 85 <div class="panel-body-title-text">${'Locked by %s on %s. Lock reason: %s' % (h.person_by_id(c.rhodecode_db_repo.locked[0]),
86 86 h.format_date(h. time_to_datetime(c.rhodecode_db_repo.locked[1])), c.rhodecode_db_repo.locked[2])}</div>
87 87 %else:
88 88 <div class="panel-body-title-text">${_('This Repository is not currently locked.')}</div>
89 89 %endif
90 90
91 91 <div class="field" >
92 92 %if c.rhodecode_db_repo.locked[0]:
93 93 ${h.hidden('set_unlock', '1')}
94 94 <button class="btn btn-small" type="submit"
95 95 onclick="return confirm('${_('Confirm to unlock repository.')}');">
96 96 <i class="icon-unlock"></i>
97 97 ${_('Unlock repository')}
98 98 </button>
99 99 %else:
100 100 ${h.hidden('set_lock', '1')}
101 101 <button class="btn btn-small" type="submit"
102 102 onclick="return confirm('${_('Confirm to lock repository.')}');">
103 103 <i class="icon-lock"></i>
104 104 ${_('Lock repository')}
105 105 </button>
106 106 %endif
107 107 </div>
108 108 <div class="field" >
109 109 <span class="help-block">
110 110 ${_('Force repository locking. This only works when anonymous access is disabled. Pulling from the repository locks the repository to that user until the same user pushes to that repository again.')}
111 111 </span>
112 112 </div>
113 113 ${h.end_form()}
114 114 </div>
115 115 </div>
116 116
117 117
118 118 <div class="panel panel-default">
119 119 <div class="panel-heading" id="advanced-hooks">
120 120 <h3 class="panel-title">${_('Hooks')} <a class="permalink" href="#advanced-hooks"></a></h3>
121 121 </div>
122 122 <div class="panel-body">
123 123 <table class="rctable">
124 124 <th>${_('Hook type')}</th>
125 125 <th>${_('Hook version')}</th>
126 126 <th>${_('Current version')}</th>
127 127 % if c.ver_info_dict:
128 128 <tr>
129 129 <td>${_('PRE HOOK')}</td>
130 130 <td>${c.ver_info_dict['pre_version']}</td>
131 131 <td>${c.rhodecode_version}</td>
132 132 </tr>
133 133 <tr>
134 134 <td>${_('POST HOOK')}</td>
135 135 <td>${c.ver_info_dict['post_version']}</td>
136 136 <td>${c.rhodecode_version}</td>
137 137 </tr>
138 138 % else:
139 139 <tr>
140 140 <td>${_('Unable to read hook information from VCS Server')}</td>
141 141 </tr>
142 142 % endif
143 143 </table>
144 144
145 145 <a href="${h.route_path('edit_repo_advanced_hooks', repo_name=c.repo_name)}"
146 146 onclick="return confirm('${_('Confirm to reinstall hooks for this repository.')}');">
147 147 ${_('Update Hooks')}
148 148 </a>
149 149 </div>
150 150 </div>
151 151
152 152 <div class="panel panel-warning">
153 153 <div class="panel-heading" id="advanced-archive">
154 154 <h3 class="panel-title">${_('Archive repository')} <a class="permalink" href="#advanced-archive"></a></h3>
155 155 </div>
156 156 <div class="panel-body">
157 157 ${h.secure_form(h.route_path('edit_repo_advanced_archive', repo_name=c.repo_name), request=request)}
158 158
159 159 <div style="margin: 0 0 20px 0" class="fake-space"></div>
160 160
161 161 <div class="field">
162 162 <button class="btn btn-small btn-danger" type="submit"
163 163 onclick="return confirm('${_('Confirm to archive this repository: %s') % c.repo_name}');">
164 164 <i class="icon-remove"></i>
165 165 ${_('Archive this repository')}
166 166 </button>
167 167 </div>
168 168 <div class="field">
169 169 <span class="help-block">
170 170 ${_('Archiving the repository will make it entirely read-only. The repository cannot be committed to.'
171 171 'It is hidden from the search results and dashboard. ')}
172 172 </span>
173 173 </div>
174 174
175 175 ${h.end_form()}
176 176 </div>
177 177 </div>
178 178
179 179
180 180 <div class="panel panel-danger">
181 181 <div class="panel-heading" id="advanced-delete">
182 182 <h3 class="panel-title">${_('Delete repository')} <a class="permalink" href="#advanced-delete"></a></h3>
183 183 </div>
184 184 <div class="panel-body">
185 185 ${h.secure_form(h.route_path('edit_repo_advanced_delete', repo_name=c.repo_name), request=request)}
186 186 <table class="display">
187 187 <tr>
188 188 <td>
189 189 ${_ungettext('This repository has %s fork.', 'This repository has %s forks.', c.rhodecode_db_repo.forks.count()) % c.rhodecode_db_repo.forks.count()}
190 190 </td>
191 191 <td>
192 192 %if c.rhodecode_db_repo.forks.count():
193 193 <input type="radio" name="forks" value="detach_forks" checked="checked"/> <label for="forks">${_('Detach forks')}</label>
194 194 %endif
195 195 </td>
196 196 <td>
197 197 %if c.rhodecode_db_repo.forks.count():
198 198 <input type="radio" name="forks" value="delete_forks"/> <label for="forks">${_('Delete forks')}</label>
199 199 %endif
200 200 </td>
201 201 </tr>
202 202 <% attached_prs = len(c.rhodecode_db_repo.pull_requests_source + c.rhodecode_db_repo.pull_requests_target) %>
203 203 % if c.rhodecode_db_repo.pull_requests_source or c.rhodecode_db_repo.pull_requests_target:
204 204 <tr>
205 205 <td>
206 206 ${_ungettext('This repository has %s attached pull request.', 'This repository has %s attached pull requests.', attached_prs) % attached_prs}
207 207 <br/>
208 208 ${_('Consider to archive this repository instead.')}
209 209 </td>
210 210 <td></td>
211 211 <td></td>
212 212 </tr>
213 213 % endif
214 214 </table>
215 215 <div style="margin: 0 0 20px 0" class="fake-space"></div>
216 216
217 217 <div class="field">
218 218 <button class="btn btn-small btn-danger" type="submit"
219 219 onclick="return confirm('${_('Confirm to delete this repository: %s') % c.repo_name}');">
220 220 <i class="icon-remove"></i>
221 221 ${_('Delete this repository')}
222 222 </button>
223 223 </div>
224 224 <div class="field">
225 225 <span class="help-block">
226 226 ${_('This repository will be renamed in a special way in order to make it inaccessible to RhodeCode Enterprise and its VCS systems. If you need to fully delete it from the file system, please do it manually, or with rhodecode-cleanup-repos command available in rhodecode-tools.')}
227 227 </span>
228 228 </div>
229 229
230 230 ${h.end_form()}
231 231 </div>
232 232 </div>
233 233
234 234
235 235 <script>
236 236
237 237 var currentRepoId = ${c.rhodecode_db_repo.repo_id};
238 238
239 239 var repoTypeFilter = function(data) {
240 240 var results = [];
241 241
242 242 if (!data.results[0]) {
243 243 return data
244 244 }
245 245
246 246 $.each(data.results[0].children, function() {
247 247 // filter out the SAME repo, it cannot be used as fork of itself
248 248 if (this.repo_id != currentRepoId) {
249 249 this.id = this.repo_id;
250 250 results.push(this)
251 251 }
252 252 });
253 253 data.results[0].children = results;
254 254 return data;
255 255 };
256 256
257 257 $("#id_fork_of").select2({
258 258 cachedDataSource: {},
259 259 minimumInputLength: 2,
260 260 placeholder: "${_('Change repository') if c.rhodecode_db_repo.fork else _('Pick repository')}",
261 261 dropdownAutoWidth: true,
262 262 containerCssClass: "drop-menu",
263 263 dropdownCssClass: "drop-menu-dropdown",
264 264 formatResult: formatRepoResult,
265 265 query: $.debounce(250, function(query){
266 266 self = this;
267 267 var cacheKey = query.term;
268 268 var cachedData = self.cachedDataSource[cacheKey];
269 269
270 270 if (cachedData) {
271 271 query.callback({results: cachedData.results});
272 272 } else {
273 273 $.ajax({
274 274 url: pyroutes.url('repo_list_data'),
275 275 data: {'query': query.term, repo_type: '${c.rhodecode_db_repo.repo_type}'},
276 276 dataType: 'json',
277 277 type: 'GET',
278 278 success: function(data) {
279 279 data = repoTypeFilter(data);
280 280 self.cachedDataSource[cacheKey] = data;
281 281 query.callback({results: data.results});
282 282 },
283 283 error: function(data, textStatus, errorThrown) {
284 284 alert("Error while fetching entries.\nError code {0} ({1}).".format(data.status, data.statusText));
285 285 }
286 286 })
287 287 }
288 288 })
289 289 });
290 290 </script>
291 291
@@ -1,222 +1,222 b''
1 1 <%namespace name="base" file="/base/base.mako"/>
2 2
3 3 <div class="panel panel-default">
4 4 <div class="panel-heading">
5 5 <h3 class="panel-title">${_('Repository Access Permissions')}</h3>
6 6 </div>
7 7 <div class="panel-body">
8 8 ${h.secure_form(h.route_path('edit_repo_perms', repo_name=c.repo_name), request=request)}
9 9 <table id="permissions_manage" class="rctable permissions">
10 10 <tr>
11 11 <th class="td-radio">${_('None')}</th>
12 12 <th class="td-radio">${_('Read')}</th>
13 13 <th class="td-radio">${_('Write')}</th>
14 14 <th class="td-radio">${_('Admin')}</th>
15 15 <th class="td-owner">${_('User/User Group')}</th>
16 16 <th class="td-action"></th>
17 17 <th class="td-action"></th>
18 18 </tr>
19 19 ## USERS
20 20 %for _user in c.rhodecode_db_repo.permissions():
21 21 %if getattr(_user, 'admin_row', None) or getattr(_user, 'owner_row', None):
22 22 <tr class="perm_admin_row">
23 23 <td class="td-radio">${h.radio('admin_perm_%s' % _user.user_id,'repository.none', disabled="disabled")}</td>
24 24 <td class="td-radio">${h.radio('admin_perm_%s' % _user.user_id,'repository.read', disabled="disabled")}</td>
25 25 <td class="td-radio">${h.radio('admin_perm_%s' % _user.user_id,'repository.write', disabled="disabled")}</td>
26 26 <td class="td-radio">${h.radio('admin_perm_%s' % _user.user_id,'repository.admin', 'repository.admin', disabled="disabled")}</td>
27 27 <td class="td-user">
28 ${base.gravatar(_user.email, 16)}
28 ${base.gravatar(_user.email, 16, user=_user, tooltip=True)}
29 29 ${h.link_to_user(_user.username)}
30 30 %if getattr(_user, 'admin_row', None):
31 31 (${_('super admin')})
32 32 %endif
33 33 %if getattr(_user, 'owner_row', None):
34 34 (${_('owner')})
35 35 %endif
36 36 </td>
37 37 <td></td>
38 38 <td class="quick_repo_menu">
39 39 % if c.rhodecode_user.is_admin:
40 40 <i class="icon-more"></i>
41 41 <div class="menu_items_container" style="display: none;">
42 42 <ul class="menu_items">
43 43 <li>
44 44 ${h.link_to('show permissions', h.route_path('edit_user_perms_summary', user_id=_user.user_id, _anchor='repositories-permissions'))}
45 45 </li>
46 46 </ul>
47 47 </div>
48 48 % endif
49 49 </td>
50 50 </tr>
51 51 %elif _user.username == h.DEFAULT_USER and c.rhodecode_db_repo.private:
52 52 <tr>
53 53 <td colspan="4">
54 54 <span class="private_repo_msg">
55 55 <strong title="${h.tooltip(_user.permission)}">${_('private repository')}</strong>
56 56 </span>
57 57 </td>
58 58 <td class="private_repo_msg">
59 59 ${base.gravatar(h.DEFAULT_USER_EMAIL, 16)}
60 60 ${h.DEFAULT_USER} - ${_('only users/user groups explicitly added here will have access')}</td>
61 61 <td></td>
62 62 <td class="quick_repo_menu">
63 63 % if c.rhodecode_user.is_admin:
64 64 <i class="icon-more"></i>
65 65 <div class="menu_items_container" style="display: none;">
66 66 <ul class="menu_items">
67 67 <li>
68 68 ${h.link_to('show permissions', h.route_path('admin_permissions_overview', _anchor='repositories-permissions'))}
69 69 </li>
70 70 </ul>
71 71 </div>
72 72 % endif
73 73 </td>
74 74 </tr>
75 75 %else:
76 76 <% used_by_n_rules = len(getattr(_user, 'branch_rules', None) or []) %>
77 77 <tr>
78 78 <td class="td-radio">${h.radio('u_perm_%s' % _user.user_id,'repository.none', checked=_user.permission=='repository.none', disabled="disabled" if (used_by_n_rules and _user.username != h.DEFAULT_USER) else None)}</td>
79 79 <td class="td-radio">${h.radio('u_perm_%s' % _user.user_id,'repository.read', checked=_user.permission=='repository.read', disabled="disabled" if (used_by_n_rules and _user.username != h.DEFAULT_USER) else None)}</td>
80 80 <td class="td-radio">${h.radio('u_perm_%s' % _user.user_id,'repository.write', checked=_user.permission=='repository.write')}</td>
81 81 <td class="td-radio">${h.radio('u_perm_%s' % _user.user_id,'repository.admin', checked=_user.permission=='repository.admin')}</td>
82 82 <td class="td-user">
83 ${base.gravatar(_user.email, 16)}
83 ${base.gravatar(_user.email, 16, user=_user, tooltip=True)}
84 84 <span class="user">
85 85 % if _user.username == h.DEFAULT_USER:
86 86 ${h.DEFAULT_USER} <span class="user-perm-help-text"> - ${_('permission for all other users')}</span>
87 87 % else:
88 88 ${h.link_to_user(_user.username)}
89 89 %if getattr(_user, 'duplicate_perm', None):
90 90 (${_('inactive duplicate')})
91 91 %endif
92 92 %if getattr(_user, 'branch_rules', None):
93 93 % if used_by_n_rules == 1:
94 94 (${_('used by {} branch rule, requires write+ permissions').format(used_by_n_rules)})
95 95 % else:
96 96 (${_('used by {} branch rules, requires write+ permissions').format(used_by_n_rules)})
97 97 % endif
98 98 %endif
99 99 % endif
100 100 </span>
101 101 </td>
102 102 <td class="td-action">
103 103 %if _user.username != h.DEFAULT_USER and getattr(_user, 'branch_rules', None) is None:
104 104 <span class="btn btn-link btn-danger revoke_perm"
105 105 member="${_user.user_id}" member_type="user">
106 106 ${_('Remove')}
107 107 </span>
108 108 %elif _user.username == h.DEFAULT_USER:
109 109 <span class="tooltip btn btn-link btn-default" onclick="enablePrivateRepo(); return false" title="${_('Private repositories are only visible to people explicitly added as collaborators.')}">
110 110 ${_('set private mode')}
111 111 </span>
112 112 %endif
113 113 </td>
114 114 <td class="quick_repo_menu">
115 115 % if c.rhodecode_user.is_admin:
116 116 <i class="icon-more"></i>
117 117 <div class="menu_items_container" style="display: none;">
118 118 <ul class="menu_items">
119 119 <li>
120 120 % if _user.username == h.DEFAULT_USER:
121 121 ${h.link_to('show permissions', h.route_path('admin_permissions_overview', _anchor='repositories-permissions'))}
122 122 % else:
123 123 ${h.link_to('show permissions', h.route_path('edit_user_perms_summary', user_id=_user.user_id, _anchor='repositories-permissions'))}
124 124 % endif
125 125 </li>
126 126 </ul>
127 127 </div>
128 128 % endif
129 129 </td>
130 130 </tr>
131 131 %endif
132 132 %endfor
133 133
134 134 ## USER GROUPS
135 135 %for _user_group in c.rhodecode_db_repo.permission_user_groups(with_members=True):
136 136 <tr>
137 137 <td class="td-radio">${h.radio('g_perm_%s' % _user_group.users_group_id,'repository.none', checked=_user_group.permission=='repository.none')}</td>
138 138 <td class="td-radio">${h.radio('g_perm_%s' % _user_group.users_group_id,'repository.read', checked=_user_group.permission=='repository.read')}</td>
139 139 <td class="td-radio">${h.radio('g_perm_%s' % _user_group.users_group_id,'repository.write', checked=_user_group.permission=='repository.write')}</td>
140 140 <td class="td-radio">${h.radio('g_perm_%s' % _user_group.users_group_id,'repository.admin', checked=_user_group.permission=='repository.admin')}</td>
141 141 <td class="td-componentname">
142 <i class="icon-user-group"></i>
142 ${base.user_group_icon(_user_group, tooltip=True)}
143 143 %if c.is_super_admin:
144 144 <a href="${h.route_path('edit_user_group',user_group_id=_user_group.users_group_id)}">
145 145 ${_user_group.users_group_name}
146 146 </a>
147 147 %else:
148 148 ${h.link_to_group(_user_group.users_group_name)}
149 149 %endif
150 150 (${_('members')}: ${len(_user_group.members)})
151 151 </td>
152 152 <td class="td-action">
153 153 <span class="btn btn-link btn-danger revoke_perm"
154 154 member="${_user_group.users_group_id}" member_type="user_group">
155 155 ${_('Remove')}
156 156 </span>
157 157 </td>
158 158 <td class="quick_repo_menu">
159 159 % if c.rhodecode_user.is_admin:
160 160 <i class="icon-more"></i>
161 161 <div class="menu_items_container" style="display: none;">
162 162 <ul class="menu_items">
163 163 <li>
164 164 ${h.link_to('show permissions', h.route_path('edit_user_group_perms_summary', user_group_id=_user_group.users_group_id, _anchor='repositories-permissions'))}
165 165 </li>
166 166 </ul>
167 167 </div>
168 168 % endif
169 169 </td>
170 170 </tr>
171 171 %endfor
172 172 <tr class="new_members" id="add_perm_input"></tr>
173 173
174 174 <tr>
175 175 <td></td>
176 176 <td></td>
177 177 <td></td>
178 178 <td></td>
179 179 <td></td>
180 180 <td>
181 181 <span id="add_perm" class="link">
182 182 ${_('Add user/user group')}
183 183 </span>
184 184 </td>
185 185 <td></td>
186 186 </tr>
187 187
188 188 </table>
189 189
190 190 <div class="buttons">
191 191 ${h.submit('save',_('Save'),class_="btn btn-primary")}
192 192 ${h.reset('reset',_('Reset'),class_="btn btn-danger")}
193 193 </div>
194 194 ${h.end_form()}
195 195 </div>
196 196 </div>
197 197
198 198 <script type="text/javascript">
199 199 $('#add_perm').on('click', function(e){
200 200 addNewPermInput($(this), 'repository');
201 201 });
202 202 $('.revoke_perm').on('click', function(e){
203 203 markRevokePermInput($(this), 'repository');
204 204 });
205 205 quick_repo_menu();
206 206
207 207 var enablePrivateRepo = function () {
208 208 var postData = {
209 209 'csrf_token': CSRF_TOKEN
210 210 };
211 211
212 212 var success = function(o) {
213 213 var defaultUrl = pyroutes.url('edit_repo_perms', {"repo_name": templateContext.repo_name});
214 214 window.location = o.redirect_url || defaultUrl;
215 215 };
216 216
217 217 ajaxPOST(
218 218 pyroutes.url('edit_repo_perms_set_private', {"repo_name": templateContext.repo_name}),
219 219 postData,
220 220 success);
221 221 }
222 222 </script>
@@ -1,207 +1,207 b''
1 1 <%namespace name="base" file="/base/base.mako"/>
2 2
3 3 <div class="panel panel-default">
4 4 <div class="panel-heading">
5 5 <h3 class="panel-title">${_('User Group Permissions')}</h3>
6 6 </div>
7 7 <div class="panel-body">
8 8 ${h.secure_form(h.route_path('edit_user_group_perms_update', user_group_id=c.user_group.users_group_id), request=request)}
9 9 <table id="permissions_manage" class="rctable permissions">
10 10 <tr>
11 11 <th class="td-radio">${_('None')}</th>
12 12 <th class="td-radio">${_('Read')}</th>
13 13 <th class="td-radio">${_('Write')}</th>
14 14 <th class="td-radio">${_('Admin')}</th>
15 15 <th>${_('User/User Group')}</th>
16 16 <th class="td-action"></th>
17 17 <th class="td-action"></th>
18 18 </tr>
19 19 ## USERS
20 20 %for _user in c.user_group.permissions():
21 21 ## super admin/owner row
22 22 %if getattr(_user, 'admin_row', None) or getattr(_user, 'owner_row', None):
23 23 <tr class="perm_admin_row">
24 24 <td class="td-radio">${h.radio('admin_perm_%s' % _user.user_id,'repository.none', disabled="disabled")}</td>
25 25 <td class="td-radio">${h.radio('admin_perm_%s' % _user.user_id,'repository.read', disabled="disabled")}</td>
26 26 <td class="td-radio">${h.radio('admin_perm_%s' % _user.user_id,'repository.write', disabled="disabled")}</td>
27 27 <td class="td-radio">${h.radio('admin_perm_%s' % _user.user_id,'repository.admin', 'repository.admin', disabled="disabled")}</td>
28 28 <td class="td-user">
29 ${base.gravatar(_user.email, 16)}
29 ${base.gravatar(_user.email, 16, user=_user, tooltip=True)}
30 30 <span class="user">
31 31 ${h.link_to_user(_user.username)}
32 32 %if getattr(_user, 'admin_row', None):
33 33 (${_('super admin')})
34 34 %endif
35 35 %if getattr(_user, 'owner_row', None):
36 36 (${_('owner')})
37 37 %endif
38 38 </span>
39 39 </td>
40 40 <td></td>
41 41 <td class="quick_repo_menu">
42 42 % if c.rhodecode_user.is_admin:
43 43 <i class="icon-more"></i>
44 44 <div class="menu_items_container" style="display: none;">
45 45 <ul class="menu_items">
46 46 <li>
47 47 ${h.link_to('show permissions', h.route_path('edit_user_perms_summary', user_id=_user.user_id, _anchor='user-groups-permissions'))}
48 48 </li>
49 49 </ul>
50 50 </div>
51 51 % endif
52 52 </td>
53 53 </tr>
54 54 %else:
55 55 ##forbid revoking permission from yourself, except if you're an super admin
56 56 <tr>
57 57 %if c.rhodecode_user.user_id != _user.user_id or c.rhodecode_user.is_admin:
58 58 <td class="td-radio">${h.radio('u_perm_%s' % _user.user_id,'usergroup.none')}</td>
59 59 <td class="td-radio">${h.radio('u_perm_%s' % _user.user_id,'usergroup.read')}</td>
60 60 <td class="td-radio">${h.radio('u_perm_%s' % _user.user_id,'usergroup.write')}</td>
61 61 <td class="td-radio">${h.radio('u_perm_%s' % _user.user_id,'usergroup.admin')}</td>
62 62 <td class="td-user">
63 ${base.gravatar(_user.email, 16)}
63 ${base.gravatar(_user.email, 16, user=_user, tooltip=True)}
64 64 <span class="user">
65 65 % if _user.username == h.DEFAULT_USER:
66 66 ${h.DEFAULT_USER} <span class="user-perm-help-text"> - ${_('permission for all other users')}</span>
67 67 % else:
68 68 ${h.link_to_user(_user.username)}
69 69 %if getattr(_user, 'duplicate_perm', None):
70 70 (${_('inactive duplicate')})
71 71 %endif
72 72 % endif
73 73 </span>
74 74 </td>
75 75 <td class="td-action">
76 76 %if _user.username != h.DEFAULT_USER:
77 77 <span class="btn btn-link btn-danger revoke_perm"
78 78 member="${_user.user_id}" member_type="user">
79 79 ${_('Remove')}
80 80 </span>
81 81 %endif
82 82 </td>
83 83 <td class="quick_repo_menu">
84 84 % if c.rhodecode_user.is_admin:
85 85 <i class="icon-more"></i>
86 86 <div class="menu_items_container" style="display: none;">
87 87 <ul class="menu_items">
88 88 <li>
89 89 % if _user.username == h.DEFAULT_USER:
90 90 ${h.link_to('show permissions', h.route_path('admin_permissions_overview', _anchor='user-groups-permissions'))}
91 91 % else:
92 92 ${h.link_to('show permissions', h.route_path('edit_user_perms_summary', user_id=_user.user_id, _anchor='user-groups-permissions'))}
93 93 % endif
94 94 </li>
95 95 </ul>
96 96 </div>
97 97 % endif
98 98 </td>
99 99 %else:
100 100 ## special case for currently logged-in user permissions, we make sure he cannot take his own permissions
101 101 <td class="td-radio">${h.radio('u_perm_%s' % _user.user_id,'usergroup.none', disabled="disabled")}</td>
102 102 <td class="td-radio">${h.radio('u_perm_%s' % _user.user_id,'usergroup.read', disabled="disabled")}</td>
103 103 <td class="td-radio">${h.radio('u_perm_%s' % _user.user_id,'usergroup.write', disabled="disabled")}</td>
104 104 <td class="td-radio">${h.radio('u_perm_%s' % _user.user_id,'usergroup.admin', disabled="disabled")}</td>
105 105 <td class="td-user">
106 ${base.gravatar(_user.email, 16)}
106 ${base.gravatar(_user.email, 16, user=_user, tooltip=True)}
107 107 <span class="user">
108 108 % if _user.username == h.DEFAULT_USER:
109 109 ${h.DEFAULT_USER} <span class="user-perm-help-text"> - ${_('permission for all other users')}</span>
110 110 % else:
111 111 ${h.link_to_user(_user.username)}
112 112 %if getattr(_user, 'duplicate_perm', None):
113 113 (${_('inactive duplicate')})
114 114 %endif
115 115 % endif
116 116 <span class="user-perm-help-text">(${_('delegated admin')})</span>
117 117 </span>
118 118 </td>
119 119 <td></td>
120 120 <td class="quick_repo_menu">
121 121 % if c.rhodecode_user.is_admin:
122 122 <i class="icon-more"></i>
123 123 <div class="menu_items_container" style="display: none;">
124 124 <ul class="menu_items">
125 125 <li>
126 126 ${h.link_to('show permissions', h.route_path('edit_user_perms_summary', user_id=_user.user_id, _anchor='user-groups-permissions'))}
127 127 </li>
128 128 </ul>
129 129 </div>
130 130 % endif
131 131 </td>
132 132 %endif
133 133 </tr>
134 134 %endif
135 135 %endfor
136 136
137 137 ## USER GROUPS
138 138 %for _user_group in c.user_group.permission_user_groups(with_members=True):
139 139 <tr>
140 140 <td class="td-radio">${h.radio('g_perm_%s' % _user_group.users_group_id,'usergroup.none')}</td>
141 141 <td class="td-radio">${h.radio('g_perm_%s' % _user_group.users_group_id,'usergroup.read')}</td>
142 142 <td class="td-radio">${h.radio('g_perm_%s' % _user_group.users_group_id,'usergroup.write')}</td>
143 143 <td class="td-radio">${h.radio('g_perm_%s' % _user_group.users_group_id,'usergroup.admin')}</td>
144 144 <td class="td-user">
145 145 <i class="icon-user-group"></i>
146 146 %if c.is_super_admin:
147 147 <a href="${h.route_path('edit_user_group',user_group_id=_user_group.users_group_id)}">
148 148 ${_user_group.users_group_name}
149 149 </a>
150 150 %else:
151 151 ${h.link_to_group(_user_group.users_group_name)}
152 152 %endif
153 153 (${_('members')}: ${len(_user_group.members)})
154 154 </td>
155 155 <td class="td-action">
156 156 <span class="btn btn-link btn-danger revoke_perm"
157 157 member="${_user_group.users_group_id}" member_type="user_group">
158 158 ${_('Remove')}
159 159 </span>
160 160 </td>
161 161 <td class="quick_repo_menu">
162 162 % if c.rhodecode_user.is_admin:
163 163 <i class="icon-more"></i>
164 164 <div class="menu_items_container" style="display: none;">
165 165 <ul class="menu_items">
166 166 <li>
167 167 ${h.link_to('show permissions', h.route_path('edit_user_group_perms_summary', user_group_id=_user_group.users_group_id, _anchor='user-groups-permissions'))}
168 168 </li>
169 169 </ul>
170 170 </div>
171 171 % endif
172 172 </td>
173 173 </tr>
174 174 %endfor
175 175 <tr class="new_members" id="add_perm_input"></tr>
176 176 <tr>
177 177 <td></td>
178 178 <td></td>
179 179 <td></td>
180 180 <td></td>
181 181 <td></td>
182 182 <td>
183 183 <span id="add_perm" class="link">
184 184 ${_('Add user/user group')}
185 185 </span>
186 186 </td>
187 187 <td></td>
188 188 </tr>
189 189 </table>
190 190
191 191 <div class="buttons">
192 192 ${h.submit('save',_('Save'),class_="btn btn-primary")}
193 193 ${h.reset('reset',_('Reset'),class_="btn btn-danger")}
194 194 </div>
195 195 ${h.end_form()}
196 196 </div>
197 197 </div>
198 198
199 199 <script type="text/javascript">
200 200 $('#add_perm').on('click', function(e){
201 201 addNewPermInput($(this), 'usergroup');
202 202 });
203 203 $('.revoke_perm').on('click', function(e){
204 204 markRevokePermInput($(this), 'usergroup');
205 205 });
206 206 quick_repo_menu()
207 207 </script>
1 NO CONTENT: modified file
The requested commit or file is too big and content was truncated. Show full diff
1 NO CONTENT: modified file
The requested commit or file is too big and content was truncated. Show full diff
1 NO CONTENT: modified file
The requested commit or file is too big and content was truncated. Show full diff
1 NO CONTENT: modified file
The requested commit or file is too big and content was truncated. Show full diff
1 NO CONTENT: modified file
The requested commit or file is too big and content was truncated. Show full diff
1 NO CONTENT: modified file
The requested commit or file is too big and content was truncated. Show full diff
1 NO CONTENT: modified file
The requested commit or file is too big and content was truncated. Show full diff
1 NO CONTENT: modified file
The requested commit or file is too big and content was truncated. Show full diff
1 NO CONTENT: modified file
The requested commit or file is too big and content was truncated. Show full diff
1 NO CONTENT: modified file
The requested commit or file is too big and content was truncated. Show full diff
1 NO CONTENT: modified file
The requested commit or file is too big and content was truncated. Show full diff
1 NO CONTENT: modified file
The requested commit or file is too big and content was truncated. Show full diff
1 NO CONTENT: modified file
The requested commit or file is too big and content was truncated. Show full diff
1 NO CONTENT: modified file
The requested commit or file is too big and content was truncated. Show full diff
1 NO CONTENT: modified file
The requested commit or file is too big and content was truncated. Show full diff
1 NO CONTENT: modified file
The requested commit or file is too big and content was truncated. Show full diff
1 NO CONTENT: modified file
The requested commit or file is too big and content was truncated. Show full diff
1 NO CONTENT: modified file
The requested commit or file is too big and content was truncated. Show full diff
1 NO CONTENT: modified file
The requested commit or file is too big and content was truncated. Show full diff
1 NO CONTENT: modified file
The requested commit or file is too big and content was truncated. Show full diff
1 NO CONTENT: modified file
The requested commit or file is too big and content was truncated. Show full diff
1 NO CONTENT: modified file
The requested commit or file is too big and content was truncated. Show full diff
1 NO CONTENT: modified file
The requested commit or file is too big and content was truncated. Show full diff
1 NO CONTENT: modified file
The requested commit or file is too big and content was truncated. Show full diff
1 NO CONTENT: modified file
The requested commit or file is too big and content was truncated. Show full diff
1 NO CONTENT: modified file
The requested commit or file is too big and content was truncated. Show full diff
1 NO CONTENT: modified file
The requested commit or file is too big and content was truncated. Show full diff
1 NO CONTENT: modified file
The requested commit or file is too big and content was truncated. Show full diff
1 NO CONTENT: modified file
The requested commit or file is too big and content was truncated. Show full diff
1 NO CONTENT: modified file
The requested commit or file is too big and content was truncated. Show full diff
General Comments 0
You need to be logged in to leave comments. Login now