##// END OF EJS Templates
release: Merge default into stable for release preparation
marcink -
r4491:f919670e merge stable
parent child Browse files
Show More

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

@@ -0,0 +1,18 b''
1 diff -rup Beaker-1.9.1-orig/beaker/session.py Beaker-1.9.1/beaker/session.py
2 --- Beaker-1.9.1-orig/beaker/session.py 2020-04-10 10:23:04.000000000 +0200
3 +++ Beaker-1.9.1/beaker/session.py 2020-04-10 10:23:34.000000000 +0200
4 @@ -156,6 +156,14 @@ def __init__(self, request, id=None, invalidate_corrupt=False,
5 if timeout and not save_accessed_time:
6 raise BeakerException("timeout requires save_accessed_time")
7 self.timeout = timeout
8 + # We want to pass timeout param to redis backend to support expiration of keys
9 + # In future, I believe, we can use this param for memcached and mongo as well
10 + if self.timeout is not None and self.type == 'ext:redis':
11 + # The backend expiration should always be a bit longer (I decied to use 2 minutes) than the
12 + # session expiration itself to prevent the case where the backend data expires while
13 + # the session is being read (PR#153)
14 + self.namespace_args['timeout'] = self.timeout + 60 * 2
15 +
16 self.save_atime = save_accessed_time
17 self.use_cookies = use_cookies
18 self.cookie_expires = cookie_expires No newline at end of file
@@ -0,0 +1,26 b''
1 diff -rup Beaker-1.9.1-orig/beaker/ext/redisnm.py Beaker-1.9.1/beaker/ext/redisnm.py
2 --- Beaker-1.9.1-orig/beaker/ext/redisnm.py 2018-04-10 10:23:04.000000000 +0200
3 +++ Beaker-1.9.1/beaker/ext/redisnm.py 2018-04-10 10:23:34.000000000 +0200
4 @@ -30,9 +30,10 @@ class RedisNamespaceManager(NamespaceManager):
5
6 clients = SyncDict()
7
8 - def __init__(self, namespace, url, **kw):
9 + def __init__(self, namespace, url, timeout=None, **kw):
10 super(RedisNamespaceManager, self).__init__(namespace)
11 self.lock_dir = None # Redis uses redis itself for locking.
12 + self.timeout = timeout
13
14 if redis is None:
15 raise RuntimeError('redis is not available')
16 @@ -68,6 +69,8 @@ def has_key(self, key):
17
18 def set_value(self, key, value, expiretime=None):
19 value = pickle.dumps(value)
20 + if expiretime is None and self.timeout is not None:
21 + expiretime = self.timeout
22 if expiretime is not None:
23 self.client.setex(self._format_key(key), int(expiretime), value)
24 else:
25
26
1 NO CONTENT: new file 100644
The requested commit or file is too big and content was truncated. Show full diff
@@ -0,0 +1,52 b''
1 # -*- coding: utf-8 -*-
2
3 import logging
4 from sqlalchemy import *
5
6 from alembic.migration import MigrationContext
7 from alembic.operations import Operations
8
9 from rhodecode.lib.dbmigrate.versions import _reset_base
10 from rhodecode.model import meta, init_model_encryption
11
12
13 log = logging.getLogger(__name__)
14
15
16 def upgrade(migrate_engine):
17 """
18 Upgrade operations go here.
19 Don't create your own engine; bind migrate_engine to your metadata
20 """
21 _reset_base(migrate_engine)
22 from rhodecode.lib.dbmigrate.schema import db_4_20_0_0 as db
23
24 init_model_encryption(db)
25
26 context = MigrationContext.configure(migrate_engine.connect())
27 op = Operations(context)
28
29 table = db.PullRequestReviewers.__table__
30 with op.batch_alter_table(table.name) as batch_op:
31 new_column = Column('role', Unicode(255), nullable=True)
32 batch_op.add_column(new_column)
33
34 _fill_reviewers_role(db, op, meta.Session)
35
36
37 def downgrade(migrate_engine):
38 meta = MetaData()
39 meta.bind = migrate_engine
40
41
42 def fixups(models, _SESSION):
43 pass
44
45
46 def _fill_reviewers_role(models, op, session):
47 params = {'role': 'reviewer'}
48 query = text(
49 'UPDATE pull_request_reviewers SET role = :role'
50 ).bindparams(**params)
51 op.execute(query)
52 session().commit()
@@ -0,0 +1,142 b''
1 ## snippet for sidebar elements
2 ## usage:
3 ## <%namespace name="sidebar" file="/base/sidebar.mako"/>
4 ## ${sidebar.comments_table()}
5 <%namespace name="base" file="/base/base.mako"/>
6
7 <%def name="comments_table(comments, counter_num, todo_comments=False, existing_ids=None, is_pr=True)">
8 <%
9 if todo_comments:
10 cls_ = 'todos-content-table'
11 def sorter(entry):
12 user_id = entry.author.user_id
13 resolved = '1' if entry.resolved else '0'
14 if user_id == c.rhodecode_user.user_id:
15 # own comments first
16 user_id = 0
17 return '{}'.format(str(entry.comment_id).zfill(10000))
18 else:
19 cls_ = 'comments-content-table'
20 def sorter(entry):
21 user_id = entry.author.user_id
22 return '{}'.format(str(entry.comment_id).zfill(10000))
23
24 existing_ids = existing_ids or []
25
26 %>
27
28 <table class="todo-table ${cls_}" data-total-count="${len(comments)}" data-counter="${counter_num}">
29
30 % for loop_obj, comment_obj in h.looper(reversed(sorted(comments, key=sorter))):
31 <%
32 display = ''
33 _cls = ''
34 %>
35
36 <%
37 comment_ver_index = comment_obj.get_index_version(getattr(c, 'versions', []))
38 prev_comment_ver_index = 0
39 if loop_obj.previous:
40 prev_comment_ver_index = loop_obj.previous.get_index_version(getattr(c, 'versions', []))
41
42 ver_info = None
43 if getattr(c, 'versions', []):
44 ver_info = c.versions[comment_ver_index-1] if comment_ver_index else None
45 %>
46 <% hidden_at_ver = comment_obj.outdated_at_version_js(c.at_version_num) %>
47 <% is_from_old_ver = comment_obj.older_than_version_js(c.at_version_num) %>
48 <%
49 if (prev_comment_ver_index > comment_ver_index):
50 comments_ver_divider = comment_ver_index
51 else:
52 comments_ver_divider = None
53 %>
54
55 % if todo_comments:
56 % if comment_obj.resolved:
57 <% _cls = 'resolved-todo' %>
58 <% display = 'none' %>
59 % endif
60 % else:
61 ## SKIP TODOs we display them in other area
62 % if comment_obj.is_todo:
63 <% display = 'none' %>
64 % endif
65 ## Skip outdated comments
66 % if comment_obj.outdated:
67 <% display = 'none' %>
68 <% _cls = 'hidden-comment' %>
69 % endif
70 % endif
71
72 % if not todo_comments and comments_ver_divider:
73 <tr class="old-comments-marker">
74 <td colspan="3">
75 % if ver_info:
76 <code>v${comments_ver_divider} ${h.age_component(ver_info.created_on, time_is_local=True, tooltip=False)}</code>
77 % else:
78 <code>v${comments_ver_divider}</code>
79 % endif
80 </td>
81 </tr>
82
83 % endif
84
85 <tr class="${_cls}" style="display: ${display};" data-sidebar-comment-id="${comment_obj.comment_id}">
86 <td class="td-todo-number">
87 <%
88 version_info = ''
89 if is_pr:
90 version_info = (' made in older version (v{})'.format(comment_ver_index) if is_from_old_ver == 'true' else ' made in this version')
91 %>
92
93 <script type="text/javascript">
94 // closure function helper
95 var sidebarComment${comment_obj.comment_id} = function() {
96 return renderTemplate('sideBarCommentHovercard', {
97 version_info: "${version_info}",
98 file_name: "${comment_obj.f_path}",
99 line_no: "${comment_obj.line_no}",
100 outdated: ${h.json.dumps(comment_obj.outdated)},
101 inline: ${h.json.dumps(comment_obj.is_inline)},
102 is_todo: ${h.json.dumps(comment_obj.is_todo)},
103 created_on: "${h.format_date(comment_obj.created_on)}",
104 datetime: "${comment_obj.created_on}${h.get_timezone(comment_obj.created_on, time_is_local=True)}",
105 review_status: "${(comment_obj.review_status or '')}"
106 })
107 }
108 </script>
109
110 % if comment_obj.outdated:
111 <i class="icon-comment-toggle tooltip-hovercard" data-hovercard-url="javascript:sidebarComment${comment_obj.comment_id}()"></i>
112 % elif comment_obj.is_inline:
113 <i class="icon-code tooltip-hovercard" data-hovercard-url="javascript:sidebarComment${comment_obj.comment_id}()"></i>
114 % else:
115 <i class="icon-comment tooltip-hovercard" data-hovercard-url="javascript:sidebarComment${comment_obj.comment_id}()"></i>
116 % endif
117
118 ## NEW, since refresh
119 % if existing_ids and comment_obj.comment_id not in existing_ids:
120 <span class="tag">NEW</span>
121 % endif
122 </td>
123
124 <td class="td-todo-gravatar">
125 ${base.gravatar(comment_obj.author.email, 16, user=comment_obj.author, tooltip=True, extra_class=['no-margin'])}
126 </td>
127 <td class="todo-comment-text-wrapper">
128 <div class="todo-comment-text ${('todo-resolved' if comment_obj.resolved else '')}">
129 <a class="${('todo-resolved' if comment_obj.resolved else '')} permalink"
130 href="#comment-${comment_obj.comment_id}"
131 onclick="return Rhodecode.comments.scrollToComment($('#comment-${comment_obj.comment_id}'), 0, ${hidden_at_ver})">
132
133 ${h.chop_at_smart(comment_obj.text, '\n', suffix_if_chopped='...')}
134 </a>
135 </div>
136 </td>
137 </tr>
138 % endfor
139
140 </table>
141
142 </%def> No newline at end of file
@@ -1,6 +1,5 b''
1 1 [bumpversion]
2 current_version = 4.20.1
2 current_version = 4.21.0
3 3 message = release: Bump version {current_version} to {new_version}
4 4
5 5 [bumpversion:file:rhodecode/VERSION]
6
@@ -5,25 +5,20 b' done = false'
5 5 done = true
6 6
7 7 [task:rc_tools_pinned]
8 done = true
9 8
10 9 [task:fixes_on_stable]
11 done = true
12 10
13 11 [task:pip2nix_generated]
14 done = true
15 12
16 13 [task:changelog_updated]
17 done = true
18 14
19 15 [task:generate_api_docs]
20 done = true
16
17 [task:updated_translation]
21 18
22 19 [release]
23 state = prepared
24 version = 4.20.1
25
26 [task:updated_translation]
20 state = in_progress
21 version = 4.21.0
27 22
28 23 [task:generate_js_routes]
29 24
@@ -147,12 +147,13 b' Use the following example to configure N'
147 147
148 148 ## Special Cache for file store, make sure you enable this intentionally as
149 149 ## it could bypass upload files permissions
150 # location /_file_store/download {
150 # location /_file_store/download/gravatars {
151 151 #
152 152 # proxy_cache cache_zone;
153 153 # # ignore Set-Cookie
154 154 # proxy_ignore_headers Set-Cookie;
155 # proxy_ignore_headers Cookie;
155 # # ignore cache-control
156 # proxy_ignore_headers Cache-Control;
156 157 #
157 158 # proxy_cache_key $host$uri$is_args$args;
158 159 # proxy_cache_methods GET;
@@ -32,6 +32,8 b' self: super: {'
32 32 patches = [
33 33 ./patches/beaker/patch-beaker-lock-func-debug.diff
34 34 ./patches/beaker/patch-beaker-metadata-reuse.diff
35 ./patches/beaker/patch-beaker-improved-redis.diff
36 ./patches/beaker/patch-beaker-improved-redis-2.diff
35 37 ];
36 38 });
37 39
@@ -35,6 +35,20 b' self: super: {'
35 35 license = [ pkgs.lib.licenses.bsdOriginal ];
36 36 };
37 37 };
38 "apispec" = super.buildPythonPackage {
39 name = "apispec-1.0.0";
40 doCheck = false;
41 propagatedBuildInputs = [
42 self."PyYAML"
43 ];
44 src = fetchurl {
45 url = "https://files.pythonhosted.org/packages/67/15/346c04988dd67d36007e28145504c520491930c878b1f484a97b27a8f497/apispec-1.0.0.tar.gz";
46 sha256 = "1712w1anvqrvadjjpvai84vbaygaxabd3zz5lxihdzwzs4gvi9sp";
47 };
48 meta = {
49 license = [ pkgs.lib.licenses.mit ];
50 };
51 };
38 52 "appenlight-client" = super.buildPythonPackage {
39 53 name = "appenlight-client-0.6.26";
40 54 doCheck = false;
@@ -236,20 +250,23 b' self: super: {'
236 250 };
237 251 };
238 252 "channelstream" = super.buildPythonPackage {
239 name = "channelstream-0.5.2";
253 name = "channelstream-0.6.14";
240 254 doCheck = false;
241 255 propagatedBuildInputs = [
242 256 self."gevent"
243 257 self."ws4py"
258 self."marshmallow"
259 self."python-dateutil"
244 260 self."pyramid"
245 261 self."pyramid-jinja2"
262 self."pyramid-apispec"
246 263 self."itsdangerous"
247 264 self."requests"
248 265 self."six"
249 266 ];
250 267 src = fetchurl {
251 url = "https://files.pythonhosted.org/packages/2b/31/29a8e085cf5bf97fa88e7b947adabfc581a18a3463adf77fb6dada34a65f/channelstream-0.5.2.tar.gz";
252 sha256 = "1qbm4xdl5hfkja683x546bncg3rqq8qv79w1m1a1wd48cqqzb6rm";
268 url = "https://files.pythonhosted.org/packages/d4/2d/86d6757ccd06ce673ee224123471da3d45251d061da7c580bfc259bad853/channelstream-0.6.14.tar.gz";
269 sha256 = "0qgy5j3rj6c8cslzidh32glhkrhbbdxjc008y69v8a0y3zyaz2d3";
253 270 };
254 271 meta = {
255 272 license = [ pkgs.lib.licenses.bsdOriginal ];
@@ -862,11 +879,11 b' self: super: {'
862 879 };
863 880 };
864 881 "itsdangerous" = super.buildPythonPackage {
865 name = "itsdangerous-0.24";
882 name = "itsdangerous-1.1.0";
866 883 doCheck = false;
867 884 src = fetchurl {
868 url = "https://files.pythonhosted.org/packages/dc/b4/a60bcdba945c00f6d608d8975131ab3f25b22f2bcfe1dab221165194b2d4/itsdangerous-0.24.tar.gz";
869 sha256 = "06856q6x675ly542ig0plbqcyab6ksfzijlyf1hzhgg3sgwgrcyb";
885 url = "https://files.pythonhosted.org/packages/68/1a/f27de07a8a304ad5fa817bbe383d1238ac4396da447fa11ed937039fa04b/itsdangerous-1.1.0.tar.gz";
886 sha256 = "068zpbksq5q2z4dckh2k1zbcq43ay74ylqn77rni797j0wyh66rj";
870 887 };
871 888 meta = {
872 889 license = [ pkgs.lib.licenses.bsdOriginal ];
@@ -993,6 +1010,17 b' self: super: {'
993 1010 license = [ pkgs.lib.licenses.bsdOriginal pkgs.lib.licenses.bsd3 ];
994 1011 };
995 1012 };
1013 "marshmallow" = super.buildPythonPackage {
1014 name = "marshmallow-2.18.0";
1015 doCheck = false;
1016 src = fetchurl {
1017 url = "https://files.pythonhosted.org/packages/ad/0b/5799965d1c6d5f608d684e2c0dce8a828e0309a3bfe8327d9418a89f591c/marshmallow-2.18.0.tar.gz";
1018 sha256 = "1g0aafpjn7yaxq06yndy8c7rs9n42adxkqq1ayhlr869pr06d3lm";
1019 };
1020 meta = {
1021 license = [ pkgs.lib.licenses.mit ];
1022 };
1023 };
996 1024 "mistune" = super.buildPythonPackage {
997 1025 name = "mistune-0.8.4";
998 1026 doCheck = false;
@@ -1522,6 +1550,20 b' self: super: {'
1522 1550 license = [ { fullName = "Repoze Public License"; } { fullName = "BSD-derived (http://www.repoze.org/LICENSE.txt)"; } ];
1523 1551 };
1524 1552 };
1553 "pyramid-apispec" = super.buildPythonPackage {
1554 name = "pyramid-apispec-0.3.2";
1555 doCheck = false;
1556 propagatedBuildInputs = [
1557 self."apispec"
1558 ];
1559 src = fetchurl {
1560 url = "https://files.pythonhosted.org/packages/2a/30/1dea5d81ea635449572ba60ec3148310d75ae4530c3c695f54b0991bb8c7/pyramid_apispec-0.3.2.tar.gz";
1561 sha256 = "0ffrcqp9dkykivhfcq0v9lgy6w0qhwl6x78925vfjmayly9r8da0";
1562 };
1563 meta = {
1564 license = [ pkgs.lib.licenses.bsdOriginal ];
1565 };
1566 };
1525 1567 "pyramid-mailer" = super.buildPythonPackage {
1526 1568 name = "pyramid-mailer-0.15.1";
1527 1569 doCheck = false;
@@ -1763,6 +1805,17 b' self: super: {'
1763 1805 license = [ pkgs.lib.licenses.bsdOriginal { fullName = "LGPL+BSD"; } { fullName = "GNU Library or Lesser General Public License (LGPL)"; } ];
1764 1806 };
1765 1807 };
1808 "PyYAML" = super.buildPythonPackage {
1809 name = "PyYAML-5.3.1";
1810 doCheck = false;
1811 src = fetchurl {
1812 url = "https://files.pythonhosted.org/packages/64/c2/b80047c7ac2478f9501676c988a5411ed5572f35d1beff9cae07d321512c/PyYAML-5.3.1.tar.gz";
1813 sha256 = "0pb4zvkfxfijkpgd1b86xjsqql97ssf1knbd1v53wkg1qm9cgsmq";
1814 };
1815 meta = {
1816 license = [ pkgs.lib.licenses.mit ];
1817 };
1818 };
1766 1819 "redis" = super.buildPythonPackage {
1767 1820 name = "redis-3.4.1";
1768 1821 doCheck = false;
@@ -1819,7 +1872,7 b' self: super: {'
1819 1872 };
1820 1873 };
1821 1874 "rhodecode-enterprise-ce" = super.buildPythonPackage {
1822 name = "rhodecode-enterprise-ce-4.20.1";
1875 name = "rhodecode-enterprise-ce-4.20.0";
1823 1876 buildInputs = [
1824 1877 self."pytest"
1825 1878 self."py"
@@ -5,7 +5,7 b' babel==1.3'
5 5 beaker==1.9.1
6 6 bleach==3.1.3
7 7 celery==4.3.0
8 channelstream==0.5.2
8 channelstream==0.6.14
9 9 click==7.0
10 10 colander==1.7.0
11 11 # our custom configobj
@@ -22,7 +22,7 b' future==0.14.3'
22 22 futures==3.0.2
23 23 infrae.cache==1.0.1
24 24 iso8601==0.1.12
25 itsdangerous==0.24
25 itsdangerous==1.1.0
26 26 kombu==4.6.6
27 27 lxml==4.2.5
28 28 mako==1.1.0
@@ -18,10 +18,11 b' jsonschema==2.6.0'
18 18 pluggy==0.13.1
19 19 pyasn1-modules==0.2.6
20 20 pyramid-jinja2==2.7
21 pyramid-apispec==0.3.2
21 22 scandir==1.10.0
22 23 setproctitle==1.1.10
23 24 tempita==0.5.2
24 25 testpath==0.4.4
25 26 transaction==2.4.0
26 27 vine==1.3.0
27 wcwidth==0.1.9
28 wcwidth==0.1.9 No newline at end of file
@@ -1,1 +1,1 b''
1 4.20.1 No newline at end of file
1 4.21.0 No newline at end of file
@@ -48,7 +48,7 b' PYRAMID_SETTINGS = {}'
48 48 EXTENSIONS = {}
49 49
50 50 __version__ = ('.'.join((str(each) for each in VERSION[:3])))
51 __dbversion__ = 108 # defines current db version for migrations
51 __dbversion__ = 109 # defines current db version for migrations
52 52 __platform__ = platform.system()
53 53 __license__ = 'AGPLv3, and Commercial License'
54 54 __author__ = 'RhodeCode GmbH'
@@ -170,8 +170,7 b' def validate_repo_permissions(apiuser, r'
170 170 """
171 171 if not HasRepoPermissionAnyApi(*perms)(
172 172 user=apiuser, repo_name=repo.repo_name):
173 raise JSONRPCError(
174 'repository `%s` does not exist' % repoid)
173 raise JSONRPCError('repository `%s` does not exist' % repoid)
175 174
176 175 return True
177 176
@@ -307,8 +307,7 b' def get_repo_changeset(request, apiuser,'
307 307 """
308 308 repo = get_repo_or_error(repoid)
309 309 if not has_superadmin_permission(apiuser):
310 _perms = (
311 'repository.admin', 'repository.write', 'repository.read',)
310 _perms = ('repository.admin', 'repository.write', 'repository.read',)
312 311 validate_repo_permissions(apiuser, repoid, repo, _perms)
313 312
314 313 changes_details = Optional.extract(details)
@@ -366,8 +365,7 b' def get_repo_changesets(request, apiuser'
366 365 """
367 366 repo = get_repo_or_error(repoid)
368 367 if not has_superadmin_permission(apiuser):
369 _perms = (
370 'repository.admin', 'repository.write', 'repository.read',)
368 _perms = ('repository.admin', 'repository.write', 'repository.read',)
371 369 validate_repo_permissions(apiuser, repoid, repo, _perms)
372 370
373 371 changes_details = Optional.extract(details)
@@ -1021,7 +1019,8 b' def update_repo('
1021 1019
1022 1020 include_secrets = False
1023 1021 if not has_superadmin_permission(apiuser):
1024 validate_repo_permissions(apiuser, repoid, repo, ('repository.admin',))
1022 _perms = ('repository.admin',)
1023 validate_repo_permissions(apiuser, repoid, repo, _perms)
1025 1024 else:
1026 1025 include_secrets = True
1027 1026
@@ -1208,8 +1207,7 b' def fork_repo(request, apiuser, repoid, '
1208 1207 if not has_superadmin_permission(apiuser):
1209 1208 # check if we have at least read permission for
1210 1209 # this repo that we fork !
1211 _perms = (
1212 'repository.admin', 'repository.write', 'repository.read')
1210 _perms = ('repository.admin', 'repository.write', 'repository.read')
1213 1211 validate_repo_permissions(apiuser, repoid, repo, _perms)
1214 1212
1215 1213 # check if the regular user has at least fork permissions as well
@@ -2370,12 +2368,13 b' def get_repo_settings(request, apiuser, '
2370 2368 }
2371 2369 """
2372 2370
2373 # Restrict access to this api method to admins only.
2371 # Restrict access to this api method to super-admins, and repo admins only.
2372 repo = get_repo_or_error(repoid)
2374 2373 if not has_superadmin_permission(apiuser):
2375 raise JSONRPCForbidden()
2374 _perms = ('repository.admin',)
2375 validate_repo_permissions(apiuser, repoid, repo, _perms)
2376 2376
2377 2377 try:
2378 repo = get_repo_or_error(repoid)
2379 2378 settings_model = VcsSettingsModel(repo=repo)
2380 2379 settings = settings_model.get_global_settings()
2381 2380 settings.update(settings_model.get_repo_settings())
@@ -2414,9 +2413,11 b' def set_repo_settings(request, apiuser, '
2414 2413 "result": true
2415 2414 }
2416 2415 """
2417 # Restrict access to this api method to admins only.
2416 # Restrict access to this api method to super-admins, and repo admins only.
2417 repo = get_repo_or_error(repoid)
2418 2418 if not has_superadmin_permission(apiuser):
2419 raise JSONRPCForbidden()
2419 _perms = ('repository.admin',)
2420 validate_repo_permissions(apiuser, repoid, repo, _perms)
2420 2421
2421 2422 if type(settings) is not dict:
2422 2423 raise JSONRPCError('Settings have to be a JSON Object.')
@@ -34,7 +34,7 b' from rhodecode.lib.channelstream import '
34 34 get_user_data,
35 35 parse_channels_info,
36 36 update_history_from_logs,
37 STATE_PUBLIC_KEYS)
37 USER_STATE_PUBLIC_KEYS)
38 38
39 39 from rhodecode.lib.auth import NotAnonymous
40 40
@@ -86,14 +86,16 b' class ChannelstreamView(BaseAppView):'
86 86 'display_name': None,
87 87 'display_link': None,
88 88 }
89 user_data['permissions'] = self._rhodecode_user.permissions_safe
89
90 #user_data['permissions'] = self._rhodecode_user.permissions_safe
91
90 92 payload = {
91 93 'username': user.username,
92 94 'user_state': user_data,
93 95 'conn_id': str(uuid.uuid4()),
94 96 'channels': channels,
95 97 'channel_configs': {},
96 'state_public_keys': STATE_PUBLIC_KEYS,
98 'state_public_keys': USER_STATE_PUBLIC_KEYS,
97 99 'info': {
98 100 'exclude_channels': ['broadcast']
99 101 }
@@ -118,10 +120,13 b' class ChannelstreamView(BaseAppView):'
118 120 'Channelstream service at {} is down'.format(channelstream_url))
119 121 return HTTPBadGateway()
120 122
123 channel_info = connect_result.get('channels_info')
124 if not channel_info:
125 raise HTTPBadRequest()
126
121 127 connect_result['channels'] = channels
122 128 connect_result['channels_info'] = parse_channels_info(
123 connect_result['channels_info'],
124 include_channel_info=filtered_channels)
129 channel_info, include_channel_info=filtered_channels)
125 130 update_history_from_logs(self.channelstream_config,
126 131 filtered_channels, connect_result)
127 132 return connect_result
@@ -167,10 +172,15 b' class ChannelstreamView(BaseAppView):'
167 172 log.exception(
168 173 'Channelstream service at {} is down'.format(channelstream_url))
169 174 return HTTPBadGateway()
175
176 channel_info = connect_result.get('channels_info')
177 if not channel_info:
178 raise HTTPBadRequest()
179
170 180 # include_channel_info will limit history only to new channel
171 181 # to not overwrite histories on other channels in client
172 182 connect_result['channels_info'] = parse_channels_info(
173 connect_result['channels_info'],
183 channel_info,
174 184 include_channel_info=filtered_channels)
175 185 update_history_from_logs(
176 186 self.channelstream_config, filtered_channels, connect_result)
@@ -43,10 +43,10 b' def includeme(config):'
43 43 pattern='/_file_store/upload')
44 44 config.add_route(
45 45 name='download_file',
46 pattern='/_file_store/download/{fid}')
46 pattern='/_file_store/download/{fid:.*}')
47 47 config.add_route(
48 48 name='download_file_by_token',
49 pattern='/_file_store/token-download/{_auth_token}/{fid}')
49 pattern='/_file_store/token-download/{_auth_token}/{fid:.*}')
50 50
51 51 # Scan module for configuration decorators.
52 52 config.scan('.views', ignore='.tests')
@@ -20,6 +20,7 b''
20 20
21 21 import os
22 22 import time
23 import errno
23 24 import shutil
24 25 import hashlib
25 26
@@ -32,9 +33,24 b' from rhodecode.apps.file_store.exception'
32 33 METADATA_VER = 'v1'
33 34
34 35
36 def safe_make_dirs(dir_path):
37 if not os.path.exists(dir_path):
38 try:
39 os.makedirs(dir_path)
40 except OSError as e:
41 if e.errno != errno.EEXIST:
42 raise
43 return
44
45
35 46 class LocalFileStorage(object):
36 47
37 48 @classmethod
49 def apply_counter(cls, counter, filename):
50 name_counted = '%d-%s' % (counter, filename)
51 return name_counted
52
53 @classmethod
38 54 def resolve_name(cls, name, directory):
39 55 """
40 56 Resolves a unique name and the correct path. If a filename
@@ -47,17 +63,16 b' class LocalFileStorage(object):'
47 63
48 64 counter = 0
49 65 while True:
50 name = '%d-%s' % (counter, name)
66 name_counted = cls.apply_counter(counter, name)
51 67
52 68 # sub_store prefix to optimize disk usage, e.g some_path/ab/final_file
53 sub_store = cls._sub_store_from_filename(name)
69 sub_store = cls._sub_store_from_filename(name_counted)
54 70 sub_store_path = os.path.join(directory, sub_store)
55 if not os.path.exists(sub_store_path):
56 os.makedirs(sub_store_path)
71 safe_make_dirs(sub_store_path)
57 72
58 path = os.path.join(sub_store_path, name)
73 path = os.path.join(sub_store_path, name_counted)
59 74 if not os.path.exists(path):
60 return name, path
75 return name_counted, path
61 76 counter += 1
62 77
63 78 @classmethod
@@ -102,8 +117,13 b' class LocalFileStorage(object):'
102 117
103 118 :param filename: base name of file
104 119 """
120 prefix_dir = ''
121 if '/' in filename:
122 prefix_dir, filename = filename.split('/')
105 123 sub_store = self._sub_store_from_filename(filename)
106 return os.path.join(self.base_path, sub_store, filename)
124 else:
125 sub_store = self._sub_store_from_filename(filename)
126 return os.path.join(self.base_path, prefix_dir, sub_store, filename)
107 127
108 128 def delete(self, filename):
109 129 """
@@ -123,7 +143,7 b' class LocalFileStorage(object):'
123 143 Checks if file exists. Resolves filename's absolute
124 144 path based on base_path.
125 145
126 :param filename: base name of file
146 :param filename: file_uid name of file, e.g 0-f62b2b2d-9708-4079-a071-ec3f958448d4.svg
127 147 """
128 148 return os.path.exists(self.store_path(filename))
129 149
@@ -158,7 +178,7 b' class LocalFileStorage(object):'
158 178 return ext in [normalize_ext(x) for x in extensions]
159 179
160 180 def save_file(self, file_obj, filename, directory=None, extensions=None,
161 extra_metadata=None, max_filesize=None, **kwargs):
181 extra_metadata=None, max_filesize=None, randomized_name=True, **kwargs):
162 182 """
163 183 Saves a file object to the uploads location.
164 184 Returns the resolved filename, i.e. the directory +
@@ -169,6 +189,7 b' class LocalFileStorage(object):'
169 189 :param directory: relative path of sub-directory
170 190 :param extensions: iterable of allowed extensions, if not default
171 191 :param max_filesize: maximum size of file that should be allowed
192 :param randomized_name: generate random generated UID or fixed based on the filename
172 193 :param extra_metadata: extra JSON metadata to store next to the file with .meta suffix
173 194
174 195 """
@@ -183,13 +204,12 b' class LocalFileStorage(object):'
183 204 else:
184 205 dest_directory = self.base_path
185 206
186 if not os.path.exists(dest_directory):
187 os.makedirs(dest_directory)
207 safe_make_dirs(dest_directory)
188 208
189 filename = utils.uid_filename(filename)
209 uid_filename = utils.uid_filename(filename, randomized=randomized_name)
190 210
191 211 # resolve also produces special sub-dir for file optimized store
192 filename, path = self.resolve_name(filename, dest_directory)
212 filename, path = self.resolve_name(uid_filename, dest_directory)
193 213 stored_file_dir = os.path.dirname(path)
194 214
195 215 file_obj.seek(0)
@@ -210,12 +230,13 b' class LocalFileStorage(object):'
210 230
211 231 file_hash = self.calculate_path_hash(path)
212 232
213 metadata.update(
214 {"filename": filename,
233 metadata.update({
234 "filename": filename,
215 235 "size": size,
216 236 "time": time.time(),
217 237 "sha256": file_hash,
218 "meta_ver": METADATA_VER})
238 "meta_ver": METADATA_VER
239 })
219 240
220 241 filename_meta = filename + '.meta'
221 242 with open(os.path.join(stored_file_dir, filename_meta), "wb") as dest_meta:
@@ -20,7 +20,7 b''
20 20
21 21
22 22 import uuid
23
23 import StringIO
24 24 import pathlib2
25 25
26 26
@@ -52,3 +52,7 b' def uid_filename(filename, randomized=Tr'
52 52 hash_key = '{}.{}'.format(filename, 'store')
53 53 uid = uuid.uuid5(uuid.NAMESPACE_URL, hash_key)
54 54 return str(uid) + ext.lower()
55
56
57 def bytes_to_file_obj(bytes_data):
58 return StringIO.StringIO(bytes_data)
@@ -64,7 +64,7 b' class FileStoreView(BaseAppView):'
64 64 file_uid, store_path)
65 65 raise HTTPNotFound()
66 66
67 db_obj = FileStore().query().filter(FileStore.file_uid == file_uid).scalar()
67 db_obj = FileStore.get_by_store_uid(file_uid, safe=True)
68 68 if not db_obj:
69 69 raise HTTPNotFound()
70 70
@@ -345,6 +345,16 b' def includeme(config):'
345 345 pattern='/{repo_name:.*?[^/]}/pull-request/{pull_request_id:\d+}/comment/{comment_id}/delete',
346 346 repo_route=True, repo_accepted_types=['hg', 'git'])
347 347
348 config.add_route(
349 name='pullrequest_comments',
350 pattern='/{repo_name:.*?[^/]}/pull-request/{pull_request_id:\d+}/comments',
351 repo_route=True)
352
353 config.add_route(
354 name='pullrequest_todos',
355 pattern='/{repo_name:.*?[^/]}/pull-request/{pull_request_id:\d+}/todos',
356 repo_route=True)
357
348 358 # Artifacts, (EE feature)
349 359 config.add_route(
350 360 name='repo_artifacts_list',
@@ -485,23 +485,10 b' class TestRepoCommitCommentsView(TestCon'
485 485
486 486
487 487 def assert_comment_links(response, comments, inline_comments):
488 if comments == 1:
489 comments_text = "%d General" % comments
490 else:
491 comments_text = "%d General" % comments
492
493 if inline_comments == 1:
494 inline_comments_text = "%d Inline" % inline_comments
495 else:
496 inline_comments_text = "%d Inline" % inline_comments
488 response.mustcontain(
489 '<span class="display-none" id="general-comments-count">{}</span>'.format(comments))
490 response.mustcontain(
491 '<span class="display-none" id="inline-comments-count">{}</span>'.format(inline_comments))
497 492
498 if comments:
499 response.mustcontain('<a href="#comments">%s</a>,' % comments_text)
500 else:
501 response.mustcontain(comments_text)
502 493
503 if inline_comments:
504 response.mustcontain(
505 'id="inline-comments-counter">%s' % inline_comments_text)
506 else:
507 response.mustcontain(inline_comments_text)
494
@@ -619,7 +619,12 b' class ComparePage(AssertResponse):'
619 619 self.contains_one_anchor(file_id)
620 620 diffblock = doc.cssselect('[data-f-path="%s"]' % filename)
621 621 assert len(diffblock) == 2
622 assert len(diffblock[0].cssselect('a[href="#%s"]' % file_id)) == 1
622 for lnk in diffblock[0].cssselect('a'):
623 if 'permalink' in lnk.text:
624 assert '#{}'.format(file_id) in lnk.attrib['href']
625 break
626 else:
627 pytest.fail('Unable to find permalink')
623 628
624 629 def contains_change_summary(self, files_changed, inserted, deleted):
625 630 template = (
@@ -150,9 +150,9 b' class TestPullrequestsView(object):'
150 150 response = self.app.post(
151 151 route_path('pullrequest_create', repo_name=source.repo_name),
152 152 [
153 ('source_repo', source.repo_name),
153 ('source_repo', source_repo_name),
154 154 ('source_ref', source_ref),
155 ('target_repo', target.repo_name),
155 ('target_repo', target_repo_name),
156 156 ('target_ref', target_ref),
157 157 ('common_ancestor', commit_ids['initial-commit']),
158 158 ('pullrequest_title', 'Title'),
@@ -1110,16 +1110,17 b' class TestPullrequestsView(object):'
1110 1110
1111 1111 # source has ancestor - change - change-2
1112 1112 backend.pull_heads(source, heads=['change-2'])
1113 target_repo_name = target.repo_name
1113 1114
1114 1115 # update PR
1115 1116 self.app.post(
1116 1117 route_path('pullrequest_update',
1117 repo_name=target.repo_name, pull_request_id=pull_request_id),
1118 repo_name=target_repo_name, pull_request_id=pull_request_id),
1118 1119 params={'update_commits': 'true', 'csrf_token': csrf_token})
1119 1120
1120 1121 response = self.app.get(
1121 1122 route_path('pullrequest_show',
1122 repo_name=target.repo_name,
1123 repo_name=target_repo_name,
1123 1124 pull_request_id=pull_request.pull_request_id))
1124 1125
1125 1126 assert response.status_int == 200
@@ -1166,10 +1167,11 b' class TestPullrequestsView(object):'
1166 1167 # source has ancestor - ancestor-new - change-rebased
1167 1168 backend.pull_heads(target, heads=['ancestor-new'])
1168 1169 backend.pull_heads(source, heads=['change-rebased'])
1170 target_repo_name = target.repo_name
1169 1171
1170 1172 # update PR
1171 1173 url = route_path('pullrequest_update',
1172 repo_name=target.repo_name,
1174 repo_name=target_repo_name,
1173 1175 pull_request_id=pull_request_id)
1174 1176 self.app.post(url,
1175 1177 params={'update_commits': 'true', 'csrf_token': csrf_token},
@@ -1183,7 +1185,7 b' class TestPullrequestsView(object):'
1183 1185
1184 1186 response = self.app.get(
1185 1187 route_path('pullrequest_show',
1186 repo_name=target.repo_name,
1188 repo_name=target_repo_name,
1187 1189 pull_request_id=pull_request.pull_request_id))
1188 1190 assert response.status_int == 200
1189 1191 response.mustcontain('Pull request updated to')
@@ -1232,16 +1234,17 b' class TestPullrequestsView(object):'
1232 1234 vcsrepo = target.scm_instance()
1233 1235 vcsrepo.config.clear_section('hooks')
1234 1236 vcsrepo.run_git_command(['reset', '--soft', 'HEAD~2'])
1237 target_repo_name = target.repo_name
1235 1238
1236 1239 # update PR
1237 1240 url = route_path('pullrequest_update',
1238 repo_name=target.repo_name,
1241 repo_name=target_repo_name,
1239 1242 pull_request_id=pull_request_id)
1240 1243 self.app.post(url,
1241 1244 params={'update_commits': 'true', 'csrf_token': csrf_token},
1242 1245 status=200)
1243 1246
1244 response = self.app.get(route_path('pullrequest_new', repo_name=target.repo_name))
1247 response = self.app.get(route_path('pullrequest_new', repo_name=target_repo_name))
1245 1248 assert response.status_int == 200
1246 1249 response.mustcontain('Pull request updated to')
1247 1250 response.mustcontain('with 0 added, 0 removed commits.')
@@ -1280,11 +1283,12 b' class TestPullrequestsView(object):'
1280 1283 # source has ancestor - ancestor-new - change-rebased
1281 1284 backend.pull_heads(target, heads=['ancestor-new'])
1282 1285 backend.pull_heads(source, heads=['change-rebased'])
1286 target_repo_name = target.repo_name
1283 1287
1284 1288 # update PR
1285 1289 self.app.post(
1286 1290 route_path('pullrequest_update',
1287 repo_name=target.repo_name, pull_request_id=pull_request_id),
1291 repo_name=target_repo_name, pull_request_id=pull_request_id),
1288 1292 params={'update_commits': 'true', 'csrf_token': csrf_token},
1289 1293 status=200)
1290 1294
@@ -1389,6 +1393,8 b' class TestPullrequestsView(object):'
1389 1393 pull_request = pr_util.create_pull_request(
1390 1394 commits, target_head='old-feature', source_head='new-feature',
1391 1395 revisions=['new-feature'], mergeable=True)
1396 pr_id = pull_request.pull_request_id
1397 target_repo_name = pull_request.target_repo.repo_name
1392 1398
1393 1399 vcs = pr_util.source_repository.scm_instance()
1394 1400 if backend.alias == 'git':
@@ -1397,8 +1403,8 b' class TestPullrequestsView(object):'
1397 1403 vcs.strip(pr_util.commit_ids['new-feature'])
1398 1404
1399 1405 url = route_path('pullrequest_update',
1400 repo_name=pull_request.target_repo.repo_name,
1401 pull_request_id=pull_request.pull_request_id)
1406 repo_name=target_repo_name,
1407 pull_request_id=pr_id)
1402 1408 response = self.app.post(url,
1403 1409 params={'update_commits': 'true',
1404 1410 'csrf_token': csrf_token})
@@ -1409,8 +1415,8 b' class TestPullrequestsView(object):'
1409 1415 # Make sure that after update, it won't raise 500 errors
1410 1416 response = self.app.get(route_path(
1411 1417 'pullrequest_show',
1412 repo_name=pr_util.target_repository.repo_name,
1413 pull_request_id=pull_request.pull_request_id))
1418 repo_name=target_repo_name,
1419 pull_request_id=pr_id))
1414 1420
1415 1421 assert response.status_int == 200
1416 1422 response.assert_response().element_contains(
@@ -18,8 +18,8 b''
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
22 21 import logging
22 import collections
23 23
24 24 from pyramid.httpexceptions import (
25 25 HTTPNotFound, HTTPBadRequest, HTTPFound, HTTPForbidden, HTTPConflict)
@@ -34,14 +34,14 b' from rhodecode.apps.file_store.exception'
34 34 from rhodecode.lib import diffs, codeblocks
35 35 from rhodecode.lib.auth import (
36 36 LoginRequired, HasRepoPermissionAnyDecorator, NotAnonymous, CSRFRequired)
37
37 from rhodecode.lib.ext_json import json
38 38 from rhodecode.lib.compat import OrderedDict
39 39 from rhodecode.lib.diffs import (
40 40 cache_diff, load_cached_diff, diff_cache_exist, get_diff_context,
41 41 get_diff_whitespace_flag)
42 42 from rhodecode.lib.exceptions import StatusChangeOnClosedPullRequestError, CommentVersionMismatch
43 43 import rhodecode.lib.helpers as h
44 from rhodecode.lib.utils2 import safe_unicode, str2bool
44 from rhodecode.lib.utils2 import safe_unicode, str2bool, StrictAttributeDict
45 45 from rhodecode.lib.vcs.backends.base import EmptyCommit
46 46 from rhodecode.lib.vcs.exceptions import (
47 47 RepositoryError, CommitDoesNotExistError)
@@ -115,6 +115,7 b' class RepoCommitsView(RepoAppView):'
115 115 except Exception:
116 116 log.exception("General failure")
117 117 raise HTTPNotFound()
118 single_commit = len(c.commit_ranges) == 1
118 119
119 120 c.changes = OrderedDict()
120 121 c.lines_added = 0
@@ -128,23 +129,48 b' class RepoCommitsView(RepoAppView):'
128 129 c.inline_comments = []
129 130 c.files = []
130 131
131 c.statuses = []
132 132 c.comments = []
133 133 c.unresolved_comments = []
134 134 c.resolved_comments = []
135 if len(c.commit_ranges) == 1:
135
136 # Single commit
137 if single_commit:
136 138 commit = c.commit_ranges[0]
137 139 c.comments = CommentsModel().get_comments(
138 140 self.db_repo.repo_id,
139 141 revision=commit.raw_id)
140 c.statuses.append(ChangesetStatusModel().get_status(
141 self.db_repo.repo_id, commit.raw_id))
142
142 143 # comments from PR
143 144 statuses = ChangesetStatusModel().get_statuses(
144 145 self.db_repo.repo_id, commit.raw_id,
145 146 with_revisions=True)
146 prs = set(st.pull_request for st in statuses
147 if st.pull_request is not None)
147
148 prs = set()
149 reviewers = list()
150 reviewers_duplicates = set() # to not have duplicates from multiple votes
151 for c_status in statuses:
152
153 # extract associated pull-requests from votes
154 if c_status.pull_request:
155 prs.add(c_status.pull_request)
156
157 # extract reviewers
158 _user_id = c_status.author.user_id
159 if _user_id not in reviewers_duplicates:
160 reviewers.append(
161 StrictAttributeDict({
162 'user': c_status.author,
163
164 # fake attributed for commit, page that we don't have
165 # but we share the display with PR page
166 'mandatory': False,
167 'reasons': [],
168 'rule_user_group_data': lambda: None
169 })
170 )
171 reviewers_duplicates.add(_user_id)
172
173 c.allowed_reviewers = reviewers
148 174 # from associated statuses, check the pull requests, and
149 175 # show comments from them
150 176 for pr in prs:
@@ -155,6 +181,37 b' class RepoCommitsView(RepoAppView):'
155 181 c.resolved_comments = CommentsModel()\
156 182 .get_commit_resolved_todos(commit.raw_id)
157 183
184 c.inline_comments_flat = CommentsModel()\
185 .get_commit_inline_comments(commit.raw_id)
186
187 review_statuses = ChangesetStatusModel().aggregate_votes_by_user(
188 statuses, reviewers)
189
190 c.commit_review_status = ChangesetStatus.STATUS_NOT_REVIEWED
191
192 c.commit_set_reviewers_data_json = collections.OrderedDict({'reviewers': []})
193
194 for review_obj, member, reasons, mandatory, status in review_statuses:
195 member_reviewer = h.reviewer_as_json(
196 member, reasons=reasons, mandatory=mandatory,
197 user_group=None
198 )
199
200 current_review_status = status[0][1].status if status else ChangesetStatus.STATUS_NOT_REVIEWED
201 member_reviewer['review_status'] = current_review_status
202 member_reviewer['review_status_label'] = h.commit_status_lbl(current_review_status)
203 member_reviewer['allowed_to_update'] = False
204 c.commit_set_reviewers_data_json['reviewers'].append(member_reviewer)
205
206 c.commit_set_reviewers_data_json = json.dumps(c.commit_set_reviewers_data_json)
207
208 # NOTE(marcink): this uses the same voting logic as in pull-requests
209 c.commit_review_status = ChangesetStatusModel().calculate_status(review_statuses)
210 c.commit_broadcast_channel = u'/repo${}$/commit/{}'.format(
211 c.repo_name,
212 commit.raw_id
213 )
214
158 215 diff = None
159 216 # Iterate over ranges (default commit view is always one commit)
160 217 for commit in c.commit_ranges:
@@ -166,8 +223,8 b' class RepoCommitsView(RepoAppView):'
166 223 if method == 'show':
167 224 inline_comments = CommentsModel().get_inline_comments(
168 225 self.db_repo.repo_id, revision=commit.raw_id)
169 c.inline_cnt = CommentsModel().get_inline_comments_count(
170 inline_comments)
226 c.inline_cnt = len(CommentsModel().get_inline_comments_as_list(
227 inline_comments))
171 228 c.inline_comments = inline_comments
172 229
173 230 cache_path = self.rhodecode_vcs_repo.get_create_shadow_cache_pr_path(
@@ -226,6 +283,7 b' class RepoCommitsView(RepoAppView):'
226 283
227 284 # sort comments by how they were generated
228 285 c.comments = sorted(c.comments, key=lambda x: x.comment_id)
286 c.at_version_num = None
229 287
230 288 if len(c.commit_ranges) == 1:
231 289 c.commit = c.commit_ranges[0]
@@ -395,6 +453,7 b' class RepoCommitsView(RepoAppView):'
395 453 }
396 454 if comment:
397 455 c.co = comment
456 c.at_version_num = 0
398 457 rendered_comment = render(
399 458 'rhodecode:templates/changeset/changeset_comment_block.mako',
400 459 self._get_template_context(c), self.request)
@@ -427,7 +486,6 b' class RepoCommitsView(RepoAppView):'
427 486 return ''
428 487
429 488 @LoginRequired()
430 @NotAnonymous()
431 489 @HasRepoPermissionAnyDecorator(
432 490 'repository.read', 'repository.write', 'repository.admin')
433 491 @CSRFRequired()
@@ -39,7 +39,7 b' from rhodecode.lib.ext_json import json'
39 39 from rhodecode.lib.auth import (
40 40 LoginRequired, HasRepoPermissionAny, HasRepoPermissionAnyDecorator,
41 41 NotAnonymous, CSRFRequired)
42 from rhodecode.lib.utils2 import str2bool, safe_str, safe_unicode
42 from rhodecode.lib.utils2 import str2bool, safe_str, safe_unicode, safe_int
43 43 from rhodecode.lib.vcs.backends.base import EmptyCommit, UpdateFailureReason
44 44 from rhodecode.lib.vcs.exceptions import (
45 45 CommitDoesNotExistError, RepositoryRequirementError, EmptyRepositoryError)
@@ -265,6 +265,36 b' class RepoPullRequestsView(RepoAppView, '
265 265
266 266 return diffset
267 267
268 def register_comments_vars(self, c, pull_request, versions):
269 comments_model = CommentsModel()
270
271 # GENERAL COMMENTS with versions #
272 q = comments_model._all_general_comments_of_pull_request(pull_request)
273 q = q.order_by(ChangesetComment.comment_id.asc())
274 general_comments = q
275
276 # pick comments we want to render at current version
277 c.comment_versions = comments_model.aggregate_comments(
278 general_comments, versions, c.at_version_num)
279
280 # INLINE COMMENTS with versions #
281 q = comments_model._all_inline_comments_of_pull_request(pull_request)
282 q = q.order_by(ChangesetComment.comment_id.asc())
283 inline_comments = q
284
285 c.inline_versions = comments_model.aggregate_comments(
286 inline_comments, versions, c.at_version_num, inline=True)
287
288 # Comments inline+general
289 if c.at_version:
290 c.inline_comments_flat = c.inline_versions[c.at_version_num]['display']
291 c.comments = c.comment_versions[c.at_version_num]['display']
292 else:
293 c.inline_comments_flat = c.inline_versions[c.at_version_num]['until']
294 c.comments = c.comment_versions[c.at_version_num]['until']
295
296 return general_comments, inline_comments
297
268 298 @LoginRequired()
269 299 @HasRepoPermissionAnyDecorator(
270 300 'repository.read', 'repository.write', 'repository.admin')
@@ -280,6 +310,8 b' class RepoPullRequestsView(RepoAppView, '
280 310 pull_request_id = pull_request.pull_request_id
281 311
282 312 c.state_progressing = pull_request.is_state_changing()
313 c.pr_broadcast_channel = '/repo${}$/pr/{}'.format(
314 pull_request.target_repo.repo_name, pull_request.pull_request_id)
283 315
284 316 _new_state = {
285 317 'created': PullRequest.STATE_CREATED,
@@ -300,22 +332,23 b' class RepoPullRequestsView(RepoAppView, '
300 332 from_version = self.request.GET.get('from_version') or version
301 333 merge_checks = self.request.GET.get('merge_checks')
302 334 c.fulldiff = str2bool(self.request.GET.get('fulldiff'))
335 force_refresh = str2bool(self.request.GET.get('force_refresh'))
336 c.range_diff_on = self.request.GET.get('range-diff') == "1"
303 337
304 338 # fetch global flags of ignore ws or context lines
305 339 diff_context = diffs.get_diff_context(self.request)
306 340 hide_whitespace_changes = diffs.get_diff_whitespace_flag(self.request)
307 341
308 force_refresh = str2bool(self.request.GET.get('force_refresh'))
309
310 342 (pull_request_latest,
311 343 pull_request_at_ver,
312 344 pull_request_display_obj,
313 345 at_version) = PullRequestModel().get_pr_version(
314 346 pull_request_id, version=version)
347
315 348 pr_closed = pull_request_latest.is_closed()
316 349
317 350 if pr_closed and (version or from_version):
318 # not allow to browse versions
351 # not allow to browse versions for closed PR
319 352 raise HTTPFound(h.route_path(
320 353 'pullrequest_show', repo_name=self.db_repo_name,
321 354 pull_request_id=pull_request_id))
@@ -323,13 +356,13 b' class RepoPullRequestsView(RepoAppView, '
323 356 versions = pull_request_display_obj.versions()
324 357 # used to store per-commit range diffs
325 358 c.changes = collections.OrderedDict()
326 c.range_diff_on = self.request.GET.get('range-diff') == "1"
327 359
328 360 c.at_version = at_version
329 361 c.at_version_num = (at_version
330 if at_version and at_version != 'latest'
362 if at_version and at_version != PullRequest.LATEST_VER
331 363 else None)
332 c.at_version_pos = ChangesetComment.get_index_from_version(
364
365 c.at_version_index = ChangesetComment.get_index_from_version(
333 366 c.at_version_num, versions)
334 367
335 368 (prev_pull_request_latest,
@@ -340,9 +373,9 b' class RepoPullRequestsView(RepoAppView, '
340 373
341 374 c.from_version = prev_at_version
342 375 c.from_version_num = (prev_at_version
343 if prev_at_version and prev_at_version != 'latest'
376 if prev_at_version and prev_at_version != PullRequest.LATEST_VER
344 377 else None)
345 c.from_version_pos = ChangesetComment.get_index_from_version(
378 c.from_version_index = ChangesetComment.get_index_from_version(
346 379 c.from_version_num, versions)
347 380
348 381 # define if we're in COMPARE mode or VIEW at version mode
@@ -351,16 +384,21 b' class RepoPullRequestsView(RepoAppView, '
351 384 # pull_requests repo_name we opened it against
352 385 # ie. target_repo must match
353 386 if self.db_repo_name != pull_request_at_ver.target_repo.repo_name:
387 log.warning('Mismatch between the current repo: %s, and target %s',
388 self.db_repo_name, pull_request_at_ver.target_repo.repo_name)
354 389 raise HTTPNotFound()
355 390
356 c.shadow_clone_url = PullRequestModel().get_shadow_clone_url(
357 pull_request_at_ver)
391 c.shadow_clone_url = PullRequestModel().get_shadow_clone_url(pull_request_at_ver)
358 392
359 393 c.pull_request = pull_request_display_obj
360 394 c.renderer = pull_request_at_ver.description_renderer or c.renderer
361 395 c.pull_request_latest = pull_request_latest
362 396
363 if compare or (at_version and not at_version == 'latest'):
397 # inject latest version
398 latest_ver = PullRequest.get_pr_display_object(pull_request_latest, pull_request_latest)
399 c.versions = versions + [latest_ver]
400
401 if compare or (at_version and not at_version == PullRequest.LATEST_VER):
364 402 c.allowed_to_change_status = False
365 403 c.allowed_to_update = False
366 404 c.allowed_to_merge = False
@@ -389,12 +427,9 b' class RepoPullRequestsView(RepoAppView, '
389 427 'rules' in pull_request_latest.reviewer_data:
390 428 rules = pull_request_latest.reviewer_data['rules'] or {}
391 429 try:
392 c.forbid_adding_reviewers = rules.get(
393 'forbid_adding_reviewers')
394 c.forbid_author_to_review = rules.get(
395 'forbid_author_to_review')
396 c.forbid_commit_author_to_review = rules.get(
397 'forbid_commit_author_to_review')
430 c.forbid_adding_reviewers = rules.get('forbid_adding_reviewers')
431 c.forbid_author_to_review = rules.get('forbid_author_to_review')
432 c.forbid_commit_author_to_review = rules.get('forbid_commit_author_to_review')
398 433 except Exception:
399 434 pass
400 435
@@ -419,41 +454,34 b' class RepoPullRequestsView(RepoAppView, '
419 454 'rhodecode:templates/pullrequests/pullrequest_merge_checks.mako'
420 455 return self._get_template_context(c)
421 456
422 comments_model = CommentsModel()
457 c.allowed_reviewers = [obj.user_id for obj in pull_request.reviewers if obj.user]
423 458
424 459 # reviewers and statuses
425 c.pull_request_reviewers = pull_request_at_ver.reviewers_statuses()
426 allowed_reviewers = [x[0].user_id for x in c.pull_request_reviewers]
460 c.pull_request_default_reviewers_data_json = json.dumps(pull_request.reviewer_data)
461 c.pull_request_set_reviewers_data_json = collections.OrderedDict({'reviewers': []})
427 462
428 # GENERAL COMMENTS with versions #
429 q = comments_model._all_general_comments_of_pull_request(pull_request_latest)
430 q = q.order_by(ChangesetComment.comment_id.asc())
431 general_comments = q
463 for review_obj, member, reasons, mandatory, status in pull_request_at_ver.reviewers_statuses():
464 member_reviewer = h.reviewer_as_json(
465 member, reasons=reasons, mandatory=mandatory,
466 user_group=review_obj.rule_user_group_data()
467 )
432 468
433 # pick comments we want to render at current version
434 c.comment_versions = comments_model.aggregate_comments(
435 general_comments, versions, c.at_version_num)
436 c.comments = c.comment_versions[c.at_version_num]['until']
469 current_review_status = status[0][1].status if status else ChangesetStatus.STATUS_NOT_REVIEWED
470 member_reviewer['review_status'] = current_review_status
471 member_reviewer['review_status_label'] = h.commit_status_lbl(current_review_status)
472 member_reviewer['allowed_to_update'] = c.allowed_to_update
473 c.pull_request_set_reviewers_data_json['reviewers'].append(member_reviewer)
437 474
438 # INLINE COMMENTS with versions #
439 q = comments_model._all_inline_comments_of_pull_request(pull_request_latest)
440 q = q.order_by(ChangesetComment.comment_id.asc())
441 inline_comments = q
475 c.pull_request_set_reviewers_data_json = json.dumps(c.pull_request_set_reviewers_data_json)
442 476
443 c.inline_versions = comments_model.aggregate_comments(
444 inline_comments, versions, c.at_version_num, inline=True)
477 general_comments, inline_comments = \
478 self.register_comments_vars(c, pull_request_latest, versions)
445 479
446 480 # TODOs
447 481 c.unresolved_comments = CommentsModel() \
448 .get_pull_request_unresolved_todos(pull_request)
482 .get_pull_request_unresolved_todos(pull_request_latest)
449 483 c.resolved_comments = CommentsModel() \
450 .get_pull_request_resolved_todos(pull_request)
451
452 # inject latest version
453 latest_ver = PullRequest.get_pr_display_object(
454 pull_request_latest, pull_request_latest)
455
456 c.versions = versions + [latest_ver]
484 .get_pull_request_resolved_todos(pull_request_latest)
457 485
458 486 # if we use version, then do not show later comments
459 487 # than current version
@@ -520,8 +548,8 b' class RepoPullRequestsView(RepoAppView, '
520 548
521 549 # empty version means latest, so we keep this to prevent
522 550 # double caching
523 version_normalized = version or 'latest'
524 from_version_normalized = from_version or 'latest'
551 version_normalized = version or PullRequest.LATEST_VER
552 from_version_normalized = from_version or PullRequest.LATEST_VER
525 553
526 554 cache_path = self.rhodecode_vcs_repo.get_create_shadow_cache_pr_path(target_repo)
527 555 cache_file_path = diff_cache_exist(
@@ -717,7 +745,7 b' class RepoPullRequestsView(RepoAppView, '
717 745
718 746 # current user review statuses for each version
719 747 c.review_versions = {}
720 if self._rhodecode_user.user_id in allowed_reviewers:
748 if self._rhodecode_user.user_id in c.allowed_reviewers:
721 749 for co in general_comments:
722 750 if co.author.user_id == self._rhodecode_user.user_id:
723 751 status = co.status_change
@@ -937,6 +965,90 b' class RepoPullRequestsView(RepoAppView, '
937 965 @NotAnonymous()
938 966 @HasRepoPermissionAnyDecorator(
939 967 'repository.read', 'repository.write', 'repository.admin')
968 @view_config(
969 route_name='pullrequest_comments', request_method='POST',
970 renderer='string', xhr=True)
971 def pullrequest_comments(self):
972 self.load_default_context()
973
974 pull_request = PullRequest.get_or_404(
975 self.request.matchdict['pull_request_id'])
976 pull_request_id = pull_request.pull_request_id
977 version = self.request.GET.get('version')
978
979 _render = self.request.get_partial_renderer(
980 'rhodecode:templates/base/sidebar.mako')
981 c = _render.get_call_context()
982
983 (pull_request_latest,
984 pull_request_at_ver,
985 pull_request_display_obj,
986 at_version) = PullRequestModel().get_pr_version(
987 pull_request_id, version=version)
988 versions = pull_request_display_obj.versions()
989 latest_ver = PullRequest.get_pr_display_object(pull_request_latest, pull_request_latest)
990 c.versions = versions + [latest_ver]
991
992 c.at_version = at_version
993 c.at_version_num = (at_version
994 if at_version and at_version != PullRequest.LATEST_VER
995 else None)
996
997 self.register_comments_vars(c, pull_request_latest, versions)
998 all_comments = c.inline_comments_flat + c.comments
999
1000 existing_ids = filter(
1001 lambda e: e, map(safe_int, self.request.POST.getall('comments[]')))
1002 return _render('comments_table', all_comments, len(all_comments),
1003 existing_ids=existing_ids)
1004
1005 @LoginRequired()
1006 @NotAnonymous()
1007 @HasRepoPermissionAnyDecorator(
1008 'repository.read', 'repository.write', 'repository.admin')
1009 @view_config(
1010 route_name='pullrequest_todos', request_method='POST',
1011 renderer='string', xhr=True)
1012 def pullrequest_todos(self):
1013 self.load_default_context()
1014
1015 pull_request = PullRequest.get_or_404(
1016 self.request.matchdict['pull_request_id'])
1017 pull_request_id = pull_request.pull_request_id
1018 version = self.request.GET.get('version')
1019
1020 _render = self.request.get_partial_renderer(
1021 'rhodecode:templates/base/sidebar.mako')
1022 c = _render.get_call_context()
1023 (pull_request_latest,
1024 pull_request_at_ver,
1025 pull_request_display_obj,
1026 at_version) = PullRequestModel().get_pr_version(
1027 pull_request_id, version=version)
1028 versions = pull_request_display_obj.versions()
1029 latest_ver = PullRequest.get_pr_display_object(pull_request_latest, pull_request_latest)
1030 c.versions = versions + [latest_ver]
1031
1032 c.at_version = at_version
1033 c.at_version_num = (at_version
1034 if at_version and at_version != PullRequest.LATEST_VER
1035 else None)
1036
1037 c.unresolved_comments = CommentsModel() \
1038 .get_pull_request_unresolved_todos(pull_request)
1039 c.resolved_comments = CommentsModel() \
1040 .get_pull_request_resolved_todos(pull_request)
1041
1042 all_comments = c.unresolved_comments + c.resolved_comments
1043 existing_ids = filter(
1044 lambda e: e, map(safe_int, self.request.POST.getall('comments[]')))
1045 return _render('comments_table', all_comments, len(c.unresolved_comments),
1046 todo_comments=True, existing_ids=existing_ids)
1047
1048 @LoginRequired()
1049 @NotAnonymous()
1050 @HasRepoPermissionAnyDecorator(
1051 'repository.read', 'repository.write', 'repository.admin')
940 1052 @CSRFRequired()
941 1053 @view_config(
942 1054 route_name='pullrequest_create', request_method='POST',
@@ -1098,7 +1210,7 b' class RepoPullRequestsView(RepoAppView, '
1098 1210 self.request.matchdict['pull_request_id'])
1099 1211 _ = self.request.translate
1100 1212
1101 self.load_default_context()
1213 c = self.load_default_context()
1102 1214 redirect_url = None
1103 1215
1104 1216 if pull_request.is_closed():
@@ -1109,6 +1221,8 b' class RepoPullRequestsView(RepoAppView, '
1109 1221 'redirect_url': redirect_url}
1110 1222
1111 1223 is_state_changing = pull_request.is_state_changing()
1224 c.pr_broadcast_channel = '/repo${}$/pr/{}'.format(
1225 pull_request.target_repo.repo_name, pull_request.pull_request_id)
1112 1226
1113 1227 # only owner or admin can update it
1114 1228 allowed_to_update = PullRequestModel().check_user_update(
@@ -1132,7 +1246,7 b' class RepoPullRequestsView(RepoAppView, '
1132 1246 return {'response': True,
1133 1247 'redirect_url': redirect_url}
1134 1248
1135 self._update_commits(pull_request)
1249 self._update_commits(c, pull_request)
1136 1250 if force_refresh:
1137 1251 redirect_url = h.route_path(
1138 1252 'pullrequest_show', repo_name=self.db_repo_name,
@@ -1168,7 +1282,7 b' class RepoPullRequestsView(RepoAppView, '
1168 1282 h.flash(msg, category='success')
1169 1283 return
1170 1284
1171 def _update_commits(self, pull_request):
1285 def _update_commits(self, c, pull_request):
1172 1286 _ = self.request.translate
1173 1287
1174 1288 with pull_request.set_state(PullRequest.STATE_UPDATING):
@@ -1196,13 +1310,18 b' class RepoPullRequestsView(RepoAppView, '
1196 1310 change_source=changed)
1197 1311 h.flash(msg, category='success')
1198 1312
1199 channel = '/repo${}$/pr/{}'.format(
1200 pull_request.target_repo.repo_name, pull_request.pull_request_id)
1201 1313 message = msg + (
1202 1314 ' - <a onclick="window.location.reload()">'
1203 1315 '<strong>{}</strong></a>'.format(_('Reload page')))
1316
1317 message_obj = {
1318 'message': message,
1319 'level': 'success',
1320 'topic': '/notifications'
1321 }
1322
1204 1323 channelstream.post_message(
1205 channel, message, self._rhodecode_user.username,
1324 c.pr_broadcast_channel, message_obj, self._rhodecode_user.username,
1206 1325 registry=self.request.registry)
1207 1326 else:
1208 1327 msg = PullRequestModel.UPDATE_STATUS_MESSAGES[resp.reason]
@@ -1472,6 +1591,7 b' class RepoPullRequestsView(RepoAppView, '
1472 1591 }
1473 1592 if comment:
1474 1593 c.co = comment
1594 c.at_version_num = None
1475 1595 rendered_comment = render(
1476 1596 'rhodecode:templates/changeset/changeset_comment_block.mako',
1477 1597 self._get_template_context(c), self.request)
@@ -1890,7 +1890,7 b''
1890 1890 "url": "http://spdx.org/licenses/BSD-4-Clause.html"
1891 1891 }
1892 1892 ],
1893 "name": "python2.7-channelstream-0.5.2"
1893 "name": "python2.7-channelstream-0.6.14"
1894 1894 },
1895 1895 {
1896 1896 "license": [
@@ -53,7 +53,7 b' from rhodecode.lib.utils2 import aslist '
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 write_metadata_if_needed, inject_app_settings)
56 write_metadata_if_needed, write_usage_data, inject_app_settings)
57 57
58 58
59 59 log = logging.getLogger(__name__)
@@ -316,6 +316,8 b' def includeme(config):'
316 316 pyramid.events.ApplicationCreated)
317 317 config.add_subscriber(write_metadata_if_needed,
318 318 pyramid.events.ApplicationCreated)
319 config.add_subscriber(write_usage_data,
320 pyramid.events.ApplicationCreated)
319 321 config.add_subscriber(write_js_routes_if_enabled,
320 322 pyramid.events.ApplicationCreated)
321 323
@@ -145,7 +145,7 b' class PullRequestCommentEvent(PullReques'
145 145
146 146 status = None
147 147 if self.comment.status_change:
148 status = self.comment.status_change[0].status
148 status = self.comment.review_status
149 149
150 150 data.update({
151 151 'comment': {
@@ -184,7 +184,7 b' class PullRequestCommentEditEvent(PullRe'
184 184
185 185 status = None
186 186 if self.comment.status_change:
187 status = self.comment.status_change[0].status
187 status = self.comment.review_status
188 188
189 189 data.update({
190 190 'comment': {
@@ -37,7 +37,8 b' log = logging.getLogger(__name__)'
37 37
38 38 LOCK = ReadWriteMutex()
39 39
40 STATE_PUBLIC_KEYS = ['id', 'username', 'first_name', 'last_name',
40 USER_STATE_PUBLIC_KEYS = [
41 'id', 'username', 'first_name', 'last_name',
41 42 'icon_link', 'display_name', 'display_link']
42 43
43 44
@@ -64,6 +65,8 b' def channelstream_request(config, payloa'
64 65 'x-channelstream-endpoint': endpoint,
65 66 'Content-Type': 'application/json'}
66 67 req_url = get_channelstream_server_url(config, endpoint)
68
69 log.debug('Sending a channelstream request to endpoint: `%s`', req_url)
67 70 response = None
68 71 try:
69 72 response = requests.post(req_url, data=json.dumps(payload),
@@ -76,6 +79,7 b' def channelstream_request(config, payloa'
76 79 log.exception('Exception related to Channelstream happened')
77 80 if raise_exc:
78 81 raise ChannelstreamConnectionException()
82 log.debug('Got channelstream response: %s', response)
79 83 return response
80 84
81 85
@@ -154,7 +158,7 b' def parse_channels_info(info_result, inc'
154 158 for userinfo in info_result['users']:
155 159 user_state_dict[userinfo['user']] = {
156 160 k: v for k, v in userinfo['state'].items()
157 if k in STATE_PUBLIC_KEYS
161 if k in USER_STATE_PUBLIC_KEYS
158 162 }
159 163
160 164 channels_info = {}
@@ -163,10 +167,10 b' def parse_channels_info(info_result, inc'
163 167 if c_name not in include_channel_info:
164 168 continue
165 169 connected_list = []
166 for userinfo in c_info['users']:
170 for username in c_info['users']:
167 171 connected_list.append({
168 'user': userinfo['user'],
169 'state': user_state_dict[userinfo['user']]
172 'user': username,
173 'state': user_state_dict[username]
170 174 })
171 175 channels_info[c_name] = {'users': connected_list,
172 176 'history': c_info['history']}
@@ -230,6 +234,14 b' def get_connection_validators(registry):'
230 234
231 235 def post_message(channel, message, username, registry=None):
232 236
237 message_obj = message
238 if isinstance(message, basestring):
239 message_obj = {
240 'message': message,
241 'level': 'success',
242 'topic': '/notifications'
243 }
244
233 245 if not registry:
234 246 registry = get_current_registry()
235 247
@@ -243,11 +255,7 b' def post_message(channel, message, usern'
243 255 'user': 'system',
244 256 'exclude_users': [username],
245 257 'channel': channel,
246 'message': {
247 'message': message,
248 'level': 'success',
249 'topic': '/notifications'
250 }
258 'message': message_obj
251 259 }
252 260
253 261 try:
@@ -90,7 +90,7 b' from rhodecode.lib.vcs.conf.settings imp'
90 90 from rhodecode.lib.index.search_utils import get_matching_line_offsets
91 91 from rhodecode.config.conf import DATE_FORMAT, DATETIME_FORMAT
92 92 from rhodecode.model.changeset_status import ChangesetStatusModel
93 from rhodecode.model.db import Permission, User, Repository, UserApiKeys
93 from rhodecode.model.db import Permission, User, Repository, UserApiKeys, FileStore
94 94 from rhodecode.model.repo_group import RepoGroupModel
95 95 from rhodecode.model.settings import IssueTrackerSettingsModel
96 96
@@ -810,8 +810,7 b' import tzlocal'
810 810 local_timezone = tzlocal.get_localzone()
811 811
812 812
813 def age_component(datetime_iso, value=None, time_is_local=False, tooltip=True):
814 title = value or format_date(datetime_iso)
813 def get_timezone(datetime_iso, time_is_local=False):
815 814 tzinfo = '+00:00'
816 815
817 816 # detect if we have a timezone info, otherwise, add it
@@ -822,6 +821,12 b' def age_component(datetime_iso, value=No'
822 821 timezone = force_timezone or local_timezone
823 822 offset = timezone.localize(datetime_iso).strftime('%z')
824 823 tzinfo = '{}:{}'.format(offset[:-2], offset[-2:])
824 return tzinfo
825
826
827 def age_component(datetime_iso, value=None, time_is_local=False, tooltip=True):
828 title = value or format_date(datetime_iso)
829 tzinfo = get_timezone(datetime_iso, time_is_local=time_is_local)
825 830
826 831 return literal(
827 832 '<time class="timeago {cls}" title="{tt_title}" datetime="{dt}{tzinfo}">{title}</time>'.format(
@@ -1357,20 +1362,76 b' class InitialsGravatar(object):'
1357 1362 return "data:image/svg+xml;base64,%s" % base64.b64encode(img_data)
1358 1363
1359 1364
1360 def initials_gravatar(email_address, first_name, last_name, size=30):
1365 def initials_gravatar(request, email_address, first_name, last_name, size=30, store_on_disk=False):
1366
1361 1367 svg_type = None
1362 1368 if email_address == User.DEFAULT_USER_EMAIL:
1363 1369 svg_type = 'default_user'
1370
1364 1371 klass = InitialsGravatar(email_address, first_name, last_name, size)
1372
1373 if store_on_disk:
1374 from rhodecode.apps.file_store import utils as store_utils
1375 from rhodecode.apps.file_store.exceptions import FileNotAllowedException, \
1376 FileOverSizeException
1377 from rhodecode.model.db import Session
1378
1379 image_key = md5_safe(email_address.lower()
1380 + first_name.lower() + last_name.lower())
1381
1382 storage = store_utils.get_file_storage(request.registry.settings)
1383 filename = '{}.svg'.format(image_key)
1384 subdir = 'gravatars'
1385 # since final name has a counter, we apply the 0
1386 uid = storage.apply_counter(0, store_utils.uid_filename(filename, randomized=False))
1387 store_uid = os.path.join(subdir, uid)
1388
1389 db_entry = FileStore.get_by_store_uid(store_uid)
1390 if db_entry:
1391 return request.route_path('download_file', fid=store_uid)
1392
1393 img_data = klass.get_img_data(svg_type=svg_type)
1394 img_file = store_utils.bytes_to_file_obj(img_data)
1395
1396 try:
1397 store_uid, metadata = storage.save_file(
1398 img_file, filename, directory=subdir,
1399 extensions=['.svg'], randomized_name=False)
1400 except (FileNotAllowedException, FileOverSizeException):
1401 raise
1402
1403 try:
1404 entry = FileStore.create(
1405 file_uid=store_uid, filename=metadata["filename"],
1406 file_hash=metadata["sha256"], file_size=metadata["size"],
1407 file_display_name=filename,
1408 file_description=u'user gravatar `{}`'.format(safe_unicode(filename)),
1409 hidden=True, check_acl=False, user_id=1
1410 )
1411 Session().add(entry)
1412 Session().commit()
1413 log.debug('Stored upload in DB as %s', entry)
1414 except Exception:
1415 raise
1416
1417 return request.route_path('download_file', fid=store_uid)
1418
1419 else:
1365 1420 return klass.generate_svg(svg_type=svg_type)
1366 1421
1367 1422
1423 def gravatar_external(request, gravatar_url_tmpl, email_address, size=30):
1424 return safe_str(gravatar_url_tmpl)\
1425 .replace('{email}', email_address) \
1426 .replace('{md5email}', md5_safe(email_address.lower())) \
1427 .replace('{netloc}', request.host) \
1428 .replace('{scheme}', request.scheme) \
1429 .replace('{size}', safe_str(size))
1430
1431
1368 1432 def gravatar_url(email_address, size=30, request=None):
1369 request = get_current_request()
1433 request = request or get_current_request()
1370 1434 _use_gravatar = request.call_context.visual.use_gravatar
1371 _gravatar_url = request.call_context.visual.gravatar_url
1372
1373 _gravatar_url = _gravatar_url or User.DEFAULT_GRAVATAR_URL
1374 1435
1375 1436 email_address = email_address or User.DEFAULT_USER_EMAIL
1376 1437 if isinstance(email_address, unicode):
@@ -1379,21 +1440,15 b' def gravatar_url(email_address, size=30,'
1379 1440
1380 1441 # empty email or default user
1381 1442 if not email_address or email_address == User.DEFAULT_USER_EMAIL:
1382 return initials_gravatar(User.DEFAULT_USER_EMAIL, '', '', size=size)
1443 return initials_gravatar(request, User.DEFAULT_USER_EMAIL, '', '', size=size)
1383 1444
1384 1445 if _use_gravatar:
1385 # TODO: Disuse pyramid thread locals. Think about another solution to
1386 # get the host and schema here.
1387 request = get_current_request()
1388 tmpl = safe_str(_gravatar_url)
1389 tmpl = tmpl.replace('{email}', email_address)\
1390 .replace('{md5email}', md5_safe(email_address.lower())) \
1391 .replace('{netloc}', request.host)\
1392 .replace('{scheme}', request.scheme)\
1393 .replace('{size}', safe_str(size))
1394 return tmpl
1446 gravatar_url_tmpl = request.call_context.visual.gravatar_url \
1447 or User.DEFAULT_GRAVATAR_URL
1448 return gravatar_external(request, gravatar_url_tmpl, email_address, size=size)
1449
1395 1450 else:
1396 return initials_gravatar(email_address, '', '', size=size)
1451 return initials_gravatar(request, email_address, '', '', size=size)
1397 1452
1398 1453
1399 1454 def breadcrumb_repo_link(repo):
@@ -1560,7 +1615,7 b' def _process_url_func(match_obj, repo_na'
1560 1615 # named regex variables
1561 1616 named_vars.update(match_obj.groupdict())
1562 1617 _url = string.Template(entry['url']).safe_substitute(**named_vars)
1563 desc = string.Template(entry['desc']).safe_substitute(**named_vars)
1618 desc = string.Template(escape(entry['desc'])).safe_substitute(**named_vars)
1564 1619 hovercard_url = string.Template(entry.get('hovercard_url', '')).safe_substitute(**named_vars)
1565 1620
1566 1621 def quote_cleaner(input_str):
@@ -1600,17 +1655,18 b' def get_active_pattern_entries(repo_name'
1600 1655
1601 1656 pr_pattern_re = re.compile(r'(?:(?:^!)|(?: !))(\d+)')
1602 1657
1658 allowed_link_formats = [
1659 'html', 'rst', 'markdown', 'html+hovercard', 'rst+hovercard', 'markdown+hovercard']
1660
1603 1661
1604 1662 def process_patterns(text_string, repo_name, link_format='html', active_entries=None):
1605 1663
1606 allowed_formats = ['html', 'rst', 'markdown',
1607 'html+hovercard', 'rst+hovercard', 'markdown+hovercard']
1608 if link_format not in allowed_formats:
1664 if link_format not in allowed_link_formats:
1609 1665 raise ValueError('Link format can be only one of:{} got {}'.format(
1610 allowed_formats, link_format))
1666 allowed_link_formats, link_format))
1611 1667
1612 1668 if active_entries is None:
1613 log.debug('Fetch active patterns for repo: %s', repo_name)
1669 log.debug('Fetch active issue tracker patterns for repo: %s', repo_name)
1614 1670 active_entries = get_active_pattern_entries(repo_name)
1615 1671
1616 1672 issues_data = []
@@ -1668,7 +1724,8 b' def process_patterns(text_string, repo_n'
1668 1724 return new_text, issues_data
1669 1725
1670 1726
1671 def urlify_commit_message(commit_text, repository=None, active_pattern_entries=None):
1727 def urlify_commit_message(commit_text, repository=None, active_pattern_entries=None,
1728 issues_container=None):
1672 1729 """
1673 1730 Parses given text message and makes proper links.
1674 1731 issues are linked to given issue-server, and rest is a commit link
@@ -1691,6 +1748,9 b' def urlify_commit_message(commit_text, r'
1691 1748 new_text, issues = process_patterns(new_text, repository or '',
1692 1749 active_entries=active_pattern_entries)
1693 1750
1751 if issues_container is not None:
1752 issues_container.extend(issues)
1753
1694 1754 return literal(new_text)
1695 1755
1696 1756
@@ -1731,7 +1791,7 b' def renderer_from_filename(filename, exc'
1731 1791
1732 1792
1733 1793 def render(source, renderer='rst', mentions=False, relative_urls=None,
1734 repo_name=None, active_pattern_entries=None):
1794 repo_name=None, active_pattern_entries=None, issues_container=None):
1735 1795
1736 1796 def maybe_convert_relative_links(html_source):
1737 1797 if relative_urls:
@@ -1748,6 +1808,8 b" def render(source, renderer='rst', menti"
1748 1808 source, issues = process_patterns(
1749 1809 source, repo_name, link_format='rst',
1750 1810 active_entries=active_pattern_entries)
1811 if issues_container is not None:
1812 issues_container.extend(issues)
1751 1813
1752 1814 return literal(
1753 1815 '<div class="rst-block">%s</div>' %
@@ -1760,6 +1822,8 b" def render(source, renderer='rst', menti"
1760 1822 source, issues = process_patterns(
1761 1823 source, repo_name, link_format='markdown',
1762 1824 active_entries=active_pattern_entries)
1825 if issues_container is not None:
1826 issues_container.extend(issues)
1763 1827
1764 1828 return literal(
1765 1829 '<div class="markdown-block">%s</div>' %
@@ -139,6 +139,18 b' def is_vcs_call(environ):'
139 139 return False
140 140
141 141
142 def get_path_elem(route_path):
143 if not route_path:
144 return None
145
146 cleaned_route_path = route_path.lstrip('/')
147 if cleaned_route_path:
148 cleaned_route_path_elems = cleaned_route_path.split('/')
149 if cleaned_route_path_elems:
150 return cleaned_route_path_elems[0]
151 return None
152
153
142 154 def detect_vcs_request(environ, backends):
143 155 checks = {
144 156 'hg': (is_hg, SimpleHg),
@@ -146,6 +158,17 b' def detect_vcs_request(environ, backends'
146 158 'svn': (is_svn, SimpleSvn),
147 159 }
148 160 handler = None
161 # List of path views first chunk we don't do any checks
162 white_list = [
163 # e.g /_file_store/download
164 '_file_store'
165 ]
166
167 path_info = environ['PATH_INFO']
168
169 if get_path_elem(path_info) in white_list:
170 log.debug('path `%s` in whitelist, skipping...', path_info)
171 return handler
149 172
150 173 if VCS_TYPE_KEY in environ:
151 174 raw_type = environ[VCS_TYPE_KEY]
@@ -224,7 +224,10 b' class RedisAuthSessions(BaseAuthSessions'
224 224 data = client.get(key)
225 225 if data:
226 226 json_data = pickle.loads(data)
227 try:
227 228 accessed_time = json_data['_accessed_time']
229 except KeyError:
230 accessed_time = 0
228 231 if accessed_time < expiry_time:
229 232 client.delete(key)
230 233 deleted_keys += 1
@@ -212,10 +212,10 b' class ChangesetStatusModel(BaseModel):'
212 212 # TODO(marcink): with group voting, how does rejected work,
213 213 # do we ever get rejected state ?
214 214
215 if approved_votes_count == reviewers_number:
215 if approved_votes_count and (approved_votes_count == reviewers_number):
216 216 return ChangesetStatus.STATUS_APPROVED
217 217
218 if rejected_votes_count == reviewers_number:
218 if rejected_votes_count and (rejected_votes_count == reviewers_number):
219 219 return ChangesetStatus.STATUS_REJECTED
220 220
221 221 return ChangesetStatus.STATUS_UNDER_REVIEW
@@ -354,34 +354,37 b' class ChangesetStatusModel(BaseModel):'
354 354 Session().add(new_status)
355 355 return new_statuses
356 356
357 def aggregate_votes_by_user(self, commit_statuses, reviewers_data):
358
359 commit_statuses_map = collections.defaultdict(list)
360 for st in commit_statuses:
361 commit_statuses_map[st.author.username] += [st]
362
363 reviewers = []
364
365 def version(commit_status):
366 return commit_status.version
367
368 for obj in reviewers_data:
369 if not obj.user:
370 continue
371 statuses = commit_statuses_map.get(obj.user.username, None)
372 if statuses:
373 status_groups = itertools.groupby(
374 sorted(statuses, key=version), version)
375 statuses = [(x, list(y)[0]) for x, y in status_groups]
376
377 reviewers.append((obj, obj.user, obj.reasons, obj.mandatory, statuses))
378
379 return reviewers
380
357 381 def reviewers_statuses(self, pull_request):
358 382 _commit_statuses = self.get_statuses(
359 383 pull_request.source_repo,
360 384 pull_request=pull_request,
361 385 with_revisions=True)
362 386
363 commit_statuses = collections.defaultdict(list)
364 for st in _commit_statuses:
365 commit_statuses[st.author.username] += [st]
366
367 pull_request_reviewers = []
368
369 def version(commit_status):
370 return commit_status.version
371
372 for obj in pull_request.reviewers:
373 if not obj.user:
374 continue
375 statuses = commit_statuses.get(obj.user.username, None)
376 if statuses:
377 status_groups = itertools.groupby(
378 sorted(statuses, key=version), version)
379 statuses = [(x, list(y)[0]) for x, y in status_groups]
380
381 pull_request_reviewers.append(
382 (obj, obj.user, obj.reasons, obj.mandatory, statuses))
383
384 return pull_request_reviewers
387 return self.aggregate_votes_by_user(_commit_statuses, pull_request.reviewers)
385 388
386 389 def calculated_review_status(self, pull_request, reviewers_statuses=None):
387 390 """
@@ -91,8 +91,7 b' class CommentsModel(BaseModel):'
91 91 # group by versions, and count until, and display objects
92 92
93 93 comment_groups = collections.defaultdict(list)
94 [comment_groups[
95 _co.pull_request_version_id].append(_co) for _co in comments]
94 [comment_groups[_co.pull_request_version_id].append(_co) for _co in comments]
96 95
97 96 def yield_comments(pos):
98 97 for co in comment_groups[pos]:
@@ -229,6 +228,14 b' class CommentsModel(BaseModel):'
229 228
230 229 return todos
231 230
231 def get_commit_inline_comments(self, commit_id):
232 inline_comments = Session().query(ChangesetComment) \
233 .filter(ChangesetComment.line_no != None) \
234 .filter(ChangesetComment.f_path != None) \
235 .filter(ChangesetComment.revision == commit_id)
236 inline_comments = inline_comments.all()
237 return inline_comments
238
232 239 def _log_audit_action(self, action, action_data, auth_user, comment):
233 240 audit_logger.store(
234 241 action=action,
@@ -456,37 +463,53 b' class CommentsModel(BaseModel):'
456 463 else:
457 464 action = 'repo.commit.comment.create'
458 465
466 comment_id = comment.comment_id
459 467 comment_data = comment.get_api_data()
468
460 469 self._log_audit_action(
461 470 action, {'data': comment_data}, auth_user, comment)
462 471
463 msg_url = ''
464 472 channel = None
465 473 if commit_obj:
466 msg_url = commit_comment_url
467 474 repo_name = repo.repo_name
468 475 channel = u'/repo${}$/commit/{}'.format(
469 476 repo_name,
470 477 commit_obj.raw_id
471 478 )
472 479 elif pull_request_obj:
473 msg_url = pr_comment_url
474 480 repo_name = pr_target_repo.repo_name
475 481 channel = u'/repo${}$/pr/{}'.format(
476 482 repo_name,
477 pull_request_id
483 pull_request_obj.pull_request_id
478 484 )
479 485
480 message = '<strong>{}</strong> {} - ' \
481 '<a onclick="window.location=\'{}\';' \
482 'window.location.reload()">' \
483 '<strong>{}</strong></a>'
486 if channel:
487 username = user.username
488 message = '<strong>{}</strong> {} #{}, {}'
484 489 message = message.format(
485 user.username, _('made a comment'), msg_url,
486 _('Show it now'))
490 username,
491 _('posted a new comment'),
492 comment_id,
493 _('Refresh the page to see new comments.'))
494
495 message_obj = {
496 'message': message,
497 'level': 'success',
498 'topic': '/notifications'
499 }
487 500
488 501 channelstream.post_message(
489 channel, message, user.username,
502 channel, message_obj, user.username,
503 registry=get_current_registry())
504
505 message_obj = {
506 'message': None,
507 'user': username,
508 'comment_id': comment_id,
509 'topic': '/comment'
510 }
511 channelstream.post_message(
512 channel, message_obj, user.username,
490 513 registry=get_current_registry())
491 514
492 515 return comment
@@ -641,16 +664,16 b' class CommentsModel(BaseModel):'
641 664 q = self._get_inline_comments_query(repo_id, revision, pull_request)
642 665 return self._group_comments_by_path_and_line_number(q)
643 666
644 def get_inline_comments_count(self, inline_comments, skip_outdated=True,
667 def get_inline_comments_as_list(self, inline_comments, skip_outdated=True,
645 668 version=None):
646 inline_cnt = 0
669 inline_comms = []
647 670 for fname, per_line_comments in inline_comments.iteritems():
648 671 for lno, comments in per_line_comments.iteritems():
649 672 for comm in comments:
650 673 if not comm.outdated_at_version(version) and skip_outdated:
651 inline_cnt += 1
674 inline_comms.append(comm)
652 675
653 return inline_cnt
676 return inline_comms
654 677
655 678 def get_outdated_comments(self, repo_id, pull_request):
656 679 # TODO: johbo: Remove `repo_id`, it is not needed to find the comments
@@ -3810,6 +3810,10 b' class ChangesetComment(Base, BaseModel):'
3810 3810 return self.display_state == self.COMMENT_OUTDATED
3811 3811
3812 3812 @property
3813 def outdated_js(self):
3814 return json.dumps(self.display_state == self.COMMENT_OUTDATED)
3815
3816 @property
3813 3817 def immutable(self):
3814 3818 return self.immutable_state == self.OP_IMMUTABLE
3815 3819
@@ -3817,16 +3821,35 b' class ChangesetComment(Base, BaseModel):'
3817 3821 """
3818 3822 Checks if comment is outdated for given pull request version
3819 3823 """
3820 return self.outdated and self.pull_request_version_id != version
3824 def version_check():
3825 return self.pull_request_version_id and self.pull_request_version_id != version
3826
3827 if self.is_inline:
3828 return self.outdated and version_check()
3829 else:
3830 # general comments don't have .outdated set, also latest don't have a version
3831 return version_check()
3832
3833 def outdated_at_version_js(self, version):
3834 """
3835 Checks if comment is outdated for given pull request version
3836 """
3837 return json.dumps(self.outdated_at_version(version))
3821 3838
3822 3839 def older_than_version(self, version):
3823 3840 """
3824 3841 Checks if comment is made from previous version than given
3825 3842 """
3826 3843 if version is None:
3827 return self.pull_request_version_id is not None
3828
3829 return self.pull_request_version_id < version
3844 return self.pull_request_version != version
3845
3846 return self.pull_request_version < version
3847
3848 def older_than_version_js(self, version):
3849 """
3850 Checks if comment is made from previous version than given
3851 """
3852 return json.dumps(self.older_than_version(version))
3830 3853
3831 3854 @property
3832 3855 def commit_id(self):
@@ -3843,7 +3866,9 b' class ChangesetComment(Base, BaseModel):'
3843 3866
3844 3867 @property
3845 3868 def is_inline(self):
3846 return self.line_no and self.f_path
3869 if self.line_no and self.f_path:
3870 return True
3871 return False
3847 3872
3848 3873 @property
3849 3874 def last_version(self):
@@ -3856,6 +3881,16 b' class ChangesetComment(Base, BaseModel):'
3856 3881 return self.get_index_from_version(
3857 3882 self.pull_request_version_id, versions)
3858 3883
3884 @property
3885 def review_status(self):
3886 if self.status_change:
3887 return self.status_change[0].status
3888
3889 @property
3890 def review_status_lbl(self):
3891 if self.status_change:
3892 return self.status_change[0].status_lbl
3893
3859 3894 def __repr__(self):
3860 3895 if self.comment_id:
3861 3896 return '<DB:Comment #%s>' % self.comment_id
@@ -4134,6 +4169,23 b' class _PullRequestBase(BaseModel):'
4134 4169 return json.dumps(self.reviewer_data)
4135 4170
4136 4171 @property
4172 def last_merge_metadata_parsed(self):
4173 metadata = {}
4174 if not self.last_merge_metadata:
4175 return metadata
4176
4177 if hasattr(self.last_merge_metadata, 'de_coerce'):
4178 for k, v in self.last_merge_metadata.de_coerce().items():
4179 if k in ['target_ref', 'source_ref']:
4180 metadata[k] = Reference(v['type'], v['name'], v['commit_id'])
4181 else:
4182 if hasattr(v, 'de_coerce'):
4183 metadata[k] = v.de_coerce()
4184 else:
4185 metadata[k] = v
4186 return metadata
4187
4188 @property
4137 4189 def work_in_progress(self):
4138 4190 """checks if pull request is work in progress by checking the title"""
4139 4191 title = self.title.upper()
@@ -4306,6 +4358,7 b' class PullRequest(Base, _PullRequestBase'
4306 4358 __table_args__ = (
4307 4359 base_table_args,
4308 4360 )
4361 LATEST_VER = 'latest'
4309 4362
4310 4363 pull_request_id = Column(
4311 4364 'pull_request_id', Integer(), nullable=False, primary_key=True)
@@ -4364,6 +4417,10 b' class PullRequest(Base, _PullRequestBase'
4364 4417 def pull_request_version_id(self):
4365 4418 return getattr(pull_request_obj, 'pull_request_version_id', None)
4366 4419
4420 @property
4421 def pull_request_last_version(self):
4422 return pull_request_obj.pull_request_last_version
4423
4367 4424 attrs = StrictAttributeDict(pull_request_obj.get_api_data(with_merge_state=False))
4368 4425
4369 4426 attrs.author = StrictAttributeDict(
@@ -4428,6 +4485,10 b' class PullRequest(Base, _PullRequestBase'
4428 4485 """
4429 4486 return self.versions.count() + 1
4430 4487
4488 @property
4489 def pull_request_last_version(self):
4490 return self.versions_count
4491
4431 4492
4432 4493 class PullRequestVersion(Base, _PullRequestBase):
4433 4494 __tablename__ = 'pull_request_versions'
@@ -4475,6 +4536,8 b' class PullRequestReviewers(Base, BaseMod'
4475 4536 __table_args__ = (
4476 4537 base_table_args,
4477 4538 )
4539 ROLE_REVIEWER = u'reviewer'
4540 ROLE_OBSERVER = u'observer'
4478 4541
4479 4542 @hybrid_property
4480 4543 def reasons(self):
@@ -4502,6 +4565,8 b' class PullRequestReviewers(Base, BaseMod'
4502 4565 JsonType('list', dialect_map=dict(mysql=UnicodeText(16384)))))
4503 4566
4504 4567 mandatory = Column("mandatory", Boolean(), nullable=False, default=False)
4568 role = Column('role', Unicode(255), nullable=True, default=ROLE_REVIEWER)
4569
4505 4570 user = relationship('User')
4506 4571 pull_request = relationship('PullRequest')
4507 4572
@@ -5425,7 +5490,10 b' class FileStore(Base, BaseModel):'
5425 5490 repo_group = relationship('RepoGroup', lazy='joined')
5426 5491
5427 5492 @classmethod
5428 def get_by_store_uid(cls, file_store_uid):
5493 def get_by_store_uid(cls, file_store_uid, safe=False):
5494 if safe:
5495 return FileStore.query().filter(FileStore.file_uid == file_store_uid).first()
5496 else:
5429 5497 return FileStore.query().filter(FileStore.file_uid == file_store_uid).scalar()
5430 5498
5431 5499 @classmethod
@@ -1600,7 +1600,7 b' class PullRequestModel(BaseModel):'
1600 1600 'source_ref': pull_request.source_ref_parts,
1601 1601 }
1602 1602 if pull_request.last_merge_metadata:
1603 metadata.update(pull_request.last_merge_metadata)
1603 metadata.update(pull_request.last_merge_metadata_parsed)
1604 1604
1605 1605 if not possible and target_ref.type == 'branch':
1606 1606 # NOTE(marcink): case for mercurial multiple heads on branch
@@ -55,3 +55,16 b''
55 55 margin: 0 auto 35px auto;
56 56 }
57 57 }
58
59 .alert-text-success {
60 color: @alert1;
61
62 }
63
64 .alert-text-error {
65 color: @alert2;
66 }
67
68 .alert-text-warning {
69 color: @alert3;
70 }
@@ -254,7 +254,7 b' input[type="button"] {'
254 254
255 255 .btn-group-actions {
256 256 position: relative;
257 z-index: 100;
257 z-index: 50;
258 258
259 259 &:not(.open) .btn-action-switcher-container {
260 260 display: none;
@@ -1078,10 +1078,16 b' input.filediff-collapse-state {'
1078 1078 background: @color5;
1079 1079 color: white;
1080 1080 }
1081
1081 1082 &[op="comments"] { /* comments on file */
1082 1083 background: @grey4;
1083 1084 color: white;
1084 1085 }
1086
1087 &[op="options"] { /* context menu */
1088 background: @grey6;
1089 color: black;
1090 }
1085 1091 }
1086 1092 }
1087 1093
@@ -31,6 +31,10 b' a { cursor: pointer; }'
31 31 clear: both;
32 32 }
33 33
34 .display-none {
35 display: none;
36 }
37
34 38 .pull-right {
35 39 float: right !important;
36 40 }
@@ -240,14 +240,14 b' div.markdown-block ol {'
240 240 div.markdown-block ul.checkbox li,
241 241 div.markdown-block ol.checkbox li {
242 242 list-style: none !important;
243 margin: 6px !important;
243 margin: 0px !important;
244 244 padding: 0 !important;
245 245 }
246 246
247 247 div.markdown-block ul li,
248 248 div.markdown-block ol li {
249 249 list-style: disc !important;
250 margin: 6px !important;
250 margin: 0px !important;
251 251 padding: 0 !important;
252 252 }
253 253
@@ -83,6 +83,11 b' body {'
83 83 }
84 84 }
85 85
86 .flex-container {
87 display: flex;
88 justify-content: space-between;
89 }
90
86 91 .action-link{
87 92 margin-left: @padding;
88 93 padding-left: @padding;
@@ -482,10 +487,15 b' ul.auth_plugins {'
482 487 text-align: left;
483 488 overflow: hidden;
484 489 white-space: pre-line;
485 }
486
487 .pr-details-title {
488 height: 16px
490 padding-top: 5px
491 }
492
493 #add_reviewer {
494 padding-top: 10px;
495 }
496
497 #add_reviewer_input {
498 padding-top: 10px
489 499 }
490 500
491 501 .pr-details-title-author-pref {
@@ -1173,9 +1183,12 b' label {'
1173 1183 a {
1174 1184 color: @grey5
1175 1185 }
1176 @media screen and (max-width: 1200px) {
1186
1187 // 1024px or smaller
1188 @media screen and (max-width: 1180px) {
1177 1189 display: none;
1178 1190 }
1191
1179 1192 }
1180 1193
1181 1194 img {
@@ -1492,26 +1505,17 b' table.integrations {'
1492 1505
1493 1506 // Pull Requests
1494 1507 .summary-details {
1495 width: 72%;
1508 width: 100%;
1496 1509 }
1497 1510 .pr-summary {
1498 1511 border-bottom: @border-thickness solid @grey5;
1499 1512 margin-bottom: @space;
1500 1513 }
1501 1514
1502 .reviewers-title {
1503 width: 25%;
1504 min-width: 200px;
1505
1506 &.first-panel {
1507 margin-top: 34px;
1508 }
1509 }
1510
1511 1515 .reviewers {
1512 width: 25%;
1513 min-width: 200px;
1514 }
1516 width: 98%;
1517 }
1518
1515 1519 .reviewers ul li {
1516 1520 position: relative;
1517 1521 width: 100%;
@@ -1523,18 +1527,14 b' table.integrations {'
1523 1527 min-height: 55px;
1524 1528 }
1525 1529
1526 .reviewers_member {
1527 width: 100%;
1528 overflow: auto;
1529 }
1530 1530 .reviewer_reason {
1531 1531 padding-left: 20px;
1532 1532 line-height: 1.5em;
1533 1533 }
1534 1534 .reviewer_status {
1535 1535 display: inline-block;
1536 width: 25px;
1537 min-width: 25px;
1536 width: 20px;
1537 min-width: 20px;
1538 1538 height: 1.2em;
1539 1539 line-height: 1em;
1540 1540 }
@@ -1557,25 +1557,20 b' table.integrations {'
1557 1557 }
1558 1558
1559 1559 .reviewer_member_mandatory {
1560 position: absolute;
1561 left: 15px;
1562 top: 8px;
1563 1560 width: 16px;
1564 1561 font-size: 11px;
1565 1562 margin: 0;
1566 1563 padding: 0;
1567 1564 color: black;
1565 opacity: 0.4;
1568 1566 }
1569 1567
1570 1568 .reviewer_member_mandatory_remove,
1571 1569 .reviewer_member_remove {
1572 position: absolute;
1573 right: 0;
1574 top: 0;
1575 1570 width: 16px;
1576 margin-bottom: 10px;
1577 1571 padding: 0;
1578 1572 color: black;
1573 cursor: pointer;
1579 1574 }
1580 1575
1581 1576 .reviewer_member_mandatory_remove {
@@ -1593,6 +1588,9 b' table.integrations {'
1593 1588 cursor: pointer;
1594 1589 }
1595 1590 .pr-details-title {
1591 height: 20px;
1592 line-height: 20px;
1593
1596 1594 padding-bottom: 8px;
1597 1595 border-bottom: @border-thickness solid @grey5;
1598 1596
@@ -1617,7 +1615,7 b' table.integrations {'
1617 1615 text-decoration: line-through;
1618 1616 }
1619 1617
1620 .todo-table {
1618 .todo-table, .comments-table {
1621 1619 width: 100%;
1622 1620
1623 1621 td {
@@ -1627,7 +1625,8 b' table.integrations {'
1627 1625 .td-todo-number {
1628 1626 text-align: left;
1629 1627 white-space: nowrap;
1630 width: 15%;
1628 width: 1%;
1629 padding-right: 2px;
1631 1630 }
1632 1631
1633 1632 .td-todo-gravatar {
@@ -1651,10 +1650,13 b' table.integrations {'
1651 1650 text-overflow: ellipsis;
1652 1651 }
1653 1652
1653 table.group_members {
1654 width: 100%
1655 }
1656
1654 1657 .group_members {
1655 1658 margin-top: 0;
1656 1659 padding: 0;
1657 list-style: outside none none;
1658 1660
1659 1661 img {
1660 1662 height: @gravatar-size;
@@ -1698,7 +1700,7 b' table.integrations {'
1698 1700 }
1699 1701
1700 1702 .reviewer_ac .ac-input {
1701 width: 92%;
1703 width: 100%;
1702 1704 margin-bottom: 1em;
1703 1705 }
1704 1706
@@ -2772,7 +2774,7 b' table.rctable td.td-search-results div {'
2772 2774 }
2773 2775
2774 2776 #help_kb .modal-content{
2775 max-width: 750px;
2777 max-width: 800px;
2776 2778 margin: 10% auto;
2777 2779
2778 2780 table{
@@ -3069,4 +3071,141 b' form.markup-form {'
3069 3071
3070 3072 .pr-hovercard-title {
3071 3073 padding-top: 5px;
3072 } No newline at end of file
3074 }
3075
3076 .action-divider {
3077 opacity: 0.5;
3078 }
3079
3080 .details-inline-block {
3081 display: inline-block;
3082 position: relative;
3083 }
3084
3085 .details-inline-block summary {
3086 list-style: none;
3087 }
3088
3089 details:not([open]) > :not(summary) {
3090 display: none !important;
3091 }
3092
3093 .details-reset > summary {
3094 list-style: none;
3095 }
3096
3097 .details-reset > summary::-webkit-details-marker {
3098 display: none;
3099 }
3100
3101 .details-dropdown {
3102 position: absolute;
3103 top: 100%;
3104 width: 185px;
3105 list-style: none;
3106 background-color: #fff;
3107 background-clip: padding-box;
3108 border: 1px solid @grey5;
3109 box-shadow: 0 8px 24px rgba(149, 157, 165, .2);
3110 left: -150px;
3111 text-align: left;
3112 z-index: 90;
3113 }
3114
3115 .dropdown-divider {
3116 display: block;
3117 height: 0;
3118 margin: 8px 0;
3119 border-top: 1px solid @grey5;
3120 }
3121
3122 .dropdown-item {
3123 display: block;
3124 padding: 4px 8px 4px 16px;
3125 overflow: hidden;
3126 text-overflow: ellipsis;
3127 white-space: nowrap;
3128 font-weight: normal;
3129 }
3130
3131 .right-sidebar {
3132 position: fixed;
3133 top: 0px;
3134 bottom: 0;
3135 right: 0;
3136
3137 background: #fafafa;
3138 z-index: 50;
3139 }
3140
3141 .right-sidebar {
3142 border-left: 1px solid @grey5;
3143 }
3144
3145 .right-sidebar.right-sidebar-expanded {
3146 width: 300px;
3147 overflow: scroll;
3148 }
3149
3150 .right-sidebar.right-sidebar-collapsed {
3151 width: 40px;
3152 padding: 0;
3153 display: block;
3154 overflow: hidden;
3155 }
3156
3157 .sidenav {
3158 float: right;
3159 will-change: min-height;
3160 background: #fafafa;
3161 width: 100%;
3162 }
3163
3164 .sidebar-toggle {
3165 height: 30px;
3166 text-align: center;
3167 margin: 15px 0px 0 0;
3168 }
3169
3170 .sidebar-toggle a {
3171
3172 }
3173
3174 .sidebar-content {
3175 margin-left: 15px;
3176 margin-right: 15px;
3177 }
3178
3179 .sidebar-heading {
3180 font-size: 1.2em;
3181 font-weight: 700;
3182 margin-top: 10px;
3183 }
3184
3185 .sidebar-element {
3186 margin-top: 20px;
3187 }
3188
3189 .right-sidebar-collapsed-state {
3190 display: flex;
3191 flex-direction: column;
3192 justify-content: center;
3193 align-items: center;
3194 padding: 0 10px;
3195 cursor: pointer;
3196 font-size: 1.3em;
3197 margin: 0 -15px;
3198 }
3199
3200 .right-sidebar-collapsed-state:hover {
3201 background-color: @grey5;
3202 }
3203
3204 .old-comments-marker {
3205 text-align: left;
3206 }
3207
3208 .old-comments-marker td {
3209 padding-top: 15px;
3210 border-bottom: 1px solid @grey5;
3211 }
@@ -790,7 +790,7 b' input {'
790 790
791 791 &.main_filter_input {
792 792 padding: 5px 10px;
793 min-width: 340px;
793
794 794 color: @grey7;
795 795 background: @black;
796 796 min-height: 18px;
@@ -800,11 +800,34 b' input {'
800 800 color: @grey2 !important;
801 801 background: white !important;
802 802 }
803
803 804 &:focus {
804 805 color: @grey2 !important;
805 806 background: white !important;
806 807 }
808
809 min-width: 360px;
810
811 @media screen and (max-width: 1600px) {
812 min-width: 300px;
807 813 }
814 @media screen and (max-width: 1500px) {
815 min-width: 280px;
816 }
817 @media screen and (max-width: 1400px) {
818 min-width: 260px;
819 }
820 @media screen and (max-width: 1300px) {
821 min-width: 240px;
822 }
823 @media screen and (max-width: 1200px) {
824 min-width: 220px;
825 }
826 @media screen and (max-width: 720px) {
827 min-width: 140px;
828 }
829 }
830
808 831 }
809 832
810 833
@@ -168,6 +168,7 b''
168 168 .icon-remove:before { content: '\e810'; } /* '' */
169 169 .icon-fork:before { content: '\e811'; } /* 'ξ ‘' */
170 170 .icon-more:before { content: '\e812'; } /* 'ξ ’' */
171 .icon-options:before { content: '\e812'; } /* 'ξ ’' */
171 172 .icon-search:before { content: '\e813'; } /* 'ξ “' */
172 173 .icon-scissors:before { content: '\e814'; } /* 'ξ ”' */
173 174 .icon-download:before { content: '\e815'; } /* 'ξ •' */
@@ -251,6 +252,7 b''
251 252 // TRANSFORM
252 253 .icon-merge:before {transform: rotate(180deg);}
253 254 .icon-wide-mode:before {transform: rotate(90deg);}
255 .icon-options:before {transform: rotate(90deg);}
254 256
255 257 // -- END ICON CLASSES -- //
256 258
@@ -131,6 +131,11 b' function setRCMouseBindings(repoName, re'
131 131 window.location = pyroutes.url(
132 132 'edit_repo_perms', {'repo_name': repoName});
133 133 });
134 Mousetrap.bind(['t s'], function(e) {
135 if (window.toggleSidebar !== undefined) {
136 window.toggleSidebar();
137 }
138 });
134 139 }
135 140 }
136 141
@@ -246,6 +246,8 b' function registerRCRoutes() {'
246 246 pyroutes.register('pullrequest_comment_create', '/%(repo_name)s/pull-request/%(pull_request_id)s/comment', ['repo_name', 'pull_request_id']);
247 247 pyroutes.register('pullrequest_comment_edit', '/%(repo_name)s/pull-request/%(pull_request_id)s/comment/%(comment_id)s/edit', ['repo_name', 'pull_request_id', 'comment_id']);
248 248 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']);
249 pyroutes.register('pullrequest_comments', '/%(repo_name)s/pull-request/%(pull_request_id)s/comments', ['repo_name', 'pull_request_id']);
250 pyroutes.register('pullrequest_todos', '/%(repo_name)s/pull-request/%(pull_request_id)s/todos', ['repo_name', 'pull_request_id']);
249 251 pyroutes.register('edit_repo', '/%(repo_name)s/settings', ['repo_name']);
250 252 pyroutes.register('edit_repo_advanced', '/%(repo_name)s/settings/advanced', ['repo_name']);
251 253 pyroutes.register('edit_repo_advanced_archive', '/%(repo_name)s/settings/advanced/archive', ['repo_name']);
@@ -28,9 +28,12 b' export class RhodecodeApp extends Polyme'
28 28 super.connectedCallback();
29 29 ccLog.debug('rhodeCodeApp created');
30 30 $.Topic('/notifications').subscribe(this.handleNotifications.bind(this));
31 $.Topic('/comment').subscribe(this.handleComment.bind(this));
31 32 $.Topic('/favicon/update').subscribe(this.faviconUpdate.bind(this));
32 33 $.Topic('/connection_controller/subscribe').subscribe(
33 this.subscribeToChannelTopic.bind(this));
34 this.subscribeToChannelTopic.bind(this)
35 );
36
34 37 // this event can be used to coordinate plugins to do their
35 38 // initialization before channelstream is kicked off
36 39 $.Topic('/__MAIN_APP__').publish({});
@@ -71,6 +74,14 b' export class RhodecodeApp extends Polyme'
71 74
72 75 }
73 76
77 handleComment(data) {
78 if (data.message.comment_id) {
79 if (window.refreshAllComments !== undefined) {
80 refreshAllComments()
81 }
82 }
83 }
84
74 85 faviconUpdate(data) {
75 86 this.shadowRoot.querySelector('rhodecode-favicon').counter = data.count;
76 87 }
@@ -95,6 +106,7 b' export class RhodecodeApp extends Polyme'
95 106 }
96 107 // append any additional channels registered in other plugins
97 108 $.Topic('/connection_controller/subscribe').processPrepared();
109
98 110 channelstreamConnection.connect();
99 111 }
100 112 }
@@ -157,8 +169,7 b' export class RhodecodeApp extends Polyme'
157 169
158 170 handleConnected(event) {
159 171 var channelstreamConnection = this.getChannelStreamConnection();
160 channelstreamConnection.set('channelsState',
161 event.detail.channels_info);
172 channelstreamConnection.set('channelsState', event.detail.channels_info);
162 173 channelstreamConnection.set('userState', event.detail.state);
163 174 channelstreamConnection.set('channels', event.detail.channels);
164 175 this.propagageChannelsState();
@@ -299,13 +299,22 b' var tooltipActivate = function () {'
299 299 var altHovercard =$origin.data('hovercardAlt');
300 300
301 301 if (hovercardUrl !== undefined && hovercardUrl !== "") {
302 var urlLoad = true;
302 303 if (hovercardUrl.substr(0,12) === 'pyroutes.url'){
303 304 hovercardUrl = eval(hovercardUrl)
305 } else if (hovercardUrl.substr(0, 11) === 'javascript:') {
306 var jsFunc = hovercardUrl.substr(11);
307 urlLoad = false;
308 loaded = true;
309 instance.content(eval(jsFunc))
304 310 }
305 311
312 if (urlLoad) {
306 313 var loaded = loadHoverCard(hovercardUrl, altHovercard, function (data) {
307 314 instance.content(data);
308 315 })
316 }
317
309 318 } else {
310 319 if ($origin.data('hovercardAltHtml')) {
311 320 var data = atob($origin.data('hovercardAltHtml'));
@@ -677,7 +686,9 b' var feedLifetimeOptions = function(query'
677 686 query.callback(data);
678 687 };
679 688
680
689 /*
690 * Retrievew via templateContext.session_attrs.key
691 * */
681 692 var storeUserSessionAttr = function (key, val) {
682 693
683 694 var postData = {
@@ -670,8 +670,20 b' var CommentsController = function() {'
670 670
671 671 var success = function(response) {
672 672 $comment.remove();
673
674 if (window.updateSticky !== undefined) {
675 // potentially our comments change the active window size, so we
676 // notify sticky elements
677 updateSticky()
678 }
679
680 if (window.refreshAllComments !== undefined) {
681 // if we have this handler, run it, and refresh all comments boxes
682 refreshAllComments()
683 }
673 684 return false;
674 685 };
686
675 687 var failure = function(jqXHR, textStatus, errorThrown) {
676 688 var prefix = "Error while deleting this comment.\n"
677 689 var message = formatErrorMessage(jqXHR, textStatus, errorThrown, prefix);
@@ -682,6 +694,9 b' var CommentsController = function() {'
682 694 return false;
683 695 };
684 696 ajaxPOST(url, postData, success, failure);
697
698
699
685 700 }
686 701
687 702 this.deleteComment = function(node) {
@@ -727,6 +742,15 b' var CommentsController = function() {'
727 742 $filediff.find('.hide-line-comments').removeClass('hide-line-comments');
728 743 $filediff.toggleClass('hide-comments');
729 744 }
745
746 // since we change the height of the diff container that has anchor points for upper
747 // sticky header, we need to tell it to re-calculate those
748 if (window.updateSticky !== undefined) {
749 // potentially our comments change the active window size, so we
750 // notify sticky elements
751 updateSticky()
752 }
753
730 754 return false;
731 755 };
732 756
@@ -747,7 +771,7 b' var CommentsController = function() {'
747 771 var cm = commentForm.getCmInstance();
748 772
749 773 if (resolvesCommentId){
750 var placeholderText = _gettext('Leave a resolution comment, or click resolve button to resolve TODO comment #{0}').format(resolvesCommentId);
774 placeholderText = _gettext('Leave a resolution comment, or click resolve button to resolve TODO comment #{0}').format(resolvesCommentId);
751 775 }
752 776
753 777 setTimeout(function() {
@@ -1077,9 +1101,15 b' var CommentsController = function() {'
1077 1101 updateSticky()
1078 1102 }
1079 1103
1104 if (window.refreshAllComments !== undefined) {
1105 // if we have this handler, run it, and refresh all comments boxes
1106 refreshAllComments()
1107 }
1108
1080 1109 commentForm.setActionButtonsDisabled(false);
1081 1110
1082 1111 };
1112
1083 1113 var submitFailCallback = function(jqXHR, textStatus, errorThrown) {
1084 1114 var prefix = "Error while editing comment.\n"
1085 1115 var message = formatErrorMessage(jqXHR, textStatus, errorThrown, prefix);
@@ -1209,6 +1239,11 b' var CommentsController = function() {'
1209 1239 updateSticky()
1210 1240 }
1211 1241
1242 if (window.refreshAllComments !== undefined) {
1243 // if we have this handler, run it, and refresh all comments boxes
1244 refreshAllComments()
1245 }
1246
1212 1247 commentForm.setActionButtonsDisabled(false);
1213 1248
1214 1249 };
@@ -35,4 +35,75 b' var quick_repo_menu = function() {'
35 35 }, function() {
36 36 hide_quick_repo_menus();
37 37 });
38 }; No newline at end of file
38 };
39
40
41 window.toggleElement = function (elem, target) {
42 var $elem = $(elem);
43 var $target = $(target);
44
45 if ($target.is(':visible') || $target.length === 0) {
46 $target.hide();
47 $elem.html($elem.data('toggleOn'))
48 } else {
49 $target.show();
50 $elem.html($elem.data('toggleOff'))
51 }
52
53 return false
54 }
55
56 var marginExpVal = '300' // needs a sync with `.right-sidebar.right-sidebar-expanded` value
57 var marginColVal = '40' // needs a sync with `.right-sidebar.right-sidebar-collapsed` value
58
59 var marginExpanded = {'margin': '0 {0}px 0 0'.format(marginExpVal)};
60 var marginCollapsed = {'margin': '0 {0}px 0 0'.format(marginColVal)};
61
62 var updateStickyHeader = function () {
63 if (window.updateSticky !== undefined) {
64 // potentially our comments change the active window size, so we
65 // notify sticky elements
66 updateSticky()
67 }
68 }
69
70 var expandSidebar = function () {
71 var $sideBar = $('.right-sidebar');
72 $('.outerwrapper').css(marginExpanded);
73 $('.sidebar-toggle a').html('<i class="icon-right" style="margin-right: -10px"></i><i class="icon-right"></i>');
74 $('.right-sidebar-collapsed-state').hide();
75 $('.right-sidebar-expanded-state').show();
76 $('.branding').addClass('display-none');
77 $sideBar.addClass('right-sidebar-expanded')
78 $sideBar.removeClass('right-sidebar-collapsed')
79 }
80
81 var collapseSidebar = function () {
82 var $sideBar = $('.right-sidebar');
83 $('.outerwrapper').css(marginCollapsed);
84 $('.sidebar-toggle a').html('<i class="icon-left" style="margin-right: -10px"></i><i class="icon-left"></i>');
85 $('.right-sidebar-collapsed-state').show();
86 $('.right-sidebar-expanded-state').hide();
87 $('.branding').removeClass('display-none');
88 $sideBar.removeClass('right-sidebar-expanded')
89 $sideBar.addClass('right-sidebar-collapsed')
90 }
91
92 window.toggleSidebar = function () {
93 var $sideBar = $('.right-sidebar');
94
95 if ($sideBar.hasClass('right-sidebar-expanded')) {
96 // expanded -> collapsed transition
97 collapseSidebar();
98 var sidebarState = 'collapsed';
99
100 } else {
101 // collapsed -> expanded
102 expandSidebar();
103 var sidebarState = 'expanded';
104 }
105
106 // update our other sticky header in same context
107 updateStickyHeader();
108 storeUserSessionAttr('rc_user_session_attr.sidebarState', sidebarState);
109 }
@@ -98,10 +98,13 b' ReviewersController = function () {'
98 98 var self = this;
99 99 this.$reviewRulesContainer = $('#review_rules');
100 100 this.$rulesList = this.$reviewRulesContainer.find('.pr-reviewer-rules');
101 this.$userRule = $('.pr-user-rule-container');
101 102 this.forbidReviewUsers = undefined;
102 103 this.$reviewMembers = $('#review_members');
103 104 this.currentRequest = null;
104 105 this.diffData = null;
106 this.enabledRules = [];
107
105 108 //dummy handler, we might register our own later
106 109 this.diffDataHandler = function(data){};
107 110
@@ -116,14 +119,17 b' ReviewersController = function () {'
116 119
117 120 this.hideReviewRules = function () {
118 121 self.$reviewRulesContainer.hide();
122 $(self.$userRule.selector).hide();
119 123 };
120 124
121 125 this.showReviewRules = function () {
122 126 self.$reviewRulesContainer.show();
127 $(self.$userRule.selector).show();
123 128 };
124 129
125 130 this.addRule = function (ruleText) {
126 131 self.showReviewRules();
132 self.enabledRules.push(ruleText);
127 133 return '<div>- {0}</div>'.format(ruleText)
128 134 };
129 135
@@ -179,6 +185,7 b' ReviewersController = function () {'
179 185 _gettext('Reviewers picked from source code changes.'))
180 186 )
181 187 }
188
182 189 if (data.rules.forbid_adding_reviewers) {
183 190 $('#add_reviewer_input').remove();
184 191 self.$rulesList.append(
@@ -186,6 +193,7 b' ReviewersController = function () {'
186 193 _gettext('Adding new reviewers is forbidden.'))
187 194 )
188 195 }
196
189 197 if (data.rules.forbid_author_to_review) {
190 198 self.forbidReviewUsers.push(data.rules_data.pr_author);
191 199 self.$rulesList.append(
@@ -193,6 +201,7 b' ReviewersController = function () {'
193 201 _gettext('Author is not allowed to be a reviewer.'))
194 202 )
195 203 }
204
196 205 if (data.rules.forbid_commit_author_to_review) {
197 206
198 207 if (data.rules_data.forbidden_users) {
@@ -208,6 +217,12 b' ReviewersController = function () {'
208 217 )
209 218 }
210 219
220 // we don't have any rules set, so we inform users about it
221 if (self.enabledRules.length === 0) {
222 self.addRule(
223 _gettext('No review rules set.'))
224 }
225
211 226 return self.forbidReviewUsers
212 227 };
213 228
@@ -264,8 +279,11 b' ReviewersController = function () {'
264 279 $('#user').show(); // show user autocomplete after load
265 280
266 281 var commitElements = data["diff_info"]['commits'];
282
267 283 if (commitElements.length === 0) {
268 prButtonLock(true, _gettext('no commits'), 'all');
284 var noCommitsMsg = '<span class="alert-text-warning">{0}</span>'.format(
285 _gettext('There are no commits to merge.'));
286 prButtonLock(true, noCommitsMsg, 'all');
269 287
270 288 } else {
271 289 // un-lock PR button, so we cannot send PR before it's calculated
@@ -309,7 +327,6 b' ReviewersController = function () {'
309 327 };
310 328
311 329 this.addReviewMember = function (reviewer_obj, reasons, mandatory) {
312 var members = self.$reviewMembers.get(0);
313 330 var id = reviewer_obj.user_id;
314 331 var username = reviewer_obj.username;
315 332
@@ -318,10 +335,10 b' ReviewersController = function () {'
318 335
319 336 // register IDS to check if we don't have this ID already in
320 337 var currentIds = [];
321 var _els = self.$reviewMembers.find('li').toArray();
322 for (el in _els) {
323 currentIds.push(_els[el].id)
324 }
338
339 $.each(self.$reviewMembers.find('.reviewer_entry'), function (index, value) {
340 currentIds.push($(value).data('reviewerUserId'))
341 })
325 342
326 343 var userAllowedReview = function (userId) {
327 344 var allowed = true;
@@ -339,20 +356,23 b' ReviewersController = function () {'
339 356 alert(_gettext('User `{0}` not allowed to be a reviewer').format(username));
340 357 } else {
341 358 // only add if it's not there
342 var alreadyReviewer = currentIds.indexOf('reviewer_' + id) != -1;
359 var alreadyReviewer = currentIds.indexOf(id) != -1;
343 360
344 361 if (alreadyReviewer) {
345 362 alert(_gettext('User `{0}` already in reviewers').format(username));
346 363 } else {
347 members.innerHTML += renderTemplate('reviewMemberEntry', {
364 var reviewerEntry = renderTemplate('reviewMemberEntry', {
348 365 'member': reviewer_obj,
349 366 'mandatory': mandatory,
367 'reasons': reasons,
350 368 'allowed_to_update': true,
351 369 'review_status': 'not_reviewed',
352 370 'review_status_label': _gettext('Not Reviewed'),
353 'reasons': reasons,
354 'create': true
355 });
371 'user_group': reviewer_obj.user_group,
372 'create': true,
373 'rule_show': true,
374 })
375 $(self.$reviewMembers.selector).append(reviewerEntry);
356 376 tooltipActivate();
357 377 }
358 378 }
@@ -476,7 +496,7 b' var ReviewerAutoComplete = function(inpu'
476 496 };
477 497
478 498
479 VersionController = function () {
499 window.VersionController = function () {
480 500 var self = this;
481 501 this.$verSource = $('input[name=ver_source]');
482 502 this.$verTarget = $('input[name=ver_target]');
@@ -596,25 +616,10 b' VersionController = function () {'
596 616 return false
597 617 };
598 618
599 this.toggleElement = function (elem, target) {
600 var $elem = $(elem);
601 var $target = $(target);
602
603 if ($target.is(':visible')) {
604 $target.hide();
605 $elem.html($elem.data('toggleOn'))
606 } else {
607 $target.show();
608 $elem.html($elem.data('toggleOff'))
609 }
610
611 return false
612 }
613
614 619 };
615 620
616 621
617 UpdatePrController = function () {
622 window.UpdatePrController = function () {
618 623 var self = this;
619 624 this.$updateCommits = $('#update_commits');
620 625 this.$updateCommitsSwitcher = $('#update_commits_switcher');
@@ -656,4 +661,230 b' UpdatePrController = function () {'
656 661 templateContext.repo_name,
657 662 templateContext.pull_request_data.pull_request_id, force);
658 663 };
659 }; No newline at end of file
664 };
665
666 /**
667 * Reviewer display panel
668 */
669 window.ReviewersPanel = {
670 editButton: null,
671 closeButton: null,
672 addButton: null,
673 removeButtons: null,
674 reviewRules: null,
675 setReviewers: null,
676
677 setSelectors: function () {
678 var self = this;
679 self.editButton = $('#open_edit_reviewers');
680 self.closeButton =$('#close_edit_reviewers');
681 self.addButton = $('#add_reviewer');
682 self.removeButtons = $('.reviewer_member_remove,.reviewer_member_mandatory_remove');
683 },
684
685 init: function (reviewRules, setReviewers) {
686 var self = this;
687 self.setSelectors();
688
689 this.reviewRules = reviewRules;
690 this.setReviewers = setReviewers;
691
692 this.editButton.on('click', function (e) {
693 self.edit();
694 });
695 this.closeButton.on('click', function (e) {
696 self.close();
697 self.renderReviewers();
698 });
699
700 self.renderReviewers();
701
702 },
703
704 renderReviewers: function () {
705
706 $('#review_members').html('')
707 $.each(this.setReviewers.reviewers, function (key, val) {
708 var member = val;
709
710 var entry = renderTemplate('reviewMemberEntry', {
711 'member': member,
712 'mandatory': member.mandatory,
713 'reasons': member.reasons,
714 'allowed_to_update': member.allowed_to_update,
715 'review_status': member.review_status,
716 'review_status_label': member.review_status_label,
717 'user_group': member.user_group,
718 'create': false
719 });
720
721 $('#review_members').append(entry)
722 });
723 tooltipActivate();
724
725 },
726
727 edit: function (event) {
728 this.editButton.hide();
729 this.closeButton.show();
730 this.addButton.show();
731 $(this.removeButtons.selector).css('visibility', 'visible');
732 // review rules
733 reviewersController.loadReviewRules(this.reviewRules);
734 },
735
736 close: function (event) {
737 this.editButton.show();
738 this.closeButton.hide();
739 this.addButton.hide();
740 $(this.removeButtons.selector).css('visibility', 'hidden');
741 // hide review rules
742 reviewersController.hideReviewRules()
743 }
744 };
745
746
747 /**
748 * OnLine presence using channelstream
749 */
750 window.ReviewerPresenceController = function (channel) {
751 var self = this;
752 this.channel = channel;
753 this.users = {};
754
755 this.storeUsers = function (users) {
756 self.users = {}
757 $.each(users, function (index, value) {
758 var userId = value.state.id;
759 self.users[userId] = value.state;
760 })
761 }
762
763 this.render = function () {
764 $.each($('.reviewer_entry'), function (index, value) {
765 var userData = $(value).data();
766 if (self.users[userData.reviewerUserId] !== undefined) {
767 $(value).find('.presence-state').show();
768 } else {
769 $(value).find('.presence-state').hide();
770 }
771 })
772 };
773
774 this.handlePresence = function (data) {
775 if (data.type == 'presence' && data.channel === self.channel) {
776 this.storeUsers(data.users);
777 this.render()
778 }
779 };
780
781 this.handleChannelUpdate = function (data) {
782 if (data.channel === this.channel) {
783 this.storeUsers(data.state.users);
784 this.render()
785 }
786
787 };
788
789 /* subscribe to the current presence */
790 $.Topic('/connection_controller/presence').subscribe(this.handlePresence.bind(this));
791 /* subscribe to updates e.g connect/disconnect */
792 $.Topic('/connection_controller/channel_update').subscribe(this.handleChannelUpdate.bind(this));
793
794 };
795
796 window.refreshComments = function (version) {
797 version = version || templateContext.pull_request_data.pull_request_version || '';
798
799 // Pull request case
800 if (templateContext.pull_request_data.pull_request_id !== null) {
801 var params = {
802 'pull_request_id': templateContext.pull_request_data.pull_request_id,
803 'repo_name': templateContext.repo_name,
804 'version': version,
805 };
806 var loadUrl = pyroutes.url('pullrequest_comments', params);
807 } // commit case
808 else {
809 return
810 }
811
812 var currentIDs = []
813 $.each($('.comment'), function (idx, element) {
814 currentIDs.push($(element).data('commentId'));
815 });
816 var data = {"comments[]": currentIDs};
817
818 var $targetElem = $('.comments-content-table');
819 $targetElem.css('opacity', 0.3);
820 $targetElem.load(
821 loadUrl, data, function (responseText, textStatus, jqXHR) {
822 if (jqXHR.status !== 200) {
823 return false;
824 }
825 var $counterElem = $('#comments-count');
826 var newCount = $(responseText).data('counter');
827 if (newCount !== undefined) {
828 var callback = function () {
829 $counterElem.animate({'opacity': 1.00}, 200)
830 $counterElem.html(newCount);
831 };
832 $counterElem.animate({'opacity': 0.15}, 200, callback);
833 }
834
835 $targetElem.css('opacity', 1);
836 tooltipActivate();
837 }
838 );
839 }
840
841 window.refreshTODOs = function (version) {
842 version = version || templateContext.pull_request_data.pull_request_version || '';
843 // Pull request case
844 if (templateContext.pull_request_data.pull_request_id !== null) {
845 var params = {
846 'pull_request_id': templateContext.pull_request_data.pull_request_id,
847 'repo_name': templateContext.repo_name,
848 'version': version,
849 };
850 var loadUrl = pyroutes.url('pullrequest_comments', params);
851 } // commit case
852 else {
853 return
854 }
855
856 var currentIDs = []
857 $.each($('.comment'), function (idx, element) {
858 currentIDs.push($(element).data('commentId'));
859 });
860
861 var data = {"comments[]": currentIDs};
862 var $targetElem = $('.todos-content-table');
863 $targetElem.css('opacity', 0.3);
864 $targetElem.load(
865 loadUrl, data, function (responseText, textStatus, jqXHR) {
866 if (jqXHR.status !== 200) {
867 return false;
868 }
869 var $counterElem = $('#todos-count')
870 var newCount = $(responseText).data('counter');
871 if (newCount !== undefined) {
872 var callback = function () {
873 $counterElem.animate({'opacity': 1.00}, 200)
874 $counterElem.html(newCount);
875 };
876 $counterElem.animate({'opacity': 0.15}, 200, callback);
877 }
878
879 $targetElem.css('opacity', 1);
880 tooltipActivate();
881 }
882 );
883 }
884
885 window.refreshAllComments = function (version) {
886 version = version || templateContext.pull_request_data.pull_request_version || '';
887
888 refreshComments(version);
889 refreshTODOs(version);
890 };
@@ -18,6 +18,7 b''
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 import io
21 import math
21 22 import re
22 23 import os
23 24 import datetime
@@ -196,6 +197,72 b' def write_metadata_if_needed(event):'
196 197 pass
197 198
198 199
200 def write_usage_data(event):
201 import rhodecode
202 from rhodecode.lib import system_info
203 from rhodecode.lib import ext_json
204
205 settings = event.app.registry.settings
206 instance_tag = settings.get('metadata.write_usage_tag')
207 if not settings.get('metadata.write_usage'):
208 return
209
210 def get_update_age(dest_file):
211 now = datetime.datetime.utcnow()
212
213 with open(dest_file, 'rb') as f:
214 data = ext_json.json.loads(f.read())
215 if 'created_on' in data:
216 update_date = parse(data['created_on'])
217 diff = now - update_date
218 return math.ceil(diff.total_seconds() / 60.0)
219
220 return 0
221
222 utc_date = datetime.datetime.utcnow()
223 hour_quarter = int(math.ceil((utc_date.hour + utc_date.minute/60.0) / 6.))
224 fname = '.rc_usage_{date.year}{date.month:02d}{date.day:02d}_{hour}.json'.format(
225 date=utc_date, hour=hour_quarter)
226 ini_loc = os.path.dirname(rhodecode.CONFIG.get('__file__'))
227
228 usage_dir = os.path.join(ini_loc, '.rcusage')
229 if not os.path.isdir(usage_dir):
230 os.makedirs(usage_dir)
231 usage_metadata_destination = os.path.join(usage_dir, fname)
232
233 try:
234 age_in_min = get_update_age(usage_metadata_destination)
235 except Exception:
236 age_in_min = 0
237
238 # write every 6th hour
239 if age_in_min and age_in_min < 60 * 6:
240 log.debug('Usage file created %s minutes ago, skipping (threashold: %s)...',
241 age_in_min, 60 * 6)
242 return
243
244 def write(dest_file):
245 configuration = system_info.SysInfo(system_info.rhodecode_config)()['value']
246 license_token = configuration['config']['license_token']
247
248 metadata = dict(
249 desc='Usage data',
250 instance_tag=instance_tag,
251 license_token=license_token,
252 created_on=datetime.datetime.utcnow().isoformat(),
253 usage=system_info.SysInfo(system_info.usage_info)()['value'],
254 )
255
256 with open(dest_file, 'wb') as f:
257 f.write(ext_json.json.dumps(metadata, indent=2, sort_keys=True))
258
259 try:
260 log.debug('Writing usage file at: %s', usage_metadata_destination)
261 write(usage_metadata_destination)
262 except Exception:
263 pass
264
265
199 266 def write_js_routes_if_enabled(event):
200 267 registry = event.app.registry
201 268
@@ -38,10 +38,12 b''
38 38 <div class="main">
39 39 ${next.main()}
40 40 </div>
41
41 42 </div>
42 43 <!-- END CONTENT -->
43 44
44 45 </div>
46
45 47 <!-- FOOTER -->
46 48 <div id="footer">
47 49 <div id="footer-inner" class="title wrapper">
@@ -699,9 +701,6 b''
699 701 notice_messages, notice_level = c.rhodecode_user.get_notice_messages()
700 702 notice_display = 'none' if len(notice_messages) == 0 else ''
701 703 %>
702 <style>
703
704 </style>
705 704
706 705 <ul id="quick" class="main_nav navigation horizontal-list">
707 706 ## notice box for important system messages
@@ -1200,6 +1199,7 b''
1200 1199 ('g p', 'Goto pull requests page'),
1201 1200 ('g o', 'Goto repository settings'),
1202 1201 ('g O', 'Goto repository access permissions settings'),
1202 ('t s', 'Toggle sidebar on some pages'),
1203 1203 ]
1204 1204 %>
1205 1205 %for key, desc in elems:
@@ -1219,3 +1219,36 b''
1219 1219 </div><!-- /.modal-content -->
1220 1220 </div><!-- /.modal-dialog -->
1221 1221 </div><!-- /.modal -->
1222
1223
1224 <script type="text/javascript">
1225 (function () {
1226 "use sctrict";
1227
1228 var $sideBar = $('.right-sidebar');
1229 var expanded = $sideBar.hasClass('right-sidebar-expanded');
1230 var sidebarState = templateContext.session_attrs.sidebarState;
1231 var sidebarEnabled = $('aside.right-sidebar').get(0);
1232
1233 if (sidebarState === 'expanded') {
1234 expanded = true
1235 } else if (sidebarState === 'collapsed') {
1236 expanded = false
1237 }
1238 if (sidebarEnabled) {
1239 // show sidebar since it's hidden on load
1240 $('.right-sidebar').show();
1241
1242 // init based on set initial class, or if defined user session attrs
1243 if (expanded) {
1244 window.expandSidebar();
1245 window.updateStickyHeader();
1246
1247 } else {
1248 window.collapseSidebar();
1249 window.updateStickyHeader();
1250 }
1251 }
1252 })()
1253
1254 </script>
@@ -4,6 +4,8 b''
4 4 <%namespace name="base" file="/base/base.mako"/>
5 5 <%namespace name="diff_block" file="/changeset/diff_block.mako"/>
6 6 <%namespace name="file_base" file="/files/base.mako"/>
7 <%namespace name="sidebar" file="/base/sidebar.mako"/>
8
7 9
8 10 <%def name="title()">
9 11 ${_('{} Commit').format(c.repo_name)} - ${h.show_id(c.commit)}
@@ -100,22 +102,6 b''
100 102 % endif
101 103 </div>
102 104
103 %if c.statuses:
104 <div class="tag status-tag-${c.statuses[0]} pull-right">
105 <i class="icon-circle review-status-${c.statuses[0]}"></i>
106 <div class="pull-right">${h.commit_status_lbl(c.statuses[0])}</div>
107 </div>
108 %endif
109
110 </div>
111
112 </div>
113 </div>
114
115 <div class="fieldset collapsable-content" data-toggle="summary-details" style="display: none;">
116 <div class="left-label-summary">
117 <p>${_('Commit navigation')}:</p>
118 <div class="right-label-summary">
119 105 <span id="parent_link" class="tag tagtag">
120 106 <a href="#parentCommit" title="${_('Parent Commit')}"><i class="icon-left icon-no-margin"></i>${_('parent')}</a>
121 107 </span>
@@ -123,7 +109,9 b''
123 109 <span id="child_link" class="tag tagtag">
124 110 <a href="#childCommit" title="${_('Child Commit')}">${_('child')}<i class="icon-right icon-no-margin"></i></a>
125 111 </span>
112
126 113 </div>
114
127 115 </div>
128 116 </div>
129 117
@@ -160,7 +148,9 b''
160 148 <%namespace name="cbdiffs" file="/codeblocks/diffs.mako"/>
161 149 ${cbdiffs.render_diffset_menu(c.changes[c.commit.raw_id], commit=c.commit)}
162 150 ${cbdiffs.render_diffset(
163 c.changes[c.commit.raw_id], commit=c.commit, use_comments=True,inline_comments=c.inline_comments )}
151 c.changes[c.commit.raw_id], commit=c.commit, use_comments=True,
152 inline_comments=c.inline_comments,
153 show_todos=False)}
164 154 </div>
165 155
166 156 ## template for inline comment form
@@ -169,7 +159,7 b''
169 159 ## comments heading with count
170 160 <div class="comments-heading">
171 161 <i class="icon-comment"></i>
172 ${_('Comments')} ${len(c.comments)}
162 ${_('General Comments')} ${len(c.comments)}
173 163 </div>
174 164
175 165 ## render comments
@@ -180,12 +170,130 b''
180 170 h.commit_status(c.rhodecode_db_repo, c.commit.raw_id))}
181 171 </div>
182 172
173 ### NAV SIDEBAR
174 <aside class="right-sidebar right-sidebar-expanded" id="commit-nav-sticky" style="display: none">
175 <div class="sidenav navbar__inner" >
176 ## TOGGLE
177 <div class="sidebar-toggle" onclick="toggleSidebar(); return false">
178 <a href="#toggleSidebar" class="grey-link-action">
179
180 </a>
181 </div>
182
183 ## CONTENT
184 <div class="sidebar-content">
185
186 ## RULES SUMMARY/RULES
187 <div class="sidebar-element clear-both">
188 <% vote_title = _ungettext(
189 'Status calculated based on votes from {} reviewer',
190 'Status calculated based on votes from {} reviewers', len(c.allowed_reviewers)).format(len(c.allowed_reviewers))
191 %>
192
193 <div class="tooltip right-sidebar-collapsed-state" style="display: none" onclick="toggleSidebar(); return false" title="${vote_title}">
194 <i class="icon-circle review-status-${c.commit_review_status}"></i>
195 ${len(c.allowed_reviewers)}
196 </div>
197 </div>
198
199 ## REVIEWERS
200 <div class="right-sidebar-expanded-state pr-details-title">
201 <span class="tooltip sidebar-heading" title="${vote_title}">
202 <i class="icon-circle review-status-${c.commit_review_status}"></i>
203 ${_('Reviewers')}
204 </span>
205 </div>
206
207 <div id="reviewers" class="right-sidebar-expanded-state pr-details-content reviewers">
208
209 <table id="review_members" class="group_members">
210 ## This content is loaded via JS and ReviewersPanel
211 </table>
212
213 </div>
214
215 ## TODOs
216 <div class="sidebar-element clear-both">
217 <div class="tooltip right-sidebar-collapsed-state" style="display: none" onclick="toggleSidebar(); return false" title="TODOs">
218 <i class="icon-flag-filled"></i>
219 <span id="todos-count">${len(c.unresolved_comments)}</span>
220 </div>
221
222 <div class="right-sidebar-expanded-state pr-details-title">
223 ## Only show unresolved, that is only what matters
224 <span class="sidebar-heading noselect" onclick="refreshTODOs(); return false">
225 <i class="icon-flag-filled"></i>
226 TODOs
227 </span>
228
229 % if c.resolved_comments:
230 <span class="block-right action_button last-item noselect" onclick="$('.unresolved-todo-text').toggle(); return toggleElement(this, '.resolved-todo');" data-toggle-on="Show resolved" data-toggle-off="Hide resolved">Show resolved</span>
231 % else:
232 <span class="block-right last-item noselect">Show resolved</span>
233 % endif
234
235 </div>
236
237 <div class="right-sidebar-expanded-state pr-details-content">
238 % if c.unresolved_comments + c.resolved_comments:
239 ${sidebar.comments_table(c.unresolved_comments + c.resolved_comments, len(c.unresolved_comments), todo_comments=True, is_pr=False)}
240 % else:
241 <table>
242 <tr>
243 <td>
244 ${_('No TODOs yet')}
245 </td>
246 </tr>
247 </table>
248 % endif
249 </div>
250 </div>
251
252 ## COMMENTS
253 <div class="sidebar-element clear-both">
254 <div class="tooltip right-sidebar-collapsed-state" style="display: none" onclick="toggleSidebar(); return false" title="${_('Comments')}">
255 <i class="icon-comment" style="color: #949494"></i>
256 <span id="comments-count">${len(c.inline_comments_flat+c.comments)}</span>
257 <span class="display-none" id="general-comments-count">${len(c.comments)}</span>
258 <span class="display-none" id="inline-comments-count">${len(c.inline_comments_flat)}</span>
259 </div>
260
261 <div class="right-sidebar-expanded-state pr-details-title">
262 <span class="sidebar-heading noselect" onclick="refreshComments(); return false">
263 <i class="icon-comment" style="color: #949494"></i>
264 ${_('Comments')}
265 </span>
266
267 </div>
268
269 <div class="right-sidebar-expanded-state pr-details-content">
270 % if c.inline_comments_flat + c.comments:
271 ${sidebar.comments_table(c.inline_comments_flat + c.comments, len(c.inline_comments_flat+c.comments), is_pr=False)}
272 % else:
273 <table>
274 <tr>
275 <td>
276 ${_('No Comments yet')}
277 </td>
278 </tr>
279 </table>
280 % endif
281 </div>
282
283 </div>
284
285 </div>
286
287 </div>
288 </aside>
289
183 290 ## FORM FOR MAKING JS ACTION AS CHANGESET COMMENTS
184 291 <script type="text/javascript">
292 window.setReviewersData = ${c.commit_set_reviewers_data_json | n};
185 293
186 294 $(document).ready(function() {
295 var boxmax = parseInt($('#trimmed_message_box').css('max-height'), 10);
187 296
188 var boxmax = parseInt($('#trimmed_message_box').css('max-height'), 10);
189 297 if($('#trimmed_message_box').height() === boxmax){
190 298 $('#message_expand').show();
191 299 }
@@ -225,9 +333,11 b''
225 333 }
226 334 if(data.results.length === 1){
227 335 var commit = data.results[0];
228 window.location = pyroutes.url('repo_commit', {'repo_name': '${c.repo_name}','commit_id': commit.raw_id});
229 }
230 else if(data.results.length === 2){
336 window.location = pyroutes.url('repo_commit', {
337 'repo_name': '${c.repo_name}',
338 'commit_id': commit.raw_id
339 });
340 } else if (data.results.length === 2) {
231 341 $('#child_link').addClass('disabled');
232 342 $('#child_link').addClass('double');
233 343
@@ -236,13 +346,19 b''
236 346 .replace('__branch__', data.results[0].branch)
237 347 .replace('__rev__','r{0}:{1}'.format(data.results[0].revision, data.results[0].raw_id.substr(0,6)))
238 348 .replace('__title__', data.results[0].message)
239 .replace('__url__', pyroutes.url('repo_commit', {'repo_name': '${c.repo_name}','commit_id': data.results[0].raw_id}));
349 .replace('__url__', pyroutes.url('repo_commit', {
350 'repo_name': '${c.repo_name}',
351 'commit_id': data.results[0].raw_id
352 }));
240 353 _html +=' | ';
241 354 _html +='<a title="__title__" href="__url__"><span class="tag branchtag"><i class="icon-code-fork"></i>__branch__</span> __rev__</a> '
242 355 .replace('__branch__', data.results[1].branch)
243 356 .replace('__rev__','r{0}:{1}'.format(data.results[1].revision, data.results[1].raw_id.substr(0,6)))
244 357 .replace('__title__', data.results[1].message)
245 .replace('__url__', pyroutes.url('repo_commit', {'repo_name': '${c.repo_name}','commit_id': data.results[1].raw_id}));
358 .replace('__url__', pyroutes.url('repo_commit', {
359 'repo_name': '${c.repo_name}',
360 'commit_id': data.results[1].raw_id
361 }));
246 362 $('#child_link').html(_html);
247 363 }
248 364 }
@@ -264,9 +380,11 b''
264 380 }
265 381 if(data.results.length === 1){
266 382 var commit = data.results[0];
267 window.location = pyroutes.url('repo_commit', {'repo_name': '${c.repo_name}','commit_id': commit.raw_id});
268 }
269 else if(data.results.length === 2){
383 window.location = pyroutes.url('repo_commit', {
384 'repo_name': '${c.repo_name}',
385 'commit_id': commit.raw_id
386 });
387 } else if (data.results.length === 2) {
270 388 $('#parent_link').addClass('disabled');
271 389 $('#parent_link').addClass('double');
272 390
@@ -275,13 +393,19 b''
275 393 .replace('__branch__', data.results[0].branch)
276 394 .replace('__rev__','r{0}:{1}'.format(data.results[0].revision, data.results[0].raw_id.substr(0,6)))
277 395 .replace('__title__', data.results[0].message)
278 .replace('__url__', pyroutes.url('repo_commit', {'repo_name': '${c.repo_name}','commit_id': data.results[0].raw_id}));
396 .replace('__url__', pyroutes.url('repo_commit', {
397 'repo_name': '${c.repo_name}',
398 'commit_id': data.results[0].raw_id
399 }));
279 400 _html +=' | ';
280 401 _html +='<a title="__title__" href="__url__"><span class="tag branchtag"><i class="icon-code-fork"></i>__branch__</span> __rev__</a>'
281 402 .replace('__branch__', data.results[1].branch)
282 403 .replace('__rev__','r{0}:{1}'.format(data.results[1].revision, data.results[1].raw_id.substr(0,6)))
283 404 .replace('__title__', data.results[1].message)
284 .replace('__url__', pyroutes.url('repo_commit', {'repo_name': '${c.repo_name}','commit_id': data.results[1].raw_id}));
405 .replace('__url__', pyroutes.url('repo_commit', {
406 'repo_name': '${c.repo_name}',
407 'commit_id': data.results[1].raw_id
408 }));
285 409 $('#parent_link').html(_html);
286 410 }
287 411 }
@@ -296,6 +420,11 b''
296 420 e.preventDefault();
297 421 });
298 422
423 ReviewersPanel.init(null, setReviewersData);
424
425 var channel = '${c.commit_broadcast_channel}';
426 new ReviewerPresenceController(channel)
427
299 428 })
300 429 </script>
301 430
@@ -10,12 +10,18 b''
10 10
11 11 <%namespace name="base" file="/base/base.mako"/>
12 12 <%def name="comment_block(comment, inline=False, active_pattern_entries=None)">
13 <% pr_index_ver = comment.get_index_version(getattr(c, 'versions', [])) %>
13
14 <%
15 from rhodecode.model.comment import CommentsModel
16 comment_model = CommentsModel()
17 %>
18 <% comment_ver = comment.get_index_version(getattr(c, 'versions', [])) %>
14 19 <% latest_ver = len(getattr(c, 'versions', [])) %>
20
15 21 % if inline:
16 <% outdated_at_ver = comment.outdated_at_version(getattr(c, 'at_version_num', None)) %>
22 <% outdated_at_ver = comment.outdated_at_version(c.at_version_num) %>
17 23 % else:
18 <% outdated_at_ver = comment.older_than_version(getattr(c, 'at_version_num', None)) %>
24 <% outdated_at_ver = comment.older_than_version(c.at_version_num) %>
19 25 % endif
20 26
21 27 <div class="comment
@@ -70,9 +76,9 b''
70 76 status_change_title = 'Status of review for commit {}'.format(h.short_id(comment.commit_id))
71 77 %>
72 78
73 <i class="icon-circle review-status-${comment.status_change[0].status}"></i>
79 <i class="icon-circle review-status-${comment.review_status}"></i>
74 80 <div class="changeset-status-lbl tooltip" title="${status_change_title}">
75 ${comment.status_change[0].status_lbl}
81 ${comment.review_status_lbl}
76 82 </div>
77 83 % else:
78 84 <div>
@@ -153,69 +159,90 b''
153 159 </div>
154 160 %endif
155 161
156 <a class="permalink" href="#comment-${comment.comment_id}"> &para;</a>
157
158 162 <div class="comment-links-block">
159 163
160 164 % if inline:
161 165 <a class="pr-version-inline" href="${request.current_route_path(_query=dict(version=comment.pull_request_version_id), _anchor='comment-{}'.format(comment.comment_id))}">
162 166 % if outdated_at_ver:
163 <code class="tooltip pr-version-num" title="${_('Outdated comment from pull request version v{0}, latest v{1}').format(pr_index_ver, latest_ver)}">
164 outdated ${'v{}'.format(pr_index_ver)} |
165 </code>
166 % elif pr_index_ver:
167 <code class="tooltip pr-version-num" title="${_('Comment from pull request version v{0}, latest v{1}').format(pr_index_ver, latest_ver)}">
168 ${'v{}'.format(pr_index_ver)} |
169 </code>
167 <code class="tooltip pr-version-num" title="${_('Outdated comment from pull request version v{0}, latest v{1}').format(comment_ver, latest_ver)}">outdated ${'v{}'.format(comment_ver)}</code>
168 <code class="action-divider">|</code>
169 % elif comment_ver:
170 <code class="tooltip pr-version-num" title="${_('Comment from pull request version v{0}, latest v{1}').format(comment_ver, latest_ver)}">${'v{}'.format(comment_ver)}</code>
171 <code class="action-divider">|</code>
170 172 % endif
171 173 </a>
172 174 % else:
173 % if pr_index_ver:
175 % if comment_ver:
174 176
175 177 % if comment.outdated:
176 178 <a class="pr-version"
177 179 href="?version=${comment.pull_request_version_id}#comment-${comment.comment_id}"
178 180 >
179 ${_('Outdated comment from pull request version v{0}, latest v{1}').format(pr_index_ver, latest_ver)}
180 </a> |
181 ${_('Outdated comment from pull request version v{0}, latest v{1}').format(comment_ver, latest_ver)}
182 </a>
183 <code class="action-divider">|</code>
181 184 % else:
182 185 <a class="tooltip pr-version"
183 title="${_('Comment from pull request version v{0}, latest v{1}').format(pr_index_ver, latest_ver)}"
186 title="${_('Comment from pull request version v{0}, latest v{1}').format(comment_ver, latest_ver)}"
184 187 href="${h.route_path('pullrequest_show',repo_name=comment.pull_request.target_repo.repo_name,pull_request_id=comment.pull_request.pull_request_id, version=comment.pull_request_version_id)}"
185 188 >
186 <code class="pr-version-num">
187 ${'v{}'.format(pr_index_ver)}
188 </code>
189 </a> |
189 <code class="pr-version-num">${'v{}'.format(comment_ver)}</code>
190 </a>
191 <code class="action-divider">|</code>
190 192 % endif
191 193
192 194 % endif
193 195 % endif
194 196
197 <details class="details-reset details-inline-block">
198 <summary class="noselect"><i class="icon-options cursor-pointer"></i></summary>
199 <details-menu class="details-dropdown">
200
201 <div class="dropdown-item">
202 ${_('Comment')} #${comment.comment_id}
203 <span class="pull-right icon-clipboard clipboard-action" data-clipboard-text="${comment_model.get_url(comment,request, permalink=True, anchor='comment-{}'.format(comment.comment_id))}" title="${_('Copy permalink')}"></span>
204 </div>
205
195 206 ## show delete comment if it's not a PR (regular comments) or it's PR that is not closed
196 207 ## only super-admin, repo admin OR comment owner can delete, also hide delete if currently viewed comment is outdated
197 208 %if not outdated_at_ver and (not comment.pull_request or (comment.pull_request and not comment.pull_request.is_closed())):
198 209 ## permissions to delete
199 210 %if comment.immutable is False and (c.is_super_admin or h.HasRepoPermissionAny('repository.admin')(c.repo_name) or comment.author.user_id == c.rhodecode_user.user_id):
200 <a onclick="return Rhodecode.comments.editComment(this);"
201 class="edit-comment">${_('Edit')}</a>
202 | <a onclick="return Rhodecode.comments.deleteComment(this);"
203 class="delete-comment">${_('Delete')}</a>
211 <div class="dropdown-divider"></div>
212 <div class="dropdown-item">
213 <a onclick="return Rhodecode.comments.editComment(this);" class="btn btn-link btn-sm edit-comment">${_('Edit')}</a>
214 </div>
215 <div class="dropdown-item">
216 <a onclick="return Rhodecode.comments.deleteComment(this);" class="btn btn-link btn-sm btn-danger delete-comment">${_('Delete')}</a>
217 </div>
204 218 %else:
219 <div class="dropdown-divider"></div>
220 <div class="dropdown-item">
205 221 <a class="tooltip edit-comment link-disabled" disabled="disabled" title="${_('Action unavailable')}">${_('Edit')}</a>
206 | <a class="tooltip edit-comment link-disabled" disabled="disabled" title="${_('Action unavailable')}">${_('Delete')}</a>
222 </div>
223 <div class="dropdown-item">
224 <a class="tooltip edit-comment link-disabled" disabled="disabled" title="${_('Action unavailable')}">${_('Delete')}</a>
225 </div>
207 226 %endif
208 227 %else:
228 <div class="dropdown-divider"></div>
229 <div class="dropdown-item">
209 230 <a class="tooltip edit-comment link-disabled" disabled="disabled" title="${_('Action unavailable')}">${_('Edit')}</a>
210 | <a class="tooltip edit-comment link-disabled" disabled="disabled" title="${_('Action unavailable')}">${_('Delete')}</a>
231 </div>
232 <div class="dropdown-item">
233 <a class="tooltip edit-comment link-disabled" disabled="disabled" title="${_('Action unavailable')}">${_('Delete')}</a>
234 </div>
211 235 %endif
236 </details-menu>
237 </details>
212 238
239 <code class="action-divider">|</code>
213 240 % if outdated_at_ver:
214 | <a onclick="return Rhodecode.comments.prevOutdatedComment(this);" class="tooltip prev-comment" title="${_('Jump to the previous outdated comment')}"> <i class="icon-angle-left"></i> </a>
215 | <a onclick="return Rhodecode.comments.nextOutdatedComment(this);" class="tooltip next-comment" title="${_('Jump to the next outdated comment')}"> <i class="icon-angle-right"></i></a>
241 <a onclick="return Rhodecode.comments.prevOutdatedComment(this);" class="tooltip prev-comment" title="${_('Jump to the previous outdated comment')}"> <i class="icon-angle-left"></i> </a>
242 <a onclick="return Rhodecode.comments.nextOutdatedComment(this);" class="tooltip next-comment" title="${_('Jump to the next outdated comment')}"> <i class="icon-angle-right"></i></a>
216 243 % else:
217 | <a onclick="return Rhodecode.comments.prevComment(this);" class="tooltip prev-comment" title="${_('Jump to the previous comment')}"> <i class="icon-angle-left"></i></a>
218 | <a onclick="return Rhodecode.comments.nextComment(this);" class="tooltip next-comment" title="${_('Jump to the next comment')}"> <i class="icon-angle-right"></i></a>
244 <a onclick="return Rhodecode.comments.prevComment(this);" class="tooltip prev-comment" title="${_('Jump to the previous comment')}"> <i class="icon-angle-left"></i></a>
245 <a onclick="return Rhodecode.comments.nextComment(this);" class="tooltip next-comment" title="${_('Jump to the next comment')}"> <i class="icon-angle-right"></i></a>
219 246 % endif
220 247
221 248 </div>
@@ -102,6 +102,11 b''
102 102 <%namespace name="diff_block" file="/changeset/diff_block.mako"/>
103 103
104 104 %for commit in c.commit_ranges:
105 ## commit range header for each individual diff
106 <h3>
107 <a class="tooltip revision" title="${h.tooltip(commit.message)}" href="${h.route_path('repo_commit',repo_name=c.repo_name,commit_id=commit.raw_id)}">${('r%s:%s' % (commit.idx,h.short_id(commit.raw_id)))}</a>
108 </h3>
109
105 110 ${cbdiffs.render_diffset_menu(c.changes[commit.raw_id])}
106 111 ${cbdiffs.render_diffset(
107 112 diffset=c.changes[commit.raw_id],
@@ -61,6 +61,8 b" return '%s_%s_%i' % (h.md5_safe(commit+f"
61 61 diffset_container_id = h.md5(diffset.target_ref)
62 62 collapse_all = len(diffset.files) > collapse_when_files_over
63 63 active_pattern_entries = h.get_active_pattern_entries(getattr(c, 'repo_name', None))
64 from rhodecode.lib.diffs import NEW_FILENODE, DEL_FILENODE, \
65 MOD_FILENODE, RENAMED_FILENODE, CHMOD_FILENODE, BIN_FILENODE, COPIED_FILENODE
64 66 %>
65 67
66 68 %if use_comments:
@@ -159,45 +161,45 b" return '%s_%s_%i' % (h.md5_safe(commit+f"
159 161 </div>
160 162 % endif
161 163
162 ## comments
163 <div class="pull-right">
164 <div class="comments-number" style="padding-left: 10px">
165 % if hasattr(c, 'comments') and hasattr(c, 'inline_cnt'):
166 <i class="icon-comment" style="color: #949494">COMMENTS:</i>
167 % if c.comments:
168 <a href="#comments">${_ungettext("{} General", "{} General", len(c.comments)).format(len(c.comments))}</a>,
169 % else:
170 ${_('0 General')}
171 % endif
172
173 % if c.inline_cnt:
174 <a href="#" onclick="return Rhodecode.comments.nextComment();"
175 id="inline-comments-counter">${_ungettext("{} Inline", "{} Inline", c.inline_cnt).format(c.inline_cnt)}
176 </a>
177 % else:
178 ${_('0 Inline')}
179 % endif
180 % endif
181
182 % if pull_request_menu:
183 <%
184 outdated_comm_count_ver = pull_request_menu['outdated_comm_count_ver']
185 %>
186
187 % if outdated_comm_count_ver:
188 <a href="#" onclick="showOutdated(); Rhodecode.comments.nextOutdatedComment(); return false;">
189 (${_("{} Outdated").format(outdated_comm_count_ver)})
190 </a>
191 <a href="#" class="showOutdatedComments" onclick="showOutdated(this); return false;"> | ${_('show outdated')}</a>
192 <a href="#" class="hideOutdatedComments" style="display: none" onclick="hideOutdated(this); return false;"> | ${_('hide outdated')}</a>
193 % else:
194 (${_("{} Outdated").format(outdated_comm_count_ver)})
195 % endif
196
197 % endif
198
199 </div>
200 </div>
164 ## ## comments
165 ## <div class="pull-right">
166 ## <div class="comments-number" style="padding-left: 10px">
167 ## % if hasattr(c, 'comments') and hasattr(c, 'inline_cnt'):
168 ## <i class="icon-comment" style="color: #949494">COMMENTS:</i>
169 ## % if c.comments:
170 ## <a href="#comments">${_ungettext("{} General", "{} General", len(c.comments)).format(len(c.comments))}</a>,
171 ## % else:
172 ## ${_('0 General')}
173 ## % endif
174 ##
175 ## % if c.inline_cnt:
176 ## <a href="#" onclick="return Rhodecode.comments.nextComment();"
177 ## id="inline-comments-counter">${_ungettext("{} Inline", "{} Inline", c.inline_cnt).format(c.inline_cnt)}
178 ## </a>
179 ## % else:
180 ## ${_('0 Inline')}
181 ## % endif
182 ## % endif
183 ##
184 ## % if pull_request_menu:
185 ## <%
186 ## outdated_comm_count_ver = pull_request_menu['outdated_comm_count_ver']
187 ## %>
188 ##
189 ## % if outdated_comm_count_ver:
190 ## <a href="#" onclick="showOutdated(); Rhodecode.comments.nextOutdatedComment(); return false;">
191 ## (${_("{} Outdated").format(outdated_comm_count_ver)})
192 ## </a>
193 ## <a href="#" class="showOutdatedComments" onclick="showOutdated(this); return false;"> | ${_('show outdated')}</a>
194 ## <a href="#" class="hideOutdatedComments" style="display: none" onclick="hideOutdated(this); return false;"> | ${_('hide outdated')}</a>
195 ## % else:
196 ## (${_("{} Outdated").format(outdated_comm_count_ver)})
197 ## % endif
198 ##
199 ## % endif
200 ##
201 ## </div>
202 ## </div>
201 203
202 204 </div>
203 205
@@ -208,13 +210,6 b" return '%s_%s_%i' % (h.md5_safe(commit+f"
208 210 <a href="${h.current_route_path(request, fulldiff=1)}" onclick="return confirm('${_("Showing a big diff might take some time and resources, continue?")}')">${_('Show full diff')}</a>
209 211 </h2>
210 212 </div>
211 ## commit range header for each individual diff
212 % elif commit and hasattr(c, 'commit_ranges') and len(c.commit_ranges) > 1:
213 <div class="diffset-heading ${(diffset.limited_diff and 'diffset-heading-warning' or '')}">
214 <div class="clearinner">
215 <a class="tooltip revision" title="${h.tooltip(commit.message)}" href="${h.route_path('repo_commit',repo_name=diffset.repo_name,commit_id=commit.raw_id)}">${('r%s:%s' % (commit.idx,h.short_id(commit.raw_id)))}</a>
216 </div>
217 </div>
218 213 % endif
219 214
220 215 <div id="todo-box">
@@ -239,6 +234,43 b" return '%s_%s_%i' % (h.md5_safe(commit+f"
239 234 <% over_lines_changed_limit = False %>
240 235 %for i, filediff in enumerate(diffset.files):
241 236
237 %if filediff.source_file_path and filediff.target_file_path:
238 %if filediff.source_file_path != filediff.target_file_path:
239 ## file was renamed, or copied
240 %if RENAMED_FILENODE in filediff.patch['stats']['ops']:
241 <%
242 final_file_name = h.literal(u'{} <i class="icon-angle-left"></i> <del>{}</del>'.format(filediff.target_file_path, filediff.source_file_path))
243 final_path = filediff.target_file_path
244 %>
245 %elif COPIED_FILENODE in filediff.patch['stats']['ops']:
246 <%
247 final_file_name = h.literal(u'{} <i class="icon-angle-left"></i> {}'.format(filediff.target_file_path, filediff.source_file_path))
248 final_path = filediff.target_file_path
249 %>
250 %endif
251 %else:
252 ## file was modified
253 <%
254 final_file_name = filediff.source_file_path
255 final_path = final_file_name
256 %>
257 %endif
258 %else:
259 %if filediff.source_file_path:
260 ## file was deleted
261 <%
262 final_file_name = filediff.source_file_path
263 final_path = final_file_name
264 %>
265 %else:
266 ## file was added
267 <%
268 final_file_name = filediff.target_file_path
269 final_path = final_file_name
270 %>
271 %endif
272 %endif
273
242 274 <%
243 275 lines_changed = filediff.patch['stats']['added'] + filediff.patch['stats']['deleted']
244 276 over_lines_changed_limit = lines_changed > lines_changed_limit
@@ -258,13 +290,39 b" return '%s_%s_%i' % (h.md5_safe(commit+f"
258 290 total_file_comments = [_c for _c in h.itertools.chain.from_iterable(file_comments) if not _c.outdated]
259 291 %>
260 292 <div class="filediff-collapse-indicator icon-"></div>
293
294 ## Comments/Options PILL
261 295 <span class="pill-group pull-right" >
262 296 <span class="pill" op="comments">
263
264 297 <i class="icon-comment"></i> ${len(total_file_comments)}
265 298 </span>
299
300 <details class="details-reset details-inline-block">
301 <summary class="noselect">
302 <i class="pill icon-options cursor-pointer" op="options"></i>
303 </summary>
304 <details-menu class="details-dropdown">
305
306 <div class="dropdown-item">
307 <span>${final_path}</span>
308 <span class="pull-right icon-clipboard clipboard-action" data-clipboard-text="${final_path}" title="Copy file path"></span>
309 </div>
310
311 <div class="dropdown-divider"></div>
312
313 <div class="dropdown-item">
314 <% permalink = request.current_route_url(_anchor='a_{}'.format(h.FID(filediff.raw_id, filediff.patch['filename']))) %>
315 <a href="${permalink}">ΒΆ permalink</a>
316 <span class="pull-right icon-clipboard clipboard-action" data-clipboard-text="${permalink}" title="Copy permalink"></span>
317 </div>
318
319
320 </details-menu>
321 </details>
322
266 323 </span>
267 ${diff_ops(filediff)}
324
325 ${diff_ops(final_file_name, filediff)}
268 326
269 327 </label>
270 328
@@ -463,43 +521,15 b" return '%s_%s_%i' % (h.md5_safe(commit+f"
463 521 </div>
464 522 </%def>
465 523
466 <%def name="diff_ops(filediff)">
524 <%def name="diff_ops(file_name, filediff)">
467 525 <%
468 526 from rhodecode.lib.diffs import NEW_FILENODE, DEL_FILENODE, \
469 527 MOD_FILENODE, RENAMED_FILENODE, CHMOD_FILENODE, BIN_FILENODE, COPIED_FILENODE
470 528 %>
471 529 <span class="pill">
472 530 <i class="icon-file-text"></i>
473 %if filediff.source_file_path and filediff.target_file_path:
474 %if filediff.source_file_path != filediff.target_file_path:
475 ## file was renamed, or copied
476 %if RENAMED_FILENODE in filediff.patch['stats']['ops']:
477 ${filediff.target_file_path} β¬… <del>${filediff.source_file_path}</del>
478 <% final_path = filediff.target_file_path %>
479 %elif COPIED_FILENODE in filediff.patch['stats']['ops']:
480 ${filediff.target_file_path} β¬… ${filediff.source_file_path}
481 <% final_path = filediff.target_file_path %>
482 %endif
483 %else:
484 ## file was modified
485 ${filediff.source_file_path}
486 <% final_path = filediff.source_file_path %>
487 %endif
488 %else:
489 %if filediff.source_file_path:
490 ## file was deleted
491 ${filediff.source_file_path}
492 <% final_path = filediff.source_file_path %>
493 %else:
494 ## file was added
495 ${filediff.target_file_path}
496 <% final_path = filediff.target_file_path %>
497 %endif
498 %endif
499 <i style="color: #aaa" class="on-hover-icon icon-clipboard clipboard-action" data-clipboard-text="${final_path}" title="${_('Copy file path')}" onclick="return false;"></i>
531 ${file_name}
500 532 </span>
501 ## anchor link
502 <a class="pill filediff-anchor" href="#a_${h.FID(filediff.raw_id, filediff.patch['filename'])}">ΒΆ</a>
503 533
504 534 <span class="pill-group pull-right">
505 535
@@ -934,7 +964,7 b' def get_comments_for(diff_type, comments'
934 964 </span>
935 965 %endif
936 966 % if commit or pull_request_menu:
937 <span id="diff_nav">Loading diff...:</span>
967 <span class="tooltip" title="Navigate to previous or next change inside files." id="diff_nav">Loading diff...:</span>
938 968 <span class="cursor-pointer" onclick="scrollToPrevChunk(); return false">
939 969 <i class="icon-angle-up"></i>
940 970 </span>
@@ -21,8 +21,9 b''
21 21 ## to speed up lookups cache some functions before the loop
22 22 <%
23 23 active_patterns = h.get_active_pattern_entries(c.repo_name)
24 urlify_commit_message = h.partial(h.urlify_commit_message, active_pattern_entries=active_patterns)
24 urlify_commit_message = h.partial(h.urlify_commit_message, active_pattern_entries=active_patterns, issues_container=getattr(c, 'referenced_commit_issues', None))
25 25 %>
26
26 27 %for commit in c.commit_ranges:
27 28 <tr id="row-${commit.raw_id}"
28 29 commit_id="${commit.raw_id}"
@@ -1,6 +1,10 b''
1 1 <%text>
2 2 <div style="display: none">
3 3
4 <script>
5 var CG = new ColorGenerator();
6 </script>
7
4 8 <script id="ejs_gravatarWithUser" type="text/template" class="ejsTemplate">
5 9
6 10 <%
@@ -34,13 +38,7 b" var data_hovercard_url = pyroutes.url('h"
34 38
35 39 </script>
36 40
37 <script>
38 var CG = new ColorGenerator();
39 </script>
40
41 41 <script id="ejs_reviewMemberEntry" type="text/template" class="ejsTemplate">
42
43 <li id="reviewer_<%= member.user_id %>" class="reviewer_entry">
44 42 <%
45 43 if (create) {
46 44 var edit_visibility = 'visible';
@@ -49,23 +47,32 b' var CG = new ColorGenerator();'
49 47 }
50 48
51 49 if (member.user_group && member.user_group.vote_rule) {
52 var groupStyle = 'border-left: 1px solid '+CG.asRGB(CG.getColor(member.user_group.vote_rule));
50 var reviewGroup = '<i class="icon-user-group"></i>';
51 var reviewGroupColor = CG.asRGB(CG.getColor(member.user_group.vote_rule));
53 52 } else {
54 var groupStyle = 'border-left: 1px solid white';
53 var reviewGroup = null;
54 var reviewGroupColor = 'transparent';
55 55 }
56 var rule_show = rule_show || false;
57
58 if (rule_show) {
59 var rule_visibility = 'table-cell';
60 } else {
61 var rule_visibility = 'none';
62 }
63
56 64 %>
57 65
58 <div class="reviewers_member" style="<%= groupStyle%>" >
66 <tr id="reviewer_<%= member.user_id %>" class="reviewer_entry" tooltip="Review Group" data-reviewer-user-id="<%= member.user_id %>">
67
68 <td style="width: 20px">
59 69 <div class="reviewer_status tooltip" title="<%= review_status_label %>">
60 70 <i class="icon-circle review-status-<%= review_status %>"></i>
61 71 </div>
72 </td>
73
74 <td>
62 75 <div id="reviewer_<%= member.user_id %>_name" class="reviewer_name">
63 <% if (mandatory) { %>
64 <div class="reviewer_member_mandatory tooltip" title="Mandatory reviewer">
65 <i class="icon-lock"></i>
66 </div>
67 <% } %>
68
69 76 <%-
70 77 renderTemplate('gravatarWithUser', {
71 78 'size': 16,
@@ -77,11 +84,43 b' var CG = new ColorGenerator();'
77 84 'gravatar_url': member.gravatar_link
78 85 })
79 86 %>
87 <span class="tooltip presence-state" style="display: none" title="This users is currently at this page">
88 <i class="icon-eye" style="color: #0ac878"></i>
89 </span>
80 90 </div>
91 </td>
92
93 <td style="width: 10px">
94 <% if (reviewGroup !== null) { %>
95 <span class="tooltip" title="Member of review group from rule: `<%= member.user_group.name %>`" style="color: <%= reviewGroupColor %>">
96 <%- reviewGroup %>
97 </span>
98 <% } %>
99 </td>
81 100
101 <% if (mandatory) { %>
102 <td style="text-align: right;width: 10px;">
103 <div class="reviewer_member_mandatory tooltip" title="Mandatory reviewer">
104 <i class="icon-lock"></i>
105 </div>
106 </td>
107
108 <% } else { %>
109 <td style="text-align: right;width: 10px;">
110 <% if (allowed_to_update) { %>
111 <div class="reviewer_member_remove" onclick="reviewersController.removeReviewMember(<%= member.user_id %>, true)" style="visibility: <%= edit_visibility %>;">
112 <i class="icon-remove"></i>
113 </div>
114 <% } %>
115 </td>
116 <% } %>
117
118 </tr>
119
120 <tr>
121 <td colspan="4" style="display: <%= rule_visibility %>" class="pr-user-rule-container">
82 122 <input type="hidden" name="__start__" value="reviewer:mapping">
83 123
84
85 124 <%if (member.user_group && member.user_group.vote_rule) {%>
86 125 <div class="reviewer_reason">
87 126
@@ -112,24 +151,11 b' var CG = new ColorGenerator();'
112 151 <input type="hidden" name="mandatory" value="<%= mandatory %>"/>
113 152
114 153 <input type="hidden" name="__end__" value="reviewer:mapping">
115
116 <% if (mandatory) { %>
117 <div class="reviewer_member_mandatory_remove" style="visibility: <%= edit_visibility %>;">
118 <i class="icon-remove"></i>
119 </div>
120 <% } else { %>
121 <% if (allowed_to_update) { %>
122 <div class="reviewer_member_remove action_button" onclick="reviewersController.removeReviewMember(<%= member.user_id %>, true)" style="visibility: <%= edit_visibility %>;">
123 <i class="icon-remove" ></i>
124 </div>
125 <% } %>
126 <% } %>
127 </div>
128 </li>
154 </td>
155 </tr>
129 156
130 157 </script>
131 158
132
133 159 <script id="ejs_commentVersion" type="text/template" class="ejsTemplate">
134 160
135 161 <%
@@ -158,8 +184,56 b' if (show_disabled) {'
158 184 </script>
159 185
160 186
187 <script id="ejs_sideBarCommentHovercard" type="text/template" class="ejsTemplate">
188
189 <div>
190 <% if (is_todo) { %>
191 <% if (inline) { %>
192 <strong>Inline</strong> TODO on line: <%= line_no %>
193 <% if (version_info) { %>
194 <%= version_info %>
195 <% } %>
196 <br/>
197 File: <code><%- file_name -%></code>
198 <% } else { %>
199 <% if (review_status) { %>
200 <i class="icon-circle review-status-<%= review_status %>"></i>
201 <% } %>
202 <strong>General</strong> TODO
203 <% if (version_info) { %>
204 <%= version_info %>
205 <% } %>
206 <% } %>
207 <% } else { %>
208 <% if (inline) { %>
209 <strong>Inline</strong> comment on line: <%= line_no %>
210 <% if (version_info) { %>
211 <%= version_info %>
212 <% } %>
213 <br/>
214 File: <code><%- file_name -%></code>
215 <% } else { %>
216 <% if (review_status) { %>
217 <i class="icon-circle review-status-<%= review_status %>"></i>
218 <% } %>
219 <strong>General</strong> comment
220 <% if (version_info) { %>
221 <%= version_info %>
222 <% } %>
223 <% } %>
224 <% } %>
225 <br/>
226 Created:
227 <time class="timeago" title="<%= created_on %>" datetime="<%= datetime %>"><%= $.timeago(datetime) %></time>
228
161 229 </div>
162 230
231 </script>
232
233 ##// END OF EJS Templates
234 </div>
235
236
163 237 <script>
164 238 // registers the templates into global cache
165 239 registerTemplates();
@@ -360,13 +360,13 b' text_monospace = "\'Menlo\', \'Liberation M'
360 360
361 361 div.markdown-block ul.checkbox li, div.markdown-block ol.checkbox li {
362 362 list-style: none !important;
363 margin: 6px !important;
363 margin: 0px !important;
364 364 padding: 0 !important
365 365 }
366 366
367 367 div.markdown-block ul li, div.markdown-block ol li {
368 368 list-style: disc !important;
369 margin: 6px !important;
369 margin: 0px !important;
370 370 padding: 0 !important
371 371 }
372 372
@@ -19,20 +19,73 b''
19 19 <div class="box">
20 20 ${h.secure_form(h.route_path('pullrequest_create', repo_name=c.repo_name, _query=request.GET.mixed()), id='pull_request_form', request=request)}
21 21
22 <div class="box pr-summary">
22 <div class="box">
23 23
24 24 <div class="summary-details block-left">
25 25
26
27 <div class="pr-details-title">
28 ${_('New pull request')}
29 </div>
30
31 26 <div class="form" style="padding-top: 10px">
32 <!-- fields -->
33 27
34 28 <div class="fields" >
35 29
30 ## COMMIT FLOW
31 <div class="field">
32 <div class="label label-textarea">
33 <label for="commit_flow">${_('Commit flow')}:</label>
34 </div>
35
36 <div class="content">
37 <div class="flex-container">
38 <div style="width: 45%;">
39 <div class="panel panel-default source-panel">
40 <div class="panel-heading">
41 <h3 class="panel-title">${_('Source repository')}</h3>
42 </div>
43 <div class="panel-body">
44 <div style="display:none">${c.rhodecode_db_repo.description}</div>
45 ${h.hidden('source_repo')}
46 ${h.hidden('source_ref')}
47
48 <div id="pr_open_message"></div>
49 </div>
50 </div>
51 </div>
52
53 <div style="width: 90px; text-align: center; padding-top: 30px">
54 <div>
55 <i class="icon-right" style="font-size: 2.2em"></i>
56 </div>
57 <div style="position: relative; top: 10px">
58 <span class="tag tag">
59 <span id="switch_base"></span>
60 </span>
61 </div>
62
63 </div>
64
65 <div style="width: 45%;">
66
67 <div class="panel panel-default target-panel">
68 <div class="panel-heading">
69 <h3 class="panel-title">${_('Target repository')}</h3>
70 </div>
71 <div class="panel-body">
72 <div style="display:none" id="target_repo_desc"></div>
73 ${h.hidden('target_repo')}
74 ${h.hidden('target_ref')}
75 <span id="target_ref_loading" style="display: none">
76 ${_('Loading refs...')}
77 </span>
78 </div>
79 </div>
80
81 </div>
82 </div>
83
84 </div>
85
86 </div>
87
88 ## TITLE
36 89 <div class="field">
37 90 <div class="label">
38 91 <label for="pullrequest_title">${_('Title')}:</label>
@@ -45,6 +98,7 b''
45 98 </p>
46 99 </div>
47 100
101 ## DESC
48 102 <div class="field">
49 103 <div class="label label-textarea">
50 104 <label for="pullrequest_desc">${_('Description')}:</label>
@@ -55,72 +109,14 b''
55 109 </div>
56 110 </div>
57 111
112 ## REVIEWERS
58 113 <div class="field">
59 114 <div class="label label-textarea">
60 <label for="commit_flow">${_('Commit flow')}:</label>
61 </div>
62
63 ## TODO: johbo: Abusing the "content" class here to get the
64 ## desired effect. Should be replaced by a proper solution.
65
66 ##ORG
67 <div class="content">
68 <strong>${_('Source repository')}:</strong>
69 ${c.rhodecode_db_repo.description}
115 <label for="pullrequest_reviewers">${_('Reviewers')}:</label>
70 116 </div>
71 117 <div class="content">
72 ${h.hidden('source_repo')}
73 ${h.hidden('source_ref')}
74 </div>
75
76 ##OTHER, most Probably the PARENT OF THIS FORK
77 <div class="content">
78 ## filled with JS
79 <div id="target_repo_desc"></div>
80 </div>
81
82 <div class="content">
83 ${h.hidden('target_repo')}
84 ${h.hidden('target_ref')}
85 <span id="target_ref_loading" style="display: none">
86 ${_('Loading refs...')}
87 </span>
88 </div>
89 </div>
90
91 <div class="field">
92 <div class="label label-textarea">
93 <label for="pullrequest_submit"></label>
94 </div>
95 <div class="input">
96 <div class="pr-submit-button">
97 <input id="pr_submit" class="btn" name="save" type="submit" value="${_('Submit Pull Request')}">
98 </div>
99 <div id="pr_open_message"></div>
100 </div>
101 </div>
102
103 <div class="pr-spacing-container"></div>
104 </div>
105 </div>
106 </div>
107 <div>
108 ## AUTHOR
109 <div class="reviewers-title block-right">
110 <div class="pr-details-title">
111 ${_('Author of this pull request')}
112 </div>
113 </div>
114 <div class="block-right pr-details-content reviewers">
115 <ul class="group_members">
116 <li>
117 ${self.gravatar_with_user(c.rhodecode_user.email, 16, tooltip=True)}
118 </li>
119 </ul>
120 </div>
121
122 118 ## REVIEW RULES
123 <div id="review_rules" style="display: none" class="reviewers-title block-right">
119 <div id="review_rules" style="display: none" class="reviewers-title">
124 120 <div class="pr-details-title">
125 121 ${_('Reviewer rules')}
126 122 </div>
@@ -130,32 +126,48 b''
130 126 </div>
131 127
132 128 ## REVIEWERS
133 <div class="reviewers-title block-right">
129 <div class="reviewers-title">
134 130 <div class="pr-details-title">
135 131 ${_('Pull request reviewers')}
136 132 <span class="calculate-reviewers"> - ${_('loading...')}</span>
137 133 </div>
138 134 </div>
139 <div id="reviewers" class="block-right pr-details-content reviewers">
135 <div id="reviewers" class="pr-details-content reviewers">
140 136 ## members goes here, filled via JS based on initial selection !
141 137 <input type="hidden" name="__start__" value="review_members:sequence">
142 <ul id="review_members" class="group_members"></ul>
138 <table id="review_members" class="group_members">
139 ## This content is loaded via JS and ReviewersPanel
140 </table>
143 141 <input type="hidden" name="__end__" value="review_members:sequence">
142
144 143 <div id="add_reviewer_input" class='ac'>
145 144 <div class="reviewer_ac">
146 145 ${h.text('user', class_='ac-input', placeholder=_('Add reviewer or reviewer group'))}
147 146 <div id="reviewers_container"></div>
148 147 </div>
149 148 </div>
149
150 150 </div>
151 151 </div>
152 152 </div>
153 <div class="box">
154 <div>
155 ## overview pulled by ajax
156 <div id="pull_request_overview"></div>
153
154 ## SUBMIT
155 <div class="field">
156 <div class="label label-textarea">
157 <label for="pullrequest_submit"></label>
158 </div>
159 <div class="input">
160 <div class="pr-submit-button">
161 <input id="pr_submit" class="btn" name="save" type="submit" value="${_('Submit Pull Request')}">
157 162 </div>
158 163 </div>
164 </div>
165 </div>
166 </div>
167 </div>
168
169 </div>
170
159 171 ${h.end_form()}
160 172 </div>
161 173
@@ -243,8 +255,6 b''
243 255
244 256 var diffDataHandler = function(data) {
245 257
246 $('#pull_request_overview').html(data);
247
248 258 var commitElements = data['commits'];
249 259 var files = data['files'];
250 260 var added = data['stats'][0]
@@ -303,27 +313,33 b''
303 313
304 314 msg += '<input type="hidden" name="__end__" value="revisions:sequence">'
305 315 msg += _ngettext(
306 'This pull requests will consist of <strong>{0} commit</strong>.',
307 'This pull requests will consist of <strong>{0} commits</strong>.',
316 'Compare summary: <strong>{0} commit</strong>',
317 'Compare summary: <strong>{0} commits</strong>',
308 318 commitElements.length).format(commitElements.length)
309 319
310 msg += '\n';
320 msg += '';
311 321 msg += _ngettext(
312 '<strong>{0} file</strong> changed, ',
313 '<strong>{0} files</strong> changed, ',
322 '<strong>, and {0} file</strong> changed.',
323 '<strong>, and {0} files</strong> changed.',
314 324 files.length).format(files.length)
315 msg += '<span class="op-added">{0} lines inserted</span>, <span class="op-deleted">{1} lines deleted</span>.'.format(added, deleted)
316 325
317 msg += '\n\n <a class="" id="pull_request_overview_url" href="{0}" target="_blank">${_('Show detailed compare.')}</a>'.format(url);
326 msg += '\n Diff: <span class="op-added">{0} lines inserted</span>, <span class="op-deleted">{1} lines deleted </span>.'.format(added, deleted)
327
328 msg += '\n <a class="" id="pull_request_overview_url" href="{0}" target="_blank">${_('Show detailed compare.')}</a>'.format(url);
318 329
319 330 if (commitElements.length) {
320 331 var commitsLink = '<a href="#pull_request_overview"><strong>{0}</strong></a>'.format(commitElements.length);
321 332 prButtonLock(false, msg.replace('__COMMITS__', commitsLink), 'compare');
322 333 }
323 334 else {
324 prButtonLock(true, "${_('There are no commits to merge.')}", 'compare');
335 var noCommitsMsg = '<span class="alert-text-warning">{0}</span>'.format(
336 _gettext('There are no commits to merge.'));
337 prButtonLock(true, noCommitsMsg, 'compare');
325 338 }
326 339
340 //make both panels equal
341 $('.target-panel').height($('.source-panel').height())
342
327 343 };
328 344
329 345 reviewersController = new ReviewersController();
@@ -429,10 +445,12 b''
429 445
430 446 var targetRepoChanged = function(repoData) {
431 447 // generate new DESC of target repo displayed next to select
448
449 $('#target_repo_desc').html(repoData['description']);
450
432 451 var prLink = pyroutes.url('pullrequest_new', {'repo_name': repoData['name']});
433 $('#target_repo_desc').html(
434 "<strong>${_('Target repository')}</strong>: {0}. <a href=\"{1}\">Switch base, and use as source.</a>".format(repoData['description'], prLink)
435 );
452 var title = _gettext('Switch target repository with the source.')
453 $('#switch_base').html("<a class=\"tooltip\" title=\"{0}\" href=\"{1}\">Switch sides</a>".format(title, prLink))
436 454
437 455 // generate dynamic select2 for refs.
438 456 initTargetRefs(repoData['refs']['select2_refs'],
This diff has been collapsed as it changes many lines, (570 lines changed) Show them Hide them
@@ -1,6 +1,8 b''
1 1 <%inherit file="/base/base.mako"/>
2 2 <%namespace name="base" file="/base/base.mako"/>
3 3 <%namespace name="dt" file="/data_table/_dt_elements.mako"/>
4 <%namespace name="sidebar" file="/base/sidebar.mako"/>
5
4 6
5 7 <%def name="title()">
6 8 ${_('{} Pull Request !{}').format(c.repo_name, c.pull_request.pull_request_id)}
@@ -21,12 +23,19 b''
21 23 ${self.repo_menu(active='showpullrequest')}
22 24 </%def>
23 25
26
24 27 <%def name="main()">
28 ## Container to gather extracted Tickets
29 <%
30 c.referenced_commit_issues = []
31 c.referenced_desc_issues = []
32 %>
25 33
26 34 <script type="text/javascript">
27 35 // TODO: marcink switch this to pyroutes
28 36 AJAX_COMMENT_DELETE_URL = "${h.route_path('pullrequest_comment_delete',repo_name=c.repo_name,pull_request_id=c.pull_request.pull_request_id,comment_id='__COMMENT_ID__')}";
29 37 templateContext.pull_request_data.pull_request_id = ${c.pull_request.pull_request_id};
38 templateContext.pull_request_data.pull_request_version = '${request.GET.get('version', '')}';
30 39 </script>
31 40
32 41 <div class="box">
@@ -79,7 +88,7 b''
79 88 </div>
80 89
81 90 <div id="pr-desc" class="input" title="${_('Rendered using {} renderer').format(c.renderer)}">
82 ${h.render(c.pull_request.description, renderer=c.renderer, repo_name=c.repo_name)}
91 ${h.render(c.pull_request.description, renderer=c.renderer, repo_name=c.repo_name, issues_container=c.referenced_desc_issues)}
83 92 </div>
84 93
85 94 <div id="pr-desc-edit" class="input textarea" style="display: none;">
@@ -89,29 +98,6 b''
89 98
90 99 <div id="summary" class="fields pr-details-content">
91 100
92 ## review
93 <div class="field">
94 <div class="label-pr-detail">
95 <label>${_('Review status')}:</label>
96 </div>
97 <div class="input">
98 %if c.pull_request_review_status:
99 <div class="tag status-tag-${c.pull_request_review_status}">
100 <i class="icon-circle review-status-${c.pull_request_review_status}"></i>
101 <span class="changeset-status-lbl">
102 %if c.pull_request.is_closed():
103 ${_('Closed')},
104 %endif
105
106 ${h.commit_status_lbl(c.pull_request_review_status)}
107
108 </span>
109 </div>
110 - ${_ungettext('calculated based on {} reviewer vote', 'calculated based on {} reviewers votes', len(c.pull_request_reviewers)).format(len(c.pull_request_reviewers))}
111 %endif
112 </div>
113 </div>
114
115 101 ## source
116 102 <div class="field">
117 103 <div class="label-pr-detail">
@@ -136,7 +122,7 b''
136 122
137 123 ${_('of')} <a href="${h.route_path('repo_summary', repo_name=c.pull_request.target_repo.repo_name)}">${c.pull_request.target_repo.repo_name}</a>
138 124
139 <a class="source-details-action" href="#expand-source-details" onclick="return versionController.toggleElement(this, '.source-details')" data-toggle-on='<i class="icon-angle-down">more details</i>' data-toggle-off='<i class="icon-angle-up">less details</i>'>
125 <a class="source-details-action" href="#expand-source-details" onclick="return toggleElement(this, '.source-details')" data-toggle-on='<i class="icon-angle-down">more details</i>' data-toggle-off='<i class="icon-angle-up">less details</i>'>
140 126 <i class="icon-angle-down">more details</i>
141 127 </a>
142 128
@@ -231,7 +217,7 b''
231 217 </code>
232 218 </td>
233 219 <td>
234 <input ${('checked="checked"' if c.from_version_num == ver_pr else '')} class="compare-radio-button" type="radio" name="ver_source" value="${ver_pr or 'latest'}" data-ver-pos="${ver_pos}"/>
220 <input ${('checked="checked"' if c.from_version_index == ver_pr else '')} class="compare-radio-button" type="radio" name="ver_source" value="${ver_pr or 'latest'}" data-ver-pos="${ver_pos}"/>
235 221 <input ${('checked="checked"' if c.at_version_num == ver_pr else '')} class="compare-radio-button" type="radio" name="ver_target" value="${ver_pr or 'latest'}" data-ver-pos="${ver_pos}"/>
236 222 </td>
237 223 <td>
@@ -280,154 +266,7 b''
280 266
281 267 </div>
282 268
283 ## REVIEW RULES
284 <div id="review_rules" style="display: none" class="reviewers-title block-right">
285 <div class="pr-details-title">
286 ${_('Reviewer rules')}
287 %if c.allowed_to_update:
288 <span id="close_edit_reviewers" class="block-right action_button last-item" style="display: none;">${_('Close')}</span>
289 %endif
290 </div>
291 <div class="pr-reviewer-rules">
292 ## review rules will be appended here, by default reviewers logic
293 </div>
294 <input id="review_data" type="hidden" name="review_data" value="">
295 </div>
296 269
297 ## REVIEWERS
298 <div class="reviewers-title first-panel block-right">
299 <div class="pr-details-title">
300 ${_('Pull request reviewers')}
301 %if c.allowed_to_update:
302 <span id="open_edit_reviewers" class="block-right action_button last-item">${_('Edit')}</span>
303 %endif
304 </div>
305 </div>
306 <div id="reviewers" class="block-right pr-details-content reviewers">
307
308 ## members redering block
309 <input type="hidden" name="__start__" value="review_members:sequence">
310 <ul id="review_members" class="group_members">
311
312 % for review_obj, member, reasons, mandatory, status in c.pull_request_reviewers:
313 <script>
314 var member = ${h.json.dumps(h.reviewer_as_json(member, reasons=reasons, mandatory=mandatory, user_group=review_obj.rule_user_group_data()))|n};
315 var status = "${(status[0][1].status if status else 'not_reviewed')}";
316 var status_lbl = "${h.commit_status_lbl(status[0][1].status if status else 'not_reviewed')}";
317 var allowed_to_update = ${h.json.dumps(c.allowed_to_update)};
318
319 var entry = renderTemplate('reviewMemberEntry', {
320 'member': member,
321 'mandatory': member.mandatory,
322 'reasons': member.reasons,
323 'allowed_to_update': allowed_to_update,
324 'review_status': status,
325 'review_status_label': status_lbl,
326 'user_group': member.user_group,
327 'create': false
328 });
329 $('#review_members').append(entry)
330 </script>
331
332 % endfor
333
334 </ul>
335
336 <input type="hidden" name="__end__" value="review_members:sequence">
337 ## end members redering block
338
339 %if not c.pull_request.is_closed():
340 <div id="add_reviewer" class="ac" style="display: none;">
341 %if c.allowed_to_update:
342 % if not c.forbid_adding_reviewers:
343 <div id="add_reviewer_input" class="reviewer_ac">
344 ${h.text('user', class_='ac-input', placeholder=_('Add reviewer or reviewer group'))}
345 <div id="reviewers_container"></div>
346 </div>
347 % endif
348 <div class="pull-right">
349 <button id="update_pull_request" class="btn btn-small no-margin">${_('Save Changes')}</button>
350 </div>
351 %endif
352 </div>
353 %endif
354 </div>
355
356 ## TODOs will be listed here
357 <div class="reviewers-title block-right">
358 <div class="pr-details-title">
359 ## Only show unresolved, that is only what matters
360 TODO Comments - ${len(c.unresolved_comments)} / ${(len(c.unresolved_comments) + len(c.resolved_comments))}
361
362 % if not c.at_version:
363 % if c.resolved_comments:
364 <span class="block-right action_button last-item noselect" onclick="$('.unresolved-todo-text').toggle(); return versionController.toggleElement(this, '.unresolved-todo');" data-toggle-on="Show resolved" data-toggle-off="Hide resolved">Show resolved</span>
365 % else:
366 <span class="block-right last-item noselect">Show resolved</span>
367 % endif
368 % endif
369 </div>
370 </div>
371 <div class="block-right pr-details-content reviewers">
372
373 <table class="todo-table">
374 <%
375 def sorter(entry):
376 user_id = entry.author.user_id
377 resolved = '1' if entry.resolved else '0'
378 if user_id == c.rhodecode_user.user_id:
379 # own comments first
380 user_id = 0
381 return '{}_{}_{}'.format(resolved, user_id, str(entry.comment_id).zfill(100))
382 %>
383
384 % if c.at_version:
385 <tr>
386 <td class="unresolved-todo-text">${_('unresolved TODOs unavailable in this view')}.</td>
387 </tr>
388 % else:
389 % for todo_comment in sorted(c.unresolved_comments + c.resolved_comments, key=sorter):
390 <% resolved = todo_comment.resolved %>
391 % if inline:
392 <% outdated_at_ver = todo_comment.outdated_at_version(getattr(c, 'at_version_num', None)) %>
393 % else:
394 <% outdated_at_ver = todo_comment.older_than_version(getattr(c, 'at_version_num', None)) %>
395 % endif
396
397 <tr ${('class="unresolved-todo" style="display: none"' if resolved else '') |n}>
398
399 <td class="td-todo-number">
400 % if resolved:
401 <a class="permalink todo-resolved tooltip" title="${_('Resolved by comment #{}').format(todo_comment.resolved.comment_id)}" href="#comment-${todo_comment.comment_id}" onclick="return Rhodecode.comments.scrollToComment($('#comment-${todo_comment.comment_id}'), 0, ${h.json.dumps(outdated_at_ver)})">
402 <i class="icon-flag-filled"></i> ${todo_comment.comment_id}</a>
403 % else:
404 <a class="permalink" href="#comment-${todo_comment.comment_id}" onclick="return Rhodecode.comments.scrollToComment($('#comment-${todo_comment.comment_id}'), 0, ${h.json.dumps(outdated_at_ver)})">
405 <i class="icon-flag-filled"></i> ${todo_comment.comment_id}</a>
406 % endif
407 </td>
408 <td class="td-todo-gravatar">
409 ${base.gravatar(todo_comment.author.email, 16, user=todo_comment.author, tooltip=True, extra_class=['no-margin'])}
410 </td>
411 <td class="todo-comment-text-wrapper">
412 <div class="todo-comment-text">
413 <code>${h.chop_at_smart(todo_comment.text, '\n', suffix_if_chopped='...')}</code>
414 </div>
415 </td>
416
417 </tr>
418 % endfor
419
420 % if len(c.unresolved_comments) == 0:
421 <tr>
422 <td class="unresolved-todo-text">${_('No unresolved TODOs')}.</td>
423 </tr>
424 % endif
425
426 % endif
427
428 </table>
429
430 </div>
431 270 </div>
432 271
433 272 </div>
@@ -484,9 +323,9 b''
484 323 <div class="compare_view_commits_title">
485 324 % if not c.compare_mode:
486 325
487 % if c.at_version_pos:
326 % if c.at_version_index:
488 327 <h4>
489 ${_('Showing changes at v%d, commenting is disabled.') % c.at_version_pos}
328 ${_('Showing changes at v{}, commenting is disabled.').format(c.at_version_index)}
490 329 </h4>
491 330 % endif
492 331
@@ -539,10 +378,11 b''
539 378 </div>
540 379
541 380 % if not c.missing_commits:
381 ## COMPARE RANGE DIFF MODE
542 382 % if c.compare_mode:
543 383 % if c.at_version:
544 384 <h4>
545 ${_('Commits and changes between v{ver_from} and {ver_to} of this pull request, commenting is disabled').format(ver_from=c.from_version_pos, ver_to=c.at_version_pos if c.at_version_pos else 'latest')}:
385 ${_('Commits and changes between v{ver_from} and {ver_to} of this pull request, commenting is disabled').format(ver_from=c.from_version_index, ver_to=c.at_version_index if c.at_version_index else 'latest')}:
546 386 </h4>
547 387
548 388 <div class="subtitle-compare">
@@ -597,7 +437,7 b''
597 437 </td>
598 438 <td class="mid td-description">
599 439 <div class="log-container truncate-wrap">
600 <div class="message truncate" id="c-${commit.raw_id}" data-message-raw="${commit.message}">${h.urlify_commit_message(commit.message, c.repo_name)}</div>
440 <div class="message truncate" id="c-${commit.raw_id}" data-message-raw="${commit.message}">${h.urlify_commit_message(commit.message, c.repo_name, issues_container=c.referenced_commit_issues)}</div>
601 441 </div>
602 442 </td>
603 443 </tr>
@@ -608,19 +448,13 b''
608 448
609 449 % endif
610 450
451 ## Regular DIFF
611 452 % else:
612 453 <%include file="/compare/compare_commits.mako" />
613 454 % endif
614 455
615 456 <div class="cs_files">
616 457 <%namespace name="cbdiffs" file="/codeblocks/diffs.mako"/>
617 % if c.at_version:
618 <% c.inline_cnt = len(c.inline_versions[c.at_version_num]['display']) %>
619 <% c.comments = c.comment_versions[c.at_version_num]['display'] %>
620 % else:
621 <% c.inline_cnt = len(c.inline_versions[c.at_version_num]['until']) %>
622 <% c.comments = c.comment_versions[c.at_version_num]['until'] %>
623 % endif
624 458
625 459 <%
626 460 pr_menu_data = {
@@ -667,7 +501,7 b''
667 501 ## comments heading with count
668 502 <div class="comments-heading">
669 503 <i class="icon-comment"></i>
670 ${_('Comments')} ${len(c.comments)}
504 ${_('General Comments')} ${len(c.comments)}
671 505 </div>
672 506
673 507 ## render general comments
@@ -704,6 +538,271 b''
704 538 % endif
705 539 </div>
706 540
541
542 ### NAV SIDEBAR
543 <aside class="right-sidebar right-sidebar-expanded" id="pr-nav-sticky" style="display: none">
544 <div class="sidenav navbar__inner" >
545 ## TOGGLE
546 <div class="sidebar-toggle" onclick="toggleSidebar(); return false">
547 <a href="#toggleSidebar" class="grey-link-action">
548
549 </a>
550 </div>
551
552 ## CONTENT
553 <div class="sidebar-content">
554
555 ## RULES SUMMARY/RULES
556 <div class="sidebar-element clear-both">
557 <% vote_title = _ungettext(
558 'Status calculated based on votes from {} reviewer',
559 'Status calculated based on votes from {} reviewers', len(c.allowed_reviewers)).format(len(c.allowed_reviewers))
560 %>
561
562 <div class="tooltip right-sidebar-collapsed-state" style="display: none" onclick="toggleSidebar(); return false" title="${vote_title}">
563 <i class="icon-circle review-status-${c.pull_request_review_status}"></i>
564 ${len(c.allowed_reviewers)}
565 </div>
566
567 ## REVIEW RULES
568 <div id="review_rules" style="display: none" class="">
569 <div class="right-sidebar-expanded-state pr-details-title">
570 <span class="sidebar-heading">
571 ${_('Reviewer rules')}
572 </span>
573
574 </div>
575 <div class="pr-reviewer-rules">
576 ## review rules will be appended here, by default reviewers logic
577 </div>
578 <input id="review_data" type="hidden" name="review_data" value="">
579 </div>
580
581 ## REVIEWERS
582 <div class="right-sidebar-expanded-state pr-details-title">
583 <span class="tooltip sidebar-heading" title="${vote_title}">
584 <i class="icon-circle review-status-${c.pull_request_review_status}"></i>
585 ${_('Reviewers')}
586 </span>
587 %if c.allowed_to_update:
588 <span id="open_edit_reviewers" class="block-right action_button last-item">${_('Edit')}</span>
589 <span id="close_edit_reviewers" class="block-right action_button last-item" style="display: none;">${_('Close')}</span>
590 %else:
591 <span id="open_edit_reviewers" class="block-right action_button last-item">${_('Show rules')}</span>
592 <span id="close_edit_reviewers" class="block-right action_button last-item" style="display: none;">${_('Close')}</span>
593 %endif
594 </div>
595
596 <div id="reviewers" class="right-sidebar-expanded-state pr-details-content reviewers">
597
598 ## members redering block
599 <input type="hidden" name="__start__" value="review_members:sequence">
600
601 <table id="review_members" class="group_members">
602 ## This content is loaded via JS and ReviewersPanel
603 </table>
604
605 <input type="hidden" name="__end__" value="review_members:sequence">
606 ## end members redering block
607
608 %if not c.pull_request.is_closed():
609 <div id="add_reviewer" class="ac" style="display: none;">
610 %if c.allowed_to_update:
611 % if not c.forbid_adding_reviewers:
612 <div id="add_reviewer_input" class="reviewer_ac">
613 ${h.text('user', class_='ac-input', placeholder=_('Add reviewer or reviewer group'))}
614 <div id="reviewers_container"></div>
615 </div>
616 % endif
617 <div class="pull-right">
618 <button id="update_pull_request" class="btn btn-small no-margin">${_('Save Changes')}</button>
619 </div>
620 %endif
621 </div>
622 %endif
623 </div>
624 </div>
625
626 ## ## OBSERVERS
627 ## <div class="sidebar-element clear-both">
628 ## <div class="tooltip right-sidebar-collapsed-state" style="display: none" onclick="toggleSidebar(); return false" title="${_('Observers')}">
629 ## <i class="icon-eye"></i>
630 ## 0
631 ## </div>
632 ##
633 ## <div class="right-sidebar-expanded-state pr-details-title">
634 ## <span class="sidebar-heading">
635 ## <i class="icon-eye"></i>
636 ## ${_('Observers')}
637 ## </span>
638 ## </div>
639 ## <div class="right-sidebar-expanded-state pr-details-content">
640 ## No observers
641 ## </div>
642 ## </div>
643
644 ## TODOs
645 <div class="sidebar-element clear-both">
646 <div class="tooltip right-sidebar-collapsed-state" style="display: none" onclick="toggleSidebar(); return false" title="TODOs">
647 <i class="icon-flag-filled"></i>
648 <span id="todos-count">${len(c.unresolved_comments)}</span>
649 </div>
650
651 <div class="right-sidebar-expanded-state pr-details-title">
652 ## Only show unresolved, that is only what matters
653 <span class="sidebar-heading noselect" onclick="refreshTODOs(); return false">
654 <i class="icon-flag-filled"></i>
655 TODOs
656 </span>
657
658 % if not c.at_version:
659 % if c.resolved_comments:
660 <span class="block-right action_button last-item noselect" onclick="$('.unresolved-todo-text').toggle(); return toggleElement(this, '.resolved-todo');" data-toggle-on="Show resolved" data-toggle-off="Hide resolved">Show resolved</span>
661 % else:
662 <span class="block-right last-item noselect">Show resolved</span>
663 % endif
664 % endif
665 </div>
666
667 <div class="right-sidebar-expanded-state pr-details-content">
668
669 % if c.at_version:
670 <table>
671 <tr>
672 <td class="unresolved-todo-text">${_('TODOs unavailable when browsing versions')}.</td>
673 </tr>
674 </table>
675 % else:
676 % if c.unresolved_comments + c.resolved_comments:
677 ${sidebar.comments_table(c.unresolved_comments + c.resolved_comments, len(c.unresolved_comments), todo_comments=True)}
678 % else:
679 <table>
680 <tr>
681 <td>
682 ${_('No TODOs yet')}
683 </td>
684 </tr>
685 </table>
686 % endif
687 % endif
688 </div>
689 </div>
690
691 ## COMMENTS
692 <div class="sidebar-element clear-both">
693 <div class="tooltip right-sidebar-collapsed-state" style="display: none" onclick="toggleSidebar(); return false" title="${_('Comments')}">
694 <i class="icon-comment" style="color: #949494"></i>
695 <span id="comments-count">${len(c.inline_comments_flat+c.comments)}</span>
696 <span class="display-none" id="general-comments-count">${len(c.comments)}</span>
697 <span class="display-none" id="inline-comments-count">${len(c.inline_comments_flat)}</span>
698 </div>
699
700 <div class="right-sidebar-expanded-state pr-details-title">
701 <span class="sidebar-heading noselect" onclick="refreshComments(); return false">
702 <i class="icon-comment" style="color: #949494"></i>
703 ${_('Comments')}
704
705 ## % if outdated_comm_count_ver:
706 ## <a href="#" onclick="showOutdated(); Rhodecode.comments.nextOutdatedComment(); return false;">
707 ## (${_("{} Outdated").format(outdated_comm_count_ver)})
708 ## </a>
709 ## <a href="#" class="showOutdatedComments" onclick="showOutdated(this); return false;"> | ${_('show outdated')}</a>
710 ## <a href="#" class="hideOutdatedComments" style="display: none" onclick="hideOutdated(this); return false;"> | ${_('hide outdated')}</a>
711
712 ## % else:
713 ## (${_("{} Outdated").format(outdated_comm_count_ver)})
714 ## % endif
715
716 </span>
717
718 % if outdated_comm_count_ver:
719 <span class="block-right action_button last-item noselect" onclick="return toggleElement(this, '.hidden-comment');" data-toggle-on="Show outdated" data-toggle-off="Hide outdated">Show outdated</span>
720 % else:
721 <span class="block-right last-item noselect">Show hidden</span>
722 % endif
723
724 </div>
725
726 <div class="right-sidebar-expanded-state pr-details-content">
727 % if c.inline_comments_flat + c.comments:
728 ${sidebar.comments_table(c.inline_comments_flat + c.comments, len(c.inline_comments_flat+c.comments))}
729 % else:
730 <table>
731 <tr>
732 <td>
733 ${_('No Comments yet')}
734 </td>
735 </tr>
736 </table>
737 % endif
738 </div>
739
740 </div>
741
742 ## Referenced Tickets
743 <div class="sidebar-element clear-both">
744 <div class="tooltip right-sidebar-collapsed-state" style="display: none" onclick="toggleSidebar(); return false" title="${_('Referenced Tickets')}">
745 <i class="icon-info-circled"></i>
746 ${(len(c.referenced_desc_issues) + len(c.referenced_commit_issues))}
747 </div>
748
749 <div class="right-sidebar-expanded-state pr-details-title">
750 <span class="sidebar-heading">
751 <i class="icon-info-circled"></i>
752 ${_('Referenced Tickets')}
753 </span>
754 </div>
755 <div class="right-sidebar-expanded-state pr-details-content">
756 <table>
757
758 <tr><td><code>${_('In pull request description')}:</code></td></tr>
759 % if c.referenced_desc_issues:
760 % for ticket_dict in c.referenced_desc_issues:
761 <tr>
762 <td>
763 <a href="${ticket_dict.get('url')}">
764 ${ticket_dict.get('id')}
765 </a>
766 </td>
767 </tr>
768 % endfor
769 % else:
770 <tr>
771 <td>
772 ${_('No Ticket data found.')}
773 </td>
774 </tr>
775 % endif
776
777 <tr><td style="padding-top: 10px"><code>${_('In commit messages')}:</code></td></tr>
778 % if c.referenced_commit_issues:
779 % for ticket_dict in c.referenced_commit_issues:
780 <tr>
781 <td>
782 <a href="${ticket_dict.get('url')}">
783 ${ticket_dict.get('id')}
784 </a>
785 </td>
786 </tr>
787 % endfor
788 % else:
789 <tr>
790 <td>
791 ${_('No Ticket data found.')}
792 </td>
793 </tr>
794 % endif
795 </table>
796
797 </div>
798 </div>
799
800 </div>
801
802 </div>
803 </aside>
804
805 ## This JS needs to be at the end
707 806 <script type="text/javascript">
708 807
709 808 versionController = new VersionController();
@@ -714,7 +813,11 b''
714 813
715 814 updateController = new UpdatePrController();
716 815
717 $(function () {
816 window.reviewerRulesData = ${c.pull_request_default_reviewers_data_json | n};
817 window.setReviewersData = ${c.pull_request_set_reviewers_data_json | n};
818
819 (function () {
820 "use strict";
718 821
719 822 // custom code mirror
720 823 var codeMirrorInstance = $('#pr-description-input').get(0).MarkupForm.cm;
@@ -737,12 +840,13 b''
737 840 },
738 841
739 842 edit: function (event) {
843 var cmInstance = $('#pr-description-input').get(0).MarkupForm.cm;
740 844 this.viewFields.hide();
741 845 this.editButton.hide();
742 846 this.deleteButton.hide();
743 847 this.closeButton.show();
744 848 this.editFields.show();
745 codeMirrorInstance.refresh();
849 cmInstance.refresh();
746 850 },
747 851
748 852 view: function (event) {
@@ -754,60 +858,24 b''
754 858 }
755 859 };
756 860
757 var ReviewersPanel = {
758 editButton: $('#open_edit_reviewers'),
759 closeButton: $('#close_edit_reviewers'),
760 addButton: $('#add_reviewer'),
761 removeButtons: $('.reviewer_member_remove,.reviewer_member_mandatory_remove'),
762
763 init: function () {
764 var self = this;
765 this.editButton.on('click', function (e) {
766 self.edit();
767 });
768 this.closeButton.on('click', function (e) {
769 self.close();
770 });
771 },
861 PRDetails.init();
862 ReviewersPanel.init(reviewerRulesData, setReviewersData);
772 863
773 edit: function (event) {
774 this.editButton.hide();
775 this.closeButton.show();
776 this.addButton.show();
777 this.removeButtons.css('visibility', 'visible');
778 // review rules
779 reviewersController.loadReviewRules(
780 ${c.pull_request.reviewer_data_json | n});
781 },
782
783 close: function (event) {
784 this.editButton.show();
785 this.closeButton.hide();
786 this.addButton.hide();
787 this.removeButtons.css('visibility', 'hidden');
788 // hide review rules
789 reviewersController.hideReviewRules()
790 }
791 };
792
793 PRDetails.init();
794 ReviewersPanel.init();
795
796 showOutdated = function (self) {
864 window.showOutdated = function (self) {
797 865 $('.comment-inline.comment-outdated').show();
798 866 $('.filediff-outdated').show();
799 867 $('.showOutdatedComments').hide();
800 868 $('.hideOutdatedComments').show();
801 869 };
802 870
803 hideOutdated = function (self) {
871 window.hideOutdated = function (self) {
804 872 $('.comment-inline.comment-outdated').hide();
805 873 $('.filediff-outdated').hide();
806 874 $('.hideOutdatedComments').hide();
807 875 $('.showOutdatedComments').show();
808 876 };
809 877
810 refreshMergeChecks = function () {
878 window.refreshMergeChecks = function () {
811 879 var loadUrl = "${request.current_route_path(_query=dict(merge_checks=1))}";
812 880 $('.pull-request-merge').css('opacity', 0.3);
813 881 $('.action-buttons-extra').css('opacity', 0.3);
@@ -821,7 +889,7 b''
821 889 );
822 890 };
823 891
824 closePullRequest = function (status) {
892 window.closePullRequest = function (status) {
825 893 if (!confirm(_gettext('Are you sure to close this pull request without merging?'))) {
826 894 return false;
827 895 }
@@ -831,6 +899,7 b''
831 899 $(generalCommentForm.submitForm).submit();
832 900 };
833 901
902 //TODO this functionality is now missing
834 903 $('#show-outdated-comments').on('click', function (e) {
835 904 var button = $(this);
836 905 var outdated = $('.comment-outdated');
@@ -844,22 +913,6 b''
844 913 }
845 914 });
846 915
847 $('.show-inline-comments').on('change', function (e) {
848 var show = 'none';
849 var target = e.currentTarget;
850 if (target.checked) {
851 show = ''
852 }
853 var boxid = $(target).attr('id_for');
854 var comments = $('#{0} .inline-comments'.format(boxid));
855 var fn_display = function (idx) {
856 $(this).css('display', show);
857 };
858 $(comments).each(fn_display);
859 var btns = $('#{0} .inline-comments-button'.format(boxid));
860 $(btns).each(fn_display);
861 });
862
863 916 $('#merge_pull_request_form').submit(function () {
864 917 if (!$('#merge_pull_request').attr('disabled')) {
865 918 $('#merge_pull_request').attr('disabled', 'disabled');
@@ -884,7 +937,6 b''
884 937 "${c.repo_name}", "${c.pull_request.pull_request_id}");
885 938 });
886 939
887
888 940 // fixing issue with caches on firefox
889 941 $('#update_commits').removeAttr("disabled");
890 942
@@ -905,6 +957,22 b''
905 957 }
906 958 });
907 959
960 $('.show-inline-comments').on('change', function (e) {
961 var show = 'none';
962 var target = e.currentTarget;
963 if (target.checked) {
964 show = ''
965 }
966 var boxid = $(target).attr('id_for');
967 var comments = $('#{0} .inline-comments'.format(boxid));
968 var fn_display = function (idx) {
969 $(this).css('display', show);
970 };
971 $(comments).each(fn_display);
972 var btns = $('#{0} .inline-comments-button'.format(boxid));
973 $(btns).each(fn_display);
974 });
975
908 976 // register submit callback on commentForm form to track TODOs
909 977 window.commentFormGlobalSubmitSuccessCallback = function () {
910 978 refreshMergeChecks();
@@ -912,10 +980,14 b''
912 980
913 981 ReviewerAutoComplete('#user');
914 982
983 })();
984
985 $(document).ready(function () {
986
987 var channel = '${c.pr_broadcast_channel}';
988 new ReviewerPresenceController(channel)
989
915 990 })
916
917 991 </script>
918 992
919 </div>
920
921 993 </%def>
@@ -948,8 +948,8 b' def assert_inline_comments(pull_request,'
948 948 if visible is not None:
949 949 inline_comments = CommentsModel().get_inline_comments(
950 950 pull_request.target_repo.repo_id, pull_request=pull_request)
951 inline_cnt = CommentsModel().get_inline_comments_count(
952 inline_comments)
951 inline_cnt = len(CommentsModel().get_inline_comments_as_list(
952 inline_comments))
953 953 assert inline_cnt == visible
954 954 if outdated is not None:
955 955 outdated_comments = CommentsModel().get_outdated_comments(
General Comments 0
You need to be logged in to leave comments. Login now