##// 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
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 [bumpversion]
1 [bumpversion]
2 current_version = 4.20.1
2 current_version = 4.21.0
3 message = release: Bump version {current_version} to {new_version}
3 message = release: Bump version {current_version} to {new_version}
4
4
5 [bumpversion:file:rhodecode/VERSION]
5 [bumpversion:file:rhodecode/VERSION]
6
@@ -5,25 +5,20 b' done = false'
5 done = true
5 done = true
6
6
7 [task:rc_tools_pinned]
7 [task:rc_tools_pinned]
8 done = true
9
8
10 [task:fixes_on_stable]
9 [task:fixes_on_stable]
11 done = true
12
10
13 [task:pip2nix_generated]
11 [task:pip2nix_generated]
14 done = true
15
12
16 [task:changelog_updated]
13 [task:changelog_updated]
17 done = true
18
14
19 [task:generate_api_docs]
15 [task:generate_api_docs]
20 done = true
16
17 [task:updated_translation]
21
18
22 [release]
19 [release]
23 state = prepared
20 state = in_progress
24 version = 4.20.1
21 version = 4.21.0
25
26 [task:updated_translation]
27
22
28 [task:generate_js_routes]
23 [task:generate_js_routes]
29
24
@@ -147,12 +147,13 b' Use the following example to configure N'
147
147
148 ## Special Cache for file store, make sure you enable this intentionally as
148 ## Special Cache for file store, make sure you enable this intentionally as
149 ## it could bypass upload files permissions
149 ## it could bypass upload files permissions
150 # location /_file_store/download {
150 # location /_file_store/download/gravatars {
151 #
151 #
152 # proxy_cache cache_zone;
152 # proxy_cache cache_zone;
153 # # ignore Set-Cookie
153 # # ignore Set-Cookie
154 # proxy_ignore_headers Set-Cookie;
154 # proxy_ignore_headers Set-Cookie;
155 # proxy_ignore_headers Cookie;
155 # # ignore cache-control
156 # proxy_ignore_headers Cache-Control;
156 #
157 #
157 # proxy_cache_key $host$uri$is_args$args;
158 # proxy_cache_key $host$uri$is_args$args;
158 # proxy_cache_methods GET;
159 # proxy_cache_methods GET;
@@ -32,6 +32,8 b' self: super: {'
32 patches = [
32 patches = [
33 ./patches/beaker/patch-beaker-lock-func-debug.diff
33 ./patches/beaker/patch-beaker-lock-func-debug.diff
34 ./patches/beaker/patch-beaker-metadata-reuse.diff
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 license = [ pkgs.lib.licenses.bsdOriginal ];
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 "appenlight-client" = super.buildPythonPackage {
52 "appenlight-client" = super.buildPythonPackage {
39 name = "appenlight-client-0.6.26";
53 name = "appenlight-client-0.6.26";
40 doCheck = false;
54 doCheck = false;
@@ -236,20 +250,23 b' self: super: {'
236 };
250 };
237 };
251 };
238 "channelstream" = super.buildPythonPackage {
252 "channelstream" = super.buildPythonPackage {
239 name = "channelstream-0.5.2";
253 name = "channelstream-0.6.14";
240 doCheck = false;
254 doCheck = false;
241 propagatedBuildInputs = [
255 propagatedBuildInputs = [
242 self."gevent"
256 self."gevent"
243 self."ws4py"
257 self."ws4py"
258 self."marshmallow"
259 self."python-dateutil"
244 self."pyramid"
260 self."pyramid"
245 self."pyramid-jinja2"
261 self."pyramid-jinja2"
262 self."pyramid-apispec"
246 self."itsdangerous"
263 self."itsdangerous"
247 self."requests"
264 self."requests"
248 self."six"
265 self."six"
249 ];
266 ];
250 src = fetchurl {
267 src = fetchurl {
251 url = "https://files.pythonhosted.org/packages/2b/31/29a8e085cf5bf97fa88e7b947adabfc581a18a3463adf77fb6dada34a65f/channelstream-0.5.2.tar.gz";
268 url = "https://files.pythonhosted.org/packages/d4/2d/86d6757ccd06ce673ee224123471da3d45251d061da7c580bfc259bad853/channelstream-0.6.14.tar.gz";
252 sha256 = "1qbm4xdl5hfkja683x546bncg3rqq8qv79w1m1a1wd48cqqzb6rm";
269 sha256 = "0qgy5j3rj6c8cslzidh32glhkrhbbdxjc008y69v8a0y3zyaz2d3";
253 };
270 };
254 meta = {
271 meta = {
255 license = [ pkgs.lib.licenses.bsdOriginal ];
272 license = [ pkgs.lib.licenses.bsdOriginal ];
@@ -862,11 +879,11 b' self: super: {'
862 };
879 };
863 };
880 };
864 "itsdangerous" = super.buildPythonPackage {
881 "itsdangerous" = super.buildPythonPackage {
865 name = "itsdangerous-0.24";
882 name = "itsdangerous-1.1.0";
866 doCheck = false;
883 doCheck = false;
867 src = fetchurl {
884 src = fetchurl {
868 url = "https://files.pythonhosted.org/packages/dc/b4/a60bcdba945c00f6d608d8975131ab3f25b22f2bcfe1dab221165194b2d4/itsdangerous-0.24.tar.gz";
885 url = "https://files.pythonhosted.org/packages/68/1a/f27de07a8a304ad5fa817bbe383d1238ac4396da447fa11ed937039fa04b/itsdangerous-1.1.0.tar.gz";
869 sha256 = "06856q6x675ly542ig0plbqcyab6ksfzijlyf1hzhgg3sgwgrcyb";
886 sha256 = "068zpbksq5q2z4dckh2k1zbcq43ay74ylqn77rni797j0wyh66rj";
870 };
887 };
871 meta = {
888 meta = {
872 license = [ pkgs.lib.licenses.bsdOriginal ];
889 license = [ pkgs.lib.licenses.bsdOriginal ];
@@ -993,6 +1010,17 b' self: super: {'
993 license = [ pkgs.lib.licenses.bsdOriginal pkgs.lib.licenses.bsd3 ];
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 "mistune" = super.buildPythonPackage {
1024 "mistune" = super.buildPythonPackage {
997 name = "mistune-0.8.4";
1025 name = "mistune-0.8.4";
998 doCheck = false;
1026 doCheck = false;
@@ -1522,6 +1550,20 b' self: super: {'
1522 license = [ { fullName = "Repoze Public License"; } { fullName = "BSD-derived (http://www.repoze.org/LICENSE.txt)"; } ];
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 "pyramid-mailer" = super.buildPythonPackage {
1567 "pyramid-mailer" = super.buildPythonPackage {
1526 name = "pyramid-mailer-0.15.1";
1568 name = "pyramid-mailer-0.15.1";
1527 doCheck = false;
1569 doCheck = false;
@@ -1763,6 +1805,17 b' self: super: {'
1763 license = [ pkgs.lib.licenses.bsdOriginal { fullName = "LGPL+BSD"; } { fullName = "GNU Library or Lesser General Public License (LGPL)"; } ];
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 "redis" = super.buildPythonPackage {
1819 "redis" = super.buildPythonPackage {
1767 name = "redis-3.4.1";
1820 name = "redis-3.4.1";
1768 doCheck = false;
1821 doCheck = false;
@@ -1819,7 +1872,7 b' self: super: {'
1819 };
1872 };
1820 };
1873 };
1821 "rhodecode-enterprise-ce" = super.buildPythonPackage {
1874 "rhodecode-enterprise-ce" = super.buildPythonPackage {
1822 name = "rhodecode-enterprise-ce-4.20.1";
1875 name = "rhodecode-enterprise-ce-4.20.0";
1823 buildInputs = [
1876 buildInputs = [
1824 self."pytest"
1877 self."pytest"
1825 self."py"
1878 self."py"
@@ -5,7 +5,7 b' babel==1.3'
5 beaker==1.9.1
5 beaker==1.9.1
6 bleach==3.1.3
6 bleach==3.1.3
7 celery==4.3.0
7 celery==4.3.0
8 channelstream==0.5.2
8 channelstream==0.6.14
9 click==7.0
9 click==7.0
10 colander==1.7.0
10 colander==1.7.0
11 # our custom configobj
11 # our custom configobj
@@ -22,7 +22,7 b' future==0.14.3'
22 futures==3.0.2
22 futures==3.0.2
23 infrae.cache==1.0.1
23 infrae.cache==1.0.1
24 iso8601==0.1.12
24 iso8601==0.1.12
25 itsdangerous==0.24
25 itsdangerous==1.1.0
26 kombu==4.6.6
26 kombu==4.6.6
27 lxml==4.2.5
27 lxml==4.2.5
28 mako==1.1.0
28 mako==1.1.0
@@ -18,10 +18,11 b' jsonschema==2.6.0'
18 pluggy==0.13.1
18 pluggy==0.13.1
19 pyasn1-modules==0.2.6
19 pyasn1-modules==0.2.6
20 pyramid-jinja2==2.7
20 pyramid-jinja2==2.7
21 pyramid-apispec==0.3.2
21 scandir==1.10.0
22 scandir==1.10.0
22 setproctitle==1.1.10
23 setproctitle==1.1.10
23 tempita==0.5.2
24 tempita==0.5.2
24 testpath==0.4.4
25 testpath==0.4.4
25 transaction==2.4.0
26 transaction==2.4.0
26 vine==1.3.0
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 EXTENSIONS = {}
48 EXTENSIONS = {}
49
49
50 __version__ = ('.'.join((str(each) for each in VERSION[:3])))
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 __platform__ = platform.system()
52 __platform__ = platform.system()
53 __license__ = 'AGPLv3, and Commercial License'
53 __license__ = 'AGPLv3, and Commercial License'
54 __author__ = 'RhodeCode GmbH'
54 __author__ = 'RhodeCode GmbH'
@@ -170,8 +170,7 b' def validate_repo_permissions(apiuser, r'
170 """
170 """
171 if not HasRepoPermissionAnyApi(*perms)(
171 if not HasRepoPermissionAnyApi(*perms)(
172 user=apiuser, repo_name=repo.repo_name):
172 user=apiuser, repo_name=repo.repo_name):
173 raise JSONRPCError(
173 raise JSONRPCError('repository `%s` does not exist' % repoid)
174 'repository `%s` does not exist' % repoid)
175
174
176 return True
175 return True
177
176
@@ -307,8 +307,7 b' def get_repo_changeset(request, apiuser,'
307 """
307 """
308 repo = get_repo_or_error(repoid)
308 repo = get_repo_or_error(repoid)
309 if not has_superadmin_permission(apiuser):
309 if not has_superadmin_permission(apiuser):
310 _perms = (
310 _perms = ('repository.admin', 'repository.write', 'repository.read',)
311 'repository.admin', 'repository.write', 'repository.read',)
312 validate_repo_permissions(apiuser, repoid, repo, _perms)
311 validate_repo_permissions(apiuser, repoid, repo, _perms)
313
312
314 changes_details = Optional.extract(details)
313 changes_details = Optional.extract(details)
@@ -366,8 +365,7 b' def get_repo_changesets(request, apiuser'
366 """
365 """
367 repo = get_repo_or_error(repoid)
366 repo = get_repo_or_error(repoid)
368 if not has_superadmin_permission(apiuser):
367 if not has_superadmin_permission(apiuser):
369 _perms = (
368 _perms = ('repository.admin', 'repository.write', 'repository.read',)
370 'repository.admin', 'repository.write', 'repository.read',)
371 validate_repo_permissions(apiuser, repoid, repo, _perms)
369 validate_repo_permissions(apiuser, repoid, repo, _perms)
372
370
373 changes_details = Optional.extract(details)
371 changes_details = Optional.extract(details)
@@ -1021,7 +1019,8 b' def update_repo('
1021
1019
1022 include_secrets = False
1020 include_secrets = False
1023 if not has_superadmin_permission(apiuser):
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 else:
1024 else:
1026 include_secrets = True
1025 include_secrets = True
1027
1026
@@ -1208,8 +1207,7 b' def fork_repo(request, apiuser, repoid, '
1208 if not has_superadmin_permission(apiuser):
1207 if not has_superadmin_permission(apiuser):
1209 # check if we have at least read permission for
1208 # check if we have at least read permission for
1210 # this repo that we fork !
1209 # this repo that we fork !
1211 _perms = (
1210 _perms = ('repository.admin', 'repository.write', 'repository.read')
1212 'repository.admin', 'repository.write', 'repository.read')
1213 validate_repo_permissions(apiuser, repoid, repo, _perms)
1211 validate_repo_permissions(apiuser, repoid, repo, _perms)
1214
1212
1215 # check if the regular user has at least fork permissions as well
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 if not has_superadmin_permission(apiuser):
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 try:
2377 try:
2378 repo = get_repo_or_error(repoid)
2379 settings_model = VcsSettingsModel(repo=repo)
2378 settings_model = VcsSettingsModel(repo=repo)
2380 settings = settings_model.get_global_settings()
2379 settings = settings_model.get_global_settings()
2381 settings.update(settings_model.get_repo_settings())
2380 settings.update(settings_model.get_repo_settings())
@@ -2414,9 +2413,11 b' def set_repo_settings(request, apiuser, '
2414 "result": true
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 if not has_superadmin_permission(apiuser):
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 if type(settings) is not dict:
2422 if type(settings) is not dict:
2422 raise JSONRPCError('Settings have to be a JSON Object.')
2423 raise JSONRPCError('Settings have to be a JSON Object.')
@@ -34,7 +34,7 b' from rhodecode.lib.channelstream import '
34 get_user_data,
34 get_user_data,
35 parse_channels_info,
35 parse_channels_info,
36 update_history_from_logs,
36 update_history_from_logs,
37 STATE_PUBLIC_KEYS)
37 USER_STATE_PUBLIC_KEYS)
38
38
39 from rhodecode.lib.auth import NotAnonymous
39 from rhodecode.lib.auth import NotAnonymous
40
40
@@ -86,14 +86,16 b' class ChannelstreamView(BaseAppView):'
86 'display_name': None,
86 'display_name': None,
87 'display_link': None,
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 payload = {
92 payload = {
91 'username': user.username,
93 'username': user.username,
92 'user_state': user_data,
94 'user_state': user_data,
93 'conn_id': str(uuid.uuid4()),
95 'conn_id': str(uuid.uuid4()),
94 'channels': channels,
96 'channels': channels,
95 'channel_configs': {},
97 'channel_configs': {},
96 'state_public_keys': STATE_PUBLIC_KEYS,
98 'state_public_keys': USER_STATE_PUBLIC_KEYS,
97 'info': {
99 'info': {
98 'exclude_channels': ['broadcast']
100 'exclude_channels': ['broadcast']
99 }
101 }
@@ -118,10 +120,13 b' class ChannelstreamView(BaseAppView):'
118 'Channelstream service at {} is down'.format(channelstream_url))
120 'Channelstream service at {} is down'.format(channelstream_url))
119 return HTTPBadGateway()
121 return HTTPBadGateway()
120
122
123 channel_info = connect_result.get('channels_info')
124 if not channel_info:
125 raise HTTPBadRequest()
126
121 connect_result['channels'] = channels
127 connect_result['channels'] = channels
122 connect_result['channels_info'] = parse_channels_info(
128 connect_result['channels_info'] = parse_channels_info(
123 connect_result['channels_info'],
129 channel_info, include_channel_info=filtered_channels)
124 include_channel_info=filtered_channels)
125 update_history_from_logs(self.channelstream_config,
130 update_history_from_logs(self.channelstream_config,
126 filtered_channels, connect_result)
131 filtered_channels, connect_result)
127 return connect_result
132 return connect_result
@@ -167,10 +172,15 b' class ChannelstreamView(BaseAppView):'
167 log.exception(
172 log.exception(
168 'Channelstream service at {} is down'.format(channelstream_url))
173 'Channelstream service at {} is down'.format(channelstream_url))
169 return HTTPBadGateway()
174 return HTTPBadGateway()
175
176 channel_info = connect_result.get('channels_info')
177 if not channel_info:
178 raise HTTPBadRequest()
179
170 # include_channel_info will limit history only to new channel
180 # include_channel_info will limit history only to new channel
171 # to not overwrite histories on other channels in client
181 # to not overwrite histories on other channels in client
172 connect_result['channels_info'] = parse_channels_info(
182 connect_result['channels_info'] = parse_channels_info(
173 connect_result['channels_info'],
183 channel_info,
174 include_channel_info=filtered_channels)
184 include_channel_info=filtered_channels)
175 update_history_from_logs(
185 update_history_from_logs(
176 self.channelstream_config, filtered_channels, connect_result)
186 self.channelstream_config, filtered_channels, connect_result)
@@ -43,10 +43,10 b' def includeme(config):'
43 pattern='/_file_store/upload')
43 pattern='/_file_store/upload')
44 config.add_route(
44 config.add_route(
45 name='download_file',
45 name='download_file',
46 pattern='/_file_store/download/{fid}')
46 pattern='/_file_store/download/{fid:.*}')
47 config.add_route(
47 config.add_route(
48 name='download_file_by_token',
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 # Scan module for configuration decorators.
51 # Scan module for configuration decorators.
52 config.scan('.views', ignore='.tests')
52 config.scan('.views', ignore='.tests')
@@ -20,6 +20,7 b''
20
20
21 import os
21 import os
22 import time
22 import time
23 import errno
23 import shutil
24 import shutil
24 import hashlib
25 import hashlib
25
26
@@ -32,9 +33,24 b' from rhodecode.apps.file_store.exception'
32 METADATA_VER = 'v1'
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 class LocalFileStorage(object):
46 class LocalFileStorage(object):
36
47
37 @classmethod
48 @classmethod
49 def apply_counter(cls, counter, filename):
50 name_counted = '%d-%s' % (counter, filename)
51 return name_counted
52
53 @classmethod
38 def resolve_name(cls, name, directory):
54 def resolve_name(cls, name, directory):
39 """
55 """
40 Resolves a unique name and the correct path. If a filename
56 Resolves a unique name and the correct path. If a filename
@@ -47,17 +63,16 b' class LocalFileStorage(object):'
47
63
48 counter = 0
64 counter = 0
49 while True:
65 while True:
50 name = '%d-%s' % (counter, name)
66 name_counted = cls.apply_counter(counter, name)
51
67
52 # sub_store prefix to optimize disk usage, e.g some_path/ab/final_file
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 sub_store_path = os.path.join(directory, sub_store)
70 sub_store_path = os.path.join(directory, sub_store)
55 if not os.path.exists(sub_store_path):
71 safe_make_dirs(sub_store_path)
56 os.makedirs(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 if not os.path.exists(path):
74 if not os.path.exists(path):
60 return name, path
75 return name_counted, path
61 counter += 1
76 counter += 1
62
77
63 @classmethod
78 @classmethod
@@ -102,8 +117,13 b' class LocalFileStorage(object):'
102
117
103 :param filename: base name of file
118 :param filename: base name of file
104 """
119 """
105 sub_store = self._sub_store_from_filename(filename)
120 prefix_dir = ''
106 return os.path.join(self.base_path, sub_store, filename)
121 if '/' in filename:
122 prefix_dir, filename = filename.split('/')
123 sub_store = self._sub_store_from_filename(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 def delete(self, filename):
128 def delete(self, filename):
109 """
129 """
@@ -123,7 +143,7 b' class LocalFileStorage(object):'
123 Checks if file exists. Resolves filename's absolute
143 Checks if file exists. Resolves filename's absolute
124 path based on base_path.
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 return os.path.exists(self.store_path(filename))
148 return os.path.exists(self.store_path(filename))
129
149
@@ -158,7 +178,7 b' class LocalFileStorage(object):'
158 return ext in [normalize_ext(x) for x in extensions]
178 return ext in [normalize_ext(x) for x in extensions]
159
179
160 def save_file(self, file_obj, filename, directory=None, extensions=None,
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 Saves a file object to the uploads location.
183 Saves a file object to the uploads location.
164 Returns the resolved filename, i.e. the directory +
184 Returns the resolved filename, i.e. the directory +
@@ -169,6 +189,7 b' class LocalFileStorage(object):'
169 :param directory: relative path of sub-directory
189 :param directory: relative path of sub-directory
170 :param extensions: iterable of allowed extensions, if not default
190 :param extensions: iterable of allowed extensions, if not default
171 :param max_filesize: maximum size of file that should be allowed
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 :param extra_metadata: extra JSON metadata to store next to the file with .meta suffix
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 else:
204 else:
184 dest_directory = self.base_path
205 dest_directory = self.base_path
185
206
186 if not os.path.exists(dest_directory):
207 safe_make_dirs(dest_directory)
187 os.makedirs(dest_directory)
188
208
189 filename = utils.uid_filename(filename)
209 uid_filename = utils.uid_filename(filename, randomized=randomized_name)
190
210
191 # resolve also produces special sub-dir for file optimized store
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 stored_file_dir = os.path.dirname(path)
213 stored_file_dir = os.path.dirname(path)
194
214
195 file_obj.seek(0)
215 file_obj.seek(0)
@@ -210,12 +230,13 b' class LocalFileStorage(object):'
210
230
211 file_hash = self.calculate_path_hash(path)
231 file_hash = self.calculate_path_hash(path)
212
232
213 metadata.update(
233 metadata.update({
214 {"filename": filename,
234 "filename": filename,
215 "size": size,
235 "size": size,
216 "time": time.time(),
236 "time": time.time(),
217 "sha256": file_hash,
237 "sha256": file_hash,
218 "meta_ver": METADATA_VER})
238 "meta_ver": METADATA_VER
239 })
219
240
220 filename_meta = filename + '.meta'
241 filename_meta = filename + '.meta'
221 with open(os.path.join(stored_file_dir, filename_meta), "wb") as dest_meta:
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 import uuid
22 import uuid
23
23 import StringIO
24 import pathlib2
24 import pathlib2
25
25
26
26
@@ -52,3 +52,7 b' def uid_filename(filename, randomized=Tr'
52 hash_key = '{}.{}'.format(filename, 'store')
52 hash_key = '{}.{}'.format(filename, 'store')
53 uid = uuid.uuid5(uuid.NAMESPACE_URL, hash_key)
53 uid = uuid.uuid5(uuid.NAMESPACE_URL, hash_key)
54 return str(uid) + ext.lower()
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 file_uid, store_path)
64 file_uid, store_path)
65 raise HTTPNotFound()
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 if not db_obj:
68 if not db_obj:
69 raise HTTPNotFound()
69 raise HTTPNotFound()
70
70
@@ -345,6 +345,16 b' def includeme(config):'
345 pattern='/{repo_name:.*?[^/]}/pull-request/{pull_request_id:\d+}/comment/{comment_id}/delete',
345 pattern='/{repo_name:.*?[^/]}/pull-request/{pull_request_id:\d+}/comment/{comment_id}/delete',
346 repo_route=True, repo_accepted_types=['hg', 'git'])
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 # Artifacts, (EE feature)
358 # Artifacts, (EE feature)
349 config.add_route(
359 config.add_route(
350 name='repo_artifacts_list',
360 name='repo_artifacts_list',
@@ -485,23 +485,10 b' class TestRepoCommitCommentsView(TestCon'
485
485
486
486
487 def assert_comment_links(response, comments, inline_comments):
487 def assert_comment_links(response, comments, inline_comments):
488 if comments == 1:
488 response.mustcontain(
489 comments_text = "%d General" % comments
489 '<span class="display-none" id="general-comments-count">{}</span>'.format(comments))
490 else:
490 response.mustcontain(
491 comments_text = "%d General" % comments
491 '<span class="display-none" id="inline-comments-count">{}</span>'.format(inline_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
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:
494
504 response.mustcontain(
505 'id="inline-comments-counter">%s' % inline_comments_text)
506 else:
507 response.mustcontain(inline_comments_text)
@@ -619,7 +619,12 b' class ComparePage(AssertResponse):'
619 self.contains_one_anchor(file_id)
619 self.contains_one_anchor(file_id)
620 diffblock = doc.cssselect('[data-f-path="%s"]' % filename)
620 diffblock = doc.cssselect('[data-f-path="%s"]' % filename)
621 assert len(diffblock) == 2
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 def contains_change_summary(self, files_changed, inserted, deleted):
629 def contains_change_summary(self, files_changed, inserted, deleted):
625 template = (
630 template = (
@@ -150,9 +150,9 b' class TestPullrequestsView(object):'
150 response = self.app.post(
150 response = self.app.post(
151 route_path('pullrequest_create', repo_name=source.repo_name),
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 ('source_ref', source_ref),
154 ('source_ref', source_ref),
155 ('target_repo', target.repo_name),
155 ('target_repo', target_repo_name),
156 ('target_ref', target_ref),
156 ('target_ref', target_ref),
157 ('common_ancestor', commit_ids['initial-commit']),
157 ('common_ancestor', commit_ids['initial-commit']),
158 ('pullrequest_title', 'Title'),
158 ('pullrequest_title', 'Title'),
@@ -1110,16 +1110,17 b' class TestPullrequestsView(object):'
1110
1110
1111 # source has ancestor - change - change-2
1111 # source has ancestor - change - change-2
1112 backend.pull_heads(source, heads=['change-2'])
1112 backend.pull_heads(source, heads=['change-2'])
1113 target_repo_name = target.repo_name
1113
1114
1114 # update PR
1115 # update PR
1115 self.app.post(
1116 self.app.post(
1116 route_path('pullrequest_update',
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 params={'update_commits': 'true', 'csrf_token': csrf_token})
1119 params={'update_commits': 'true', 'csrf_token': csrf_token})
1119
1120
1120 response = self.app.get(
1121 response = self.app.get(
1121 route_path('pullrequest_show',
1122 route_path('pullrequest_show',
1122 repo_name=target.repo_name,
1123 repo_name=target_repo_name,
1123 pull_request_id=pull_request.pull_request_id))
1124 pull_request_id=pull_request.pull_request_id))
1124
1125
1125 assert response.status_int == 200
1126 assert response.status_int == 200
@@ -1166,10 +1167,11 b' class TestPullrequestsView(object):'
1166 # source has ancestor - ancestor-new - change-rebased
1167 # source has ancestor - ancestor-new - change-rebased
1167 backend.pull_heads(target, heads=['ancestor-new'])
1168 backend.pull_heads(target, heads=['ancestor-new'])
1168 backend.pull_heads(source, heads=['change-rebased'])
1169 backend.pull_heads(source, heads=['change-rebased'])
1170 target_repo_name = target.repo_name
1169
1171
1170 # update PR
1172 # update PR
1171 url = route_path('pullrequest_update',
1173 url = route_path('pullrequest_update',
1172 repo_name=target.repo_name,
1174 repo_name=target_repo_name,
1173 pull_request_id=pull_request_id)
1175 pull_request_id=pull_request_id)
1174 self.app.post(url,
1176 self.app.post(url,
1175 params={'update_commits': 'true', 'csrf_token': csrf_token},
1177 params={'update_commits': 'true', 'csrf_token': csrf_token},
@@ -1183,7 +1185,7 b' class TestPullrequestsView(object):'
1183
1185
1184 response = self.app.get(
1186 response = self.app.get(
1185 route_path('pullrequest_show',
1187 route_path('pullrequest_show',
1186 repo_name=target.repo_name,
1188 repo_name=target_repo_name,
1187 pull_request_id=pull_request.pull_request_id))
1189 pull_request_id=pull_request.pull_request_id))
1188 assert response.status_int == 200
1190 assert response.status_int == 200
1189 response.mustcontain('Pull request updated to')
1191 response.mustcontain('Pull request updated to')
@@ -1232,16 +1234,17 b' class TestPullrequestsView(object):'
1232 vcsrepo = target.scm_instance()
1234 vcsrepo = target.scm_instance()
1233 vcsrepo.config.clear_section('hooks')
1235 vcsrepo.config.clear_section('hooks')
1234 vcsrepo.run_git_command(['reset', '--soft', 'HEAD~2'])
1236 vcsrepo.run_git_command(['reset', '--soft', 'HEAD~2'])
1237 target_repo_name = target.repo_name
1235
1238
1236 # update PR
1239 # update PR
1237 url = route_path('pullrequest_update',
1240 url = route_path('pullrequest_update',
1238 repo_name=target.repo_name,
1241 repo_name=target_repo_name,
1239 pull_request_id=pull_request_id)
1242 pull_request_id=pull_request_id)
1240 self.app.post(url,
1243 self.app.post(url,
1241 params={'update_commits': 'true', 'csrf_token': csrf_token},
1244 params={'update_commits': 'true', 'csrf_token': csrf_token},
1242 status=200)
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 assert response.status_int == 200
1248 assert response.status_int == 200
1246 response.mustcontain('Pull request updated to')
1249 response.mustcontain('Pull request updated to')
1247 response.mustcontain('with 0 added, 0 removed commits.')
1250 response.mustcontain('with 0 added, 0 removed commits.')
@@ -1280,11 +1283,12 b' class TestPullrequestsView(object):'
1280 # source has ancestor - ancestor-new - change-rebased
1283 # source has ancestor - ancestor-new - change-rebased
1281 backend.pull_heads(target, heads=['ancestor-new'])
1284 backend.pull_heads(target, heads=['ancestor-new'])
1282 backend.pull_heads(source, heads=['change-rebased'])
1285 backend.pull_heads(source, heads=['change-rebased'])
1286 target_repo_name = target.repo_name
1283
1287
1284 # update PR
1288 # update PR
1285 self.app.post(
1289 self.app.post(
1286 route_path('pullrequest_update',
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 params={'update_commits': 'true', 'csrf_token': csrf_token},
1292 params={'update_commits': 'true', 'csrf_token': csrf_token},
1289 status=200)
1293 status=200)
1290
1294
@@ -1389,6 +1393,8 b' class TestPullrequestsView(object):'
1389 pull_request = pr_util.create_pull_request(
1393 pull_request = pr_util.create_pull_request(
1390 commits, target_head='old-feature', source_head='new-feature',
1394 commits, target_head='old-feature', source_head='new-feature',
1391 revisions=['new-feature'], mergeable=True)
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 vcs = pr_util.source_repository.scm_instance()
1399 vcs = pr_util.source_repository.scm_instance()
1394 if backend.alias == 'git':
1400 if backend.alias == 'git':
@@ -1397,8 +1403,8 b' class TestPullrequestsView(object):'
1397 vcs.strip(pr_util.commit_ids['new-feature'])
1403 vcs.strip(pr_util.commit_ids['new-feature'])
1398
1404
1399 url = route_path('pullrequest_update',
1405 url = route_path('pullrequest_update',
1400 repo_name=pull_request.target_repo.repo_name,
1406 repo_name=target_repo_name,
1401 pull_request_id=pull_request.pull_request_id)
1407 pull_request_id=pr_id)
1402 response = self.app.post(url,
1408 response = self.app.post(url,
1403 params={'update_commits': 'true',
1409 params={'update_commits': 'true',
1404 'csrf_token': csrf_token})
1410 'csrf_token': csrf_token})
@@ -1409,8 +1415,8 b' class TestPullrequestsView(object):'
1409 # Make sure that after update, it won't raise 500 errors
1415 # Make sure that after update, it won't raise 500 errors
1410 response = self.app.get(route_path(
1416 response = self.app.get(route_path(
1411 'pullrequest_show',
1417 'pullrequest_show',
1412 repo_name=pr_util.target_repository.repo_name,
1418 repo_name=target_repo_name,
1413 pull_request_id=pull_request.pull_request_id))
1419 pull_request_id=pr_id))
1414
1420
1415 assert response.status_int == 200
1421 assert response.status_int == 200
1416 response.assert_response().element_contains(
1422 response.assert_response().element_contains(
@@ -18,8 +18,8 b''
18 # RhodeCode Enterprise Edition, including its added features, Support services,
18 # RhodeCode Enterprise Edition, including its added features, Support services,
19 # and proprietary license terms, please see https://rhodecode.com/licenses/
19 # and proprietary license terms, please see https://rhodecode.com/licenses/
20
20
21
22 import logging
21 import logging
22 import collections
23
23
24 from pyramid.httpexceptions import (
24 from pyramid.httpexceptions import (
25 HTTPNotFound, HTTPBadRequest, HTTPFound, HTTPForbidden, HTTPConflict)
25 HTTPNotFound, HTTPBadRequest, HTTPFound, HTTPForbidden, HTTPConflict)
@@ -34,14 +34,14 b' from rhodecode.apps.file_store.exception'
34 from rhodecode.lib import diffs, codeblocks
34 from rhodecode.lib import diffs, codeblocks
35 from rhodecode.lib.auth import (
35 from rhodecode.lib.auth import (
36 LoginRequired, HasRepoPermissionAnyDecorator, NotAnonymous, CSRFRequired)
36 LoginRequired, HasRepoPermissionAnyDecorator, NotAnonymous, CSRFRequired)
37
37 from rhodecode.lib.ext_json import json
38 from rhodecode.lib.compat import OrderedDict
38 from rhodecode.lib.compat import OrderedDict
39 from rhodecode.lib.diffs import (
39 from rhodecode.lib.diffs import (
40 cache_diff, load_cached_diff, diff_cache_exist, get_diff_context,
40 cache_diff, load_cached_diff, diff_cache_exist, get_diff_context,
41 get_diff_whitespace_flag)
41 get_diff_whitespace_flag)
42 from rhodecode.lib.exceptions import StatusChangeOnClosedPullRequestError, CommentVersionMismatch
42 from rhodecode.lib.exceptions import StatusChangeOnClosedPullRequestError, CommentVersionMismatch
43 import rhodecode.lib.helpers as h
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 from rhodecode.lib.vcs.backends.base import EmptyCommit
45 from rhodecode.lib.vcs.backends.base import EmptyCommit
46 from rhodecode.lib.vcs.exceptions import (
46 from rhodecode.lib.vcs.exceptions import (
47 RepositoryError, CommitDoesNotExistError)
47 RepositoryError, CommitDoesNotExistError)
@@ -115,6 +115,7 b' class RepoCommitsView(RepoAppView):'
115 except Exception:
115 except Exception:
116 log.exception("General failure")
116 log.exception("General failure")
117 raise HTTPNotFound()
117 raise HTTPNotFound()
118 single_commit = len(c.commit_ranges) == 1
118
119
119 c.changes = OrderedDict()
120 c.changes = OrderedDict()
120 c.lines_added = 0
121 c.lines_added = 0
@@ -128,23 +129,48 b' class RepoCommitsView(RepoAppView):'
128 c.inline_comments = []
129 c.inline_comments = []
129 c.files = []
130 c.files = []
130
131
131 c.statuses = []
132 c.comments = []
132 c.comments = []
133 c.unresolved_comments = []
133 c.unresolved_comments = []
134 c.resolved_comments = []
134 c.resolved_comments = []
135 if len(c.commit_ranges) == 1:
135
136 # Single commit
137 if single_commit:
136 commit = c.commit_ranges[0]
138 commit = c.commit_ranges[0]
137 c.comments = CommentsModel().get_comments(
139 c.comments = CommentsModel().get_comments(
138 self.db_repo.repo_id,
140 self.db_repo.repo_id,
139 revision=commit.raw_id)
141 revision=commit.raw_id)
140 c.statuses.append(ChangesetStatusModel().get_status(
142
141 self.db_repo.repo_id, commit.raw_id))
142 # comments from PR
143 # comments from PR
143 statuses = ChangesetStatusModel().get_statuses(
144 statuses = ChangesetStatusModel().get_statuses(
144 self.db_repo.repo_id, commit.raw_id,
145 self.db_repo.repo_id, commit.raw_id,
145 with_revisions=True)
146 with_revisions=True)
146 prs = set(st.pull_request for st in statuses
147
147 if st.pull_request is not None)
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 # from associated statuses, check the pull requests, and
174 # from associated statuses, check the pull requests, and
149 # show comments from them
175 # show comments from them
150 for pr in prs:
176 for pr in prs:
@@ -155,6 +181,37 b' class RepoCommitsView(RepoAppView):'
155 c.resolved_comments = CommentsModel()\
181 c.resolved_comments = CommentsModel()\
156 .get_commit_resolved_todos(commit.raw_id)
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 diff = None
215 diff = None
159 # Iterate over ranges (default commit view is always one commit)
216 # Iterate over ranges (default commit view is always one commit)
160 for commit in c.commit_ranges:
217 for commit in c.commit_ranges:
@@ -166,8 +223,8 b' class RepoCommitsView(RepoAppView):'
166 if method == 'show':
223 if method == 'show':
167 inline_comments = CommentsModel().get_inline_comments(
224 inline_comments = CommentsModel().get_inline_comments(
168 self.db_repo.repo_id, revision=commit.raw_id)
225 self.db_repo.repo_id, revision=commit.raw_id)
169 c.inline_cnt = CommentsModel().get_inline_comments_count(
226 c.inline_cnt = len(CommentsModel().get_inline_comments_as_list(
170 inline_comments)
227 inline_comments))
171 c.inline_comments = inline_comments
228 c.inline_comments = inline_comments
172
229
173 cache_path = self.rhodecode_vcs_repo.get_create_shadow_cache_pr_path(
230 cache_path = self.rhodecode_vcs_repo.get_create_shadow_cache_pr_path(
@@ -226,6 +283,7 b' class RepoCommitsView(RepoAppView):'
226
283
227 # sort comments by how they were generated
284 # sort comments by how they were generated
228 c.comments = sorted(c.comments, key=lambda x: x.comment_id)
285 c.comments = sorted(c.comments, key=lambda x: x.comment_id)
286 c.at_version_num = None
229
287
230 if len(c.commit_ranges) == 1:
288 if len(c.commit_ranges) == 1:
231 c.commit = c.commit_ranges[0]
289 c.commit = c.commit_ranges[0]
@@ -395,6 +453,7 b' class RepoCommitsView(RepoAppView):'
395 }
453 }
396 if comment:
454 if comment:
397 c.co = comment
455 c.co = comment
456 c.at_version_num = 0
398 rendered_comment = render(
457 rendered_comment = render(
399 'rhodecode:templates/changeset/changeset_comment_block.mako',
458 'rhodecode:templates/changeset/changeset_comment_block.mako',
400 self._get_template_context(c), self.request)
459 self._get_template_context(c), self.request)
@@ -427,7 +486,6 b' class RepoCommitsView(RepoAppView):'
427 return ''
486 return ''
428
487
429 @LoginRequired()
488 @LoginRequired()
430 @NotAnonymous()
431 @HasRepoPermissionAnyDecorator(
489 @HasRepoPermissionAnyDecorator(
432 'repository.read', 'repository.write', 'repository.admin')
490 'repository.read', 'repository.write', 'repository.admin')
433 @CSRFRequired()
491 @CSRFRequired()
@@ -39,7 +39,7 b' from rhodecode.lib.ext_json import json'
39 from rhodecode.lib.auth import (
39 from rhodecode.lib.auth import (
40 LoginRequired, HasRepoPermissionAny, HasRepoPermissionAnyDecorator,
40 LoginRequired, HasRepoPermissionAny, HasRepoPermissionAnyDecorator,
41 NotAnonymous, CSRFRequired)
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 from rhodecode.lib.vcs.backends.base import EmptyCommit, UpdateFailureReason
43 from rhodecode.lib.vcs.backends.base import EmptyCommit, UpdateFailureReason
44 from rhodecode.lib.vcs.exceptions import (
44 from rhodecode.lib.vcs.exceptions import (
45 CommitDoesNotExistError, RepositoryRequirementError, EmptyRepositoryError)
45 CommitDoesNotExistError, RepositoryRequirementError, EmptyRepositoryError)
@@ -265,6 +265,36 b' class RepoPullRequestsView(RepoAppView, '
265
265
266 return diffset
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 @LoginRequired()
298 @LoginRequired()
269 @HasRepoPermissionAnyDecorator(
299 @HasRepoPermissionAnyDecorator(
270 'repository.read', 'repository.write', 'repository.admin')
300 'repository.read', 'repository.write', 'repository.admin')
@@ -280,6 +310,8 b' class RepoPullRequestsView(RepoAppView, '
280 pull_request_id = pull_request.pull_request_id
310 pull_request_id = pull_request.pull_request_id
281
311
282 c.state_progressing = pull_request.is_state_changing()
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 _new_state = {
316 _new_state = {
285 'created': PullRequest.STATE_CREATED,
317 'created': PullRequest.STATE_CREATED,
@@ -300,22 +332,23 b' class RepoPullRequestsView(RepoAppView, '
300 from_version = self.request.GET.get('from_version') or version
332 from_version = self.request.GET.get('from_version') or version
301 merge_checks = self.request.GET.get('merge_checks')
333 merge_checks = self.request.GET.get('merge_checks')
302 c.fulldiff = str2bool(self.request.GET.get('fulldiff'))
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 # fetch global flags of ignore ws or context lines
338 # fetch global flags of ignore ws or context lines
305 diff_context = diffs.get_diff_context(self.request)
339 diff_context = diffs.get_diff_context(self.request)
306 hide_whitespace_changes = diffs.get_diff_whitespace_flag(self.request)
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 (pull_request_latest,
342 (pull_request_latest,
311 pull_request_at_ver,
343 pull_request_at_ver,
312 pull_request_display_obj,
344 pull_request_display_obj,
313 at_version) = PullRequestModel().get_pr_version(
345 at_version) = PullRequestModel().get_pr_version(
314 pull_request_id, version=version)
346 pull_request_id, version=version)
347
315 pr_closed = pull_request_latest.is_closed()
348 pr_closed = pull_request_latest.is_closed()
316
349
317 if pr_closed and (version or from_version):
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 raise HTTPFound(h.route_path(
352 raise HTTPFound(h.route_path(
320 'pullrequest_show', repo_name=self.db_repo_name,
353 'pullrequest_show', repo_name=self.db_repo_name,
321 pull_request_id=pull_request_id))
354 pull_request_id=pull_request_id))
@@ -323,13 +356,13 b' class RepoPullRequestsView(RepoAppView, '
323 versions = pull_request_display_obj.versions()
356 versions = pull_request_display_obj.versions()
324 # used to store per-commit range diffs
357 # used to store per-commit range diffs
325 c.changes = collections.OrderedDict()
358 c.changes = collections.OrderedDict()
326 c.range_diff_on = self.request.GET.get('range-diff') == "1"
327
359
328 c.at_version = at_version
360 c.at_version = at_version
329 c.at_version_num = (at_version
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 else None)
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 c.at_version_num, versions)
366 c.at_version_num, versions)
334
367
335 (prev_pull_request_latest,
368 (prev_pull_request_latest,
@@ -340,9 +373,9 b' class RepoPullRequestsView(RepoAppView, '
340
373
341 c.from_version = prev_at_version
374 c.from_version = prev_at_version
342 c.from_version_num = (prev_at_version
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 else None)
377 else None)
345 c.from_version_pos = ChangesetComment.get_index_from_version(
378 c.from_version_index = ChangesetComment.get_index_from_version(
346 c.from_version_num, versions)
379 c.from_version_num, versions)
347
380
348 # define if we're in COMPARE mode or VIEW at version mode
381 # define if we're in COMPARE mode or VIEW at version mode
@@ -351,16 +384,21 b' class RepoPullRequestsView(RepoAppView, '
351 # pull_requests repo_name we opened it against
384 # pull_requests repo_name we opened it against
352 # ie. target_repo must match
385 # ie. target_repo must match
353 if self.db_repo_name != pull_request_at_ver.target_repo.repo_name:
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 raise HTTPNotFound()
389 raise HTTPNotFound()
355
390
356 c.shadow_clone_url = PullRequestModel().get_shadow_clone_url(
391 c.shadow_clone_url = PullRequestModel().get_shadow_clone_url(pull_request_at_ver)
357 pull_request_at_ver)
358
392
359 c.pull_request = pull_request_display_obj
393 c.pull_request = pull_request_display_obj
360 c.renderer = pull_request_at_ver.description_renderer or c.renderer
394 c.renderer = pull_request_at_ver.description_renderer or c.renderer
361 c.pull_request_latest = pull_request_latest
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 c.allowed_to_change_status = False
402 c.allowed_to_change_status = False
365 c.allowed_to_update = False
403 c.allowed_to_update = False
366 c.allowed_to_merge = False
404 c.allowed_to_merge = False
@@ -389,12 +427,9 b' class RepoPullRequestsView(RepoAppView, '
389 'rules' in pull_request_latest.reviewer_data:
427 'rules' in pull_request_latest.reviewer_data:
390 rules = pull_request_latest.reviewer_data['rules'] or {}
428 rules = pull_request_latest.reviewer_data['rules'] or {}
391 try:
429 try:
392 c.forbid_adding_reviewers = rules.get(
430 c.forbid_adding_reviewers = rules.get('forbid_adding_reviewers')
393 'forbid_adding_reviewers')
431 c.forbid_author_to_review = rules.get('forbid_author_to_review')
394 c.forbid_author_to_review = rules.get(
432 c.forbid_commit_author_to_review = rules.get('forbid_commit_author_to_review')
395 'forbid_author_to_review')
396 c.forbid_commit_author_to_review = rules.get(
397 'forbid_commit_author_to_review')
398 except Exception:
433 except Exception:
399 pass
434 pass
400
435
@@ -419,41 +454,34 b' class RepoPullRequestsView(RepoAppView, '
419 'rhodecode:templates/pullrequests/pullrequest_merge_checks.mako'
454 'rhodecode:templates/pullrequests/pullrequest_merge_checks.mako'
420 return self._get_template_context(c)
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 # reviewers and statuses
459 # reviewers and statuses
425 c.pull_request_reviewers = pull_request_at_ver.reviewers_statuses()
460 c.pull_request_default_reviewers_data_json = json.dumps(pull_request.reviewer_data)
426 allowed_reviewers = [x[0].user_id for x in c.pull_request_reviewers]
461 c.pull_request_set_reviewers_data_json = collections.OrderedDict({'reviewers': []})
427
462
428 # GENERAL COMMENTS with versions #
463 for review_obj, member, reasons, mandatory, status in pull_request_at_ver.reviewers_statuses():
429 q = comments_model._all_general_comments_of_pull_request(pull_request_latest)
464 member_reviewer = h.reviewer_as_json(
430 q = q.order_by(ChangesetComment.comment_id.asc())
465 member, reasons=reasons, mandatory=mandatory,
431 general_comments = q
466 user_group=review_obj.rule_user_group_data()
467 )
432
468
433 # pick comments we want to render at current version
469 current_review_status = status[0][1].status if status else ChangesetStatus.STATUS_NOT_REVIEWED
434 c.comment_versions = comments_model.aggregate_comments(
470 member_reviewer['review_status'] = current_review_status
435 general_comments, versions, c.at_version_num)
471 member_reviewer['review_status_label'] = h.commit_status_lbl(current_review_status)
436 c.comments = c.comment_versions[c.at_version_num]['until']
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 #
475 c.pull_request_set_reviewers_data_json = json.dumps(c.pull_request_set_reviewers_data_json)
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
442
476
443 c.inline_versions = comments_model.aggregate_comments(
477 general_comments, inline_comments = \
444 inline_comments, versions, c.at_version_num, inline=True)
478 self.register_comments_vars(c, pull_request_latest, versions)
445
479
446 # TODOs
480 # TODOs
447 c.unresolved_comments = CommentsModel() \
481 c.unresolved_comments = CommentsModel() \
448 .get_pull_request_unresolved_todos(pull_request)
482 .get_pull_request_unresolved_todos(pull_request_latest)
449 c.resolved_comments = CommentsModel() \
483 c.resolved_comments = CommentsModel() \
450 .get_pull_request_resolved_todos(pull_request)
484 .get_pull_request_resolved_todos(pull_request_latest)
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]
457
485
458 # if we use version, then do not show later comments
486 # if we use version, then do not show later comments
459 # than current version
487 # than current version
@@ -520,8 +548,8 b' class RepoPullRequestsView(RepoAppView, '
520
548
521 # empty version means latest, so we keep this to prevent
549 # empty version means latest, so we keep this to prevent
522 # double caching
550 # double caching
523 version_normalized = version or 'latest'
551 version_normalized = version or PullRequest.LATEST_VER
524 from_version_normalized = from_version or 'latest'
552 from_version_normalized = from_version or PullRequest.LATEST_VER
525
553
526 cache_path = self.rhodecode_vcs_repo.get_create_shadow_cache_pr_path(target_repo)
554 cache_path = self.rhodecode_vcs_repo.get_create_shadow_cache_pr_path(target_repo)
527 cache_file_path = diff_cache_exist(
555 cache_file_path = diff_cache_exist(
@@ -613,7 +641,7 b' class RepoPullRequestsView(RepoAppView, '
613 diff_limit, file_limit, c.fulldiff,
641 diff_limit, file_limit, c.fulldiff,
614 hide_whitespace_changes, diff_context,
642 hide_whitespace_changes, diff_context,
615 use_ancestor=use_ancestor
643 use_ancestor=use_ancestor
616 )
644 )
617
645
618 # save cached diff
646 # save cached diff
619 if caching_enabled:
647 if caching_enabled:
@@ -717,7 +745,7 b' class RepoPullRequestsView(RepoAppView, '
717
745
718 # current user review statuses for each version
746 # current user review statuses for each version
719 c.review_versions = {}
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 for co in general_comments:
749 for co in general_comments:
722 if co.author.user_id == self._rhodecode_user.user_id:
750 if co.author.user_id == self._rhodecode_user.user_id:
723 status = co.status_change
751 status = co.status_change
@@ -937,6 +965,90 b' class RepoPullRequestsView(RepoAppView, '
937 @NotAnonymous()
965 @NotAnonymous()
938 @HasRepoPermissionAnyDecorator(
966 @HasRepoPermissionAnyDecorator(
939 'repository.read', 'repository.write', 'repository.admin')
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 @CSRFRequired()
1052 @CSRFRequired()
941 @view_config(
1053 @view_config(
942 route_name='pullrequest_create', request_method='POST',
1054 route_name='pullrequest_create', request_method='POST',
@@ -1098,7 +1210,7 b' class RepoPullRequestsView(RepoAppView, '
1098 self.request.matchdict['pull_request_id'])
1210 self.request.matchdict['pull_request_id'])
1099 _ = self.request.translate
1211 _ = self.request.translate
1100
1212
1101 self.load_default_context()
1213 c = self.load_default_context()
1102 redirect_url = None
1214 redirect_url = None
1103
1215
1104 if pull_request.is_closed():
1216 if pull_request.is_closed():
@@ -1109,6 +1221,8 b' class RepoPullRequestsView(RepoAppView, '
1109 'redirect_url': redirect_url}
1221 'redirect_url': redirect_url}
1110
1222
1111 is_state_changing = pull_request.is_state_changing()
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 # only owner or admin can update it
1227 # only owner or admin can update it
1114 allowed_to_update = PullRequestModel().check_user_update(
1228 allowed_to_update = PullRequestModel().check_user_update(
@@ -1132,7 +1246,7 b' class RepoPullRequestsView(RepoAppView, '
1132 return {'response': True,
1246 return {'response': True,
1133 'redirect_url': redirect_url}
1247 'redirect_url': redirect_url}
1134
1248
1135 self._update_commits(pull_request)
1249 self._update_commits(c, pull_request)
1136 if force_refresh:
1250 if force_refresh:
1137 redirect_url = h.route_path(
1251 redirect_url = h.route_path(
1138 'pullrequest_show', repo_name=self.db_repo_name,
1252 'pullrequest_show', repo_name=self.db_repo_name,
@@ -1168,7 +1282,7 b' class RepoPullRequestsView(RepoAppView, '
1168 h.flash(msg, category='success')
1282 h.flash(msg, category='success')
1169 return
1283 return
1170
1284
1171 def _update_commits(self, pull_request):
1285 def _update_commits(self, c, pull_request):
1172 _ = self.request.translate
1286 _ = self.request.translate
1173
1287
1174 with pull_request.set_state(PullRequest.STATE_UPDATING):
1288 with pull_request.set_state(PullRequest.STATE_UPDATING):
@@ -1196,13 +1310,18 b' class RepoPullRequestsView(RepoAppView, '
1196 change_source=changed)
1310 change_source=changed)
1197 h.flash(msg, category='success')
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 message = msg + (
1313 message = msg + (
1202 ' - <a onclick="window.location.reload()">'
1314 ' - <a onclick="window.location.reload()">'
1203 '<strong>{}</strong></a>'.format(_('Reload page')))
1315 '<strong>{}</strong></a>'.format(_('Reload page')))
1316
1317 message_obj = {
1318 'message': message,
1319 'level': 'success',
1320 'topic': '/notifications'
1321 }
1322
1204 channelstream.post_message(
1323 channelstream.post_message(
1205 channel, message, self._rhodecode_user.username,
1324 c.pr_broadcast_channel, message_obj, self._rhodecode_user.username,
1206 registry=self.request.registry)
1325 registry=self.request.registry)
1207 else:
1326 else:
1208 msg = PullRequestModel.UPDATE_STATUS_MESSAGES[resp.reason]
1327 msg = PullRequestModel.UPDATE_STATUS_MESSAGES[resp.reason]
@@ -1472,6 +1591,7 b' class RepoPullRequestsView(RepoAppView, '
1472 }
1591 }
1473 if comment:
1592 if comment:
1474 c.co = comment
1593 c.co = comment
1594 c.at_version_num = None
1475 rendered_comment = render(
1595 rendered_comment = render(
1476 'rhodecode:templates/changeset/changeset_comment_block.mako',
1596 'rhodecode:templates/changeset/changeset_comment_block.mako',
1477 self._get_template_context(c), self.request)
1597 self._get_template_context(c), self.request)
@@ -1890,7 +1890,7 b''
1890 "url": "http://spdx.org/licenses/BSD-4-Clause.html"
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 "license": [
1896 "license": [
@@ -53,7 +53,7 b' from rhodecode.lib.utils2 import aslist '
53 from rhodecode.lib.exc_tracking import store_exception
53 from rhodecode.lib.exc_tracking import store_exception
54 from rhodecode.subscribers import (
54 from rhodecode.subscribers import (
55 scan_repositories_if_enabled, write_js_routes_if_enabled,
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 log = logging.getLogger(__name__)
59 log = logging.getLogger(__name__)
@@ -316,6 +316,8 b' def includeme(config):'
316 pyramid.events.ApplicationCreated)
316 pyramid.events.ApplicationCreated)
317 config.add_subscriber(write_metadata_if_needed,
317 config.add_subscriber(write_metadata_if_needed,
318 pyramid.events.ApplicationCreated)
318 pyramid.events.ApplicationCreated)
319 config.add_subscriber(write_usage_data,
320 pyramid.events.ApplicationCreated)
319 config.add_subscriber(write_js_routes_if_enabled,
321 config.add_subscriber(write_js_routes_if_enabled,
320 pyramid.events.ApplicationCreated)
322 pyramid.events.ApplicationCreated)
321
323
@@ -145,7 +145,7 b' class PullRequestCommentEvent(PullReques'
145
145
146 status = None
146 status = None
147 if self.comment.status_change:
147 if self.comment.status_change:
148 status = self.comment.status_change[0].status
148 status = self.comment.review_status
149
149
150 data.update({
150 data.update({
151 'comment': {
151 'comment': {
@@ -184,7 +184,7 b' class PullRequestCommentEditEvent(PullRe'
184
184
185 status = None
185 status = None
186 if self.comment.status_change:
186 if self.comment.status_change:
187 status = self.comment.status_change[0].status
187 status = self.comment.review_status
188
188
189 data.update({
189 data.update({
190 'comment': {
190 'comment': {
@@ -37,8 +37,9 b' log = logging.getLogger(__name__)'
37
37
38 LOCK = ReadWriteMutex()
38 LOCK = ReadWriteMutex()
39
39
40 STATE_PUBLIC_KEYS = ['id', 'username', 'first_name', 'last_name',
40 USER_STATE_PUBLIC_KEYS = [
41 'icon_link', 'display_name', 'display_link']
41 'id', 'username', 'first_name', 'last_name',
42 'icon_link', 'display_name', 'display_link']
42
43
43
44
44 class ChannelstreamException(Exception):
45 class ChannelstreamException(Exception):
@@ -64,6 +65,8 b' def channelstream_request(config, payloa'
64 'x-channelstream-endpoint': endpoint,
65 'x-channelstream-endpoint': endpoint,
65 'Content-Type': 'application/json'}
66 'Content-Type': 'application/json'}
66 req_url = get_channelstream_server_url(config, endpoint)
67 req_url = get_channelstream_server_url(config, endpoint)
68
69 log.debug('Sending a channelstream request to endpoint: `%s`', req_url)
67 response = None
70 response = None
68 try:
71 try:
69 response = requests.post(req_url, data=json.dumps(payload),
72 response = requests.post(req_url, data=json.dumps(payload),
@@ -76,6 +79,7 b' def channelstream_request(config, payloa'
76 log.exception('Exception related to Channelstream happened')
79 log.exception('Exception related to Channelstream happened')
77 if raise_exc:
80 if raise_exc:
78 raise ChannelstreamConnectionException()
81 raise ChannelstreamConnectionException()
82 log.debug('Got channelstream response: %s', response)
79 return response
83 return response
80
84
81
85
@@ -154,7 +158,7 b' def parse_channels_info(info_result, inc'
154 for userinfo in info_result['users']:
158 for userinfo in info_result['users']:
155 user_state_dict[userinfo['user']] = {
159 user_state_dict[userinfo['user']] = {
156 k: v for k, v in userinfo['state'].items()
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 channels_info = {}
164 channels_info = {}
@@ -163,10 +167,10 b' def parse_channels_info(info_result, inc'
163 if c_name not in include_channel_info:
167 if c_name not in include_channel_info:
164 continue
168 continue
165 connected_list = []
169 connected_list = []
166 for userinfo in c_info['users']:
170 for username in c_info['users']:
167 connected_list.append({
171 connected_list.append({
168 'user': userinfo['user'],
172 'user': username,
169 'state': user_state_dict[userinfo['user']]
173 'state': user_state_dict[username]
170 })
174 })
171 channels_info[c_name] = {'users': connected_list,
175 channels_info[c_name] = {'users': connected_list,
172 'history': c_info['history']}
176 'history': c_info['history']}
@@ -230,6 +234,14 b' def get_connection_validators(registry):'
230
234
231 def post_message(channel, message, username, registry=None):
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 if not registry:
245 if not registry:
234 registry = get_current_registry()
246 registry = get_current_registry()
235
247
@@ -243,11 +255,7 b' def post_message(channel, message, usern'
243 'user': 'system',
255 'user': 'system',
244 'exclude_users': [username],
256 'exclude_users': [username],
245 'channel': channel,
257 'channel': channel,
246 'message': {
258 'message': message_obj
247 'message': message,
248 'level': 'success',
249 'topic': '/notifications'
250 }
251 }
259 }
252
260
253 try:
261 try:
@@ -90,7 +90,7 b' from rhodecode.lib.vcs.conf.settings imp'
90 from rhodecode.lib.index.search_utils import get_matching_line_offsets
90 from rhodecode.lib.index.search_utils import get_matching_line_offsets
91 from rhodecode.config.conf import DATE_FORMAT, DATETIME_FORMAT
91 from rhodecode.config.conf import DATE_FORMAT, DATETIME_FORMAT
92 from rhodecode.model.changeset_status import ChangesetStatusModel
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 from rhodecode.model.repo_group import RepoGroupModel
94 from rhodecode.model.repo_group import RepoGroupModel
95 from rhodecode.model.settings import IssueTrackerSettingsModel
95 from rhodecode.model.settings import IssueTrackerSettingsModel
96
96
@@ -810,8 +810,7 b' import tzlocal'
810 local_timezone = tzlocal.get_localzone()
810 local_timezone = tzlocal.get_localzone()
811
811
812
812
813 def age_component(datetime_iso, value=None, time_is_local=False, tooltip=True):
813 def get_timezone(datetime_iso, time_is_local=False):
814 title = value or format_date(datetime_iso)
815 tzinfo = '+00:00'
814 tzinfo = '+00:00'
816
815
817 # detect if we have a timezone info, otherwise, add it
816 # detect if we have a timezone info, otherwise, add it
@@ -822,6 +821,12 b' def age_component(datetime_iso, value=No'
822 timezone = force_timezone or local_timezone
821 timezone = force_timezone or local_timezone
823 offset = timezone.localize(datetime_iso).strftime('%z')
822 offset = timezone.localize(datetime_iso).strftime('%z')
824 tzinfo = '{}:{}'.format(offset[:-2], offset[-2:])
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 return literal(
831 return literal(
827 '<time class="timeago {cls}" title="{tt_title}" datetime="{dt}{tzinfo}">{title}</time>'.format(
832 '<time class="timeago {cls}" title="{tt_title}" datetime="{dt}{tzinfo}">{title}</time>'.format(
@@ -1357,20 +1362,76 b' class InitialsGravatar(object):'
1357 return "data:image/svg+xml;base64,%s" % base64.b64encode(img_data)
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 svg_type = None
1367 svg_type = None
1362 if email_address == User.DEFAULT_USER_EMAIL:
1368 if email_address == User.DEFAULT_USER_EMAIL:
1363 svg_type = 'default_user'
1369 svg_type = 'default_user'
1370
1364 klass = InitialsGravatar(email_address, first_name, last_name, size)
1371 klass = InitialsGravatar(email_address, first_name, last_name, size)
1365 return klass.generate_svg(svg_type=svg_type)
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:
1420 return klass.generate_svg(svg_type=svg_type)
1421
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))
1366
1430
1367
1431
1368 def gravatar_url(email_address, size=30, request=None):
1432 def gravatar_url(email_address, size=30, request=None):
1369 request = get_current_request()
1433 request = request or get_current_request()
1370 _use_gravatar = request.call_context.visual.use_gravatar
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 email_address = email_address or User.DEFAULT_USER_EMAIL
1436 email_address = email_address or User.DEFAULT_USER_EMAIL
1376 if isinstance(email_address, unicode):
1437 if isinstance(email_address, unicode):
@@ -1379,21 +1440,15 b' def gravatar_url(email_address, size=30,'
1379
1440
1380 # empty email or default user
1441 # empty email or default user
1381 if not email_address or email_address == User.DEFAULT_USER_EMAIL:
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 if _use_gravatar:
1445 if _use_gravatar:
1385 # TODO: Disuse pyramid thread locals. Think about another solution to
1446 gravatar_url_tmpl = request.call_context.visual.gravatar_url \
1386 # get the host and schema here.
1447 or User.DEFAULT_GRAVATAR_URL
1387 request = get_current_request()
1448 return gravatar_external(request, gravatar_url_tmpl, email_address, size=size)
1388 tmpl = safe_str(_gravatar_url)
1449
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
1395 else:
1450 else:
1396 return initials_gravatar(email_address, '', '', size=size)
1451 return initials_gravatar(request, email_address, '', '', size=size)
1397
1452
1398
1453
1399 def breadcrumb_repo_link(repo):
1454 def breadcrumb_repo_link(repo):
@@ -1560,7 +1615,7 b' def _process_url_func(match_obj, repo_na'
1560 # named regex variables
1615 # named regex variables
1561 named_vars.update(match_obj.groupdict())
1616 named_vars.update(match_obj.groupdict())
1562 _url = string.Template(entry['url']).safe_substitute(**named_vars)
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 hovercard_url = string.Template(entry.get('hovercard_url', '')).safe_substitute(**named_vars)
1619 hovercard_url = string.Template(entry.get('hovercard_url', '')).safe_substitute(**named_vars)
1565
1620
1566 def quote_cleaner(input_str):
1621 def quote_cleaner(input_str):
@@ -1600,17 +1655,18 b' def get_active_pattern_entries(repo_name'
1600
1655
1601 pr_pattern_re = re.compile(r'(?:(?:^!)|(?: !))(\d+)')
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 def process_patterns(text_string, repo_name, link_format='html', active_entries=None):
1662 def process_patterns(text_string, repo_name, link_format='html', active_entries=None):
1605
1663
1606 allowed_formats = ['html', 'rst', 'markdown',
1664 if link_format not in allowed_link_formats:
1607 'html+hovercard', 'rst+hovercard', 'markdown+hovercard']
1608 if link_format not in allowed_formats:
1609 raise ValueError('Link format can be only one of:{} got {}'.format(
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 if active_entries is None:
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 active_entries = get_active_pattern_entries(repo_name)
1670 active_entries = get_active_pattern_entries(repo_name)
1615
1671
1616 issues_data = []
1672 issues_data = []
@@ -1668,7 +1724,8 b' def process_patterns(text_string, repo_n'
1668 return new_text, issues_data
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 Parses given text message and makes proper links.
1730 Parses given text message and makes proper links.
1674 issues are linked to given issue-server, and rest is a commit link
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 new_text, issues = process_patterns(new_text, repository or '',
1748 new_text, issues = process_patterns(new_text, repository or '',
1692 active_entries=active_pattern_entries)
1749 active_entries=active_pattern_entries)
1693
1750
1751 if issues_container is not None:
1752 issues_container.extend(issues)
1753
1694 return literal(new_text)
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 def render(source, renderer='rst', mentions=False, relative_urls=None,
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 def maybe_convert_relative_links(html_source):
1796 def maybe_convert_relative_links(html_source):
1737 if relative_urls:
1797 if relative_urls:
@@ -1748,6 +1808,8 b" def render(source, renderer='rst', menti"
1748 source, issues = process_patterns(
1808 source, issues = process_patterns(
1749 source, repo_name, link_format='rst',
1809 source, repo_name, link_format='rst',
1750 active_entries=active_pattern_entries)
1810 active_entries=active_pattern_entries)
1811 if issues_container is not None:
1812 issues_container.extend(issues)
1751
1813
1752 return literal(
1814 return literal(
1753 '<div class="rst-block">%s</div>' %
1815 '<div class="rst-block">%s</div>' %
@@ -1760,6 +1822,8 b" def render(source, renderer='rst', menti"
1760 source, issues = process_patterns(
1822 source, issues = process_patterns(
1761 source, repo_name, link_format='markdown',
1823 source, repo_name, link_format='markdown',
1762 active_entries=active_pattern_entries)
1824 active_entries=active_pattern_entries)
1825 if issues_container is not None:
1826 issues_container.extend(issues)
1763
1827
1764 return literal(
1828 return literal(
1765 '<div class="markdown-block">%s</div>' %
1829 '<div class="markdown-block">%s</div>' %
@@ -139,6 +139,18 b' def is_vcs_call(environ):'
139 return False
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 def detect_vcs_request(environ, backends):
154 def detect_vcs_request(environ, backends):
143 checks = {
155 checks = {
144 'hg': (is_hg, SimpleHg),
156 'hg': (is_hg, SimpleHg),
@@ -146,6 +158,17 b' def detect_vcs_request(environ, backends'
146 'svn': (is_svn, SimpleSvn),
158 'svn': (is_svn, SimpleSvn),
147 }
159 }
148 handler = None
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 if VCS_TYPE_KEY in environ:
173 if VCS_TYPE_KEY in environ:
151 raw_type = environ[VCS_TYPE_KEY]
174 raw_type = environ[VCS_TYPE_KEY]
@@ -224,7 +224,10 b' class RedisAuthSessions(BaseAuthSessions'
224 data = client.get(key)
224 data = client.get(key)
225 if data:
225 if data:
226 json_data = pickle.loads(data)
226 json_data = pickle.loads(data)
227 accessed_time = json_data['_accessed_time']
227 try:
228 accessed_time = json_data['_accessed_time']
229 except KeyError:
230 accessed_time = 0
228 if accessed_time < expiry_time:
231 if accessed_time < expiry_time:
229 client.delete(key)
232 client.delete(key)
230 deleted_keys += 1
233 deleted_keys += 1
@@ -212,10 +212,10 b' class ChangesetStatusModel(BaseModel):'
212 # TODO(marcink): with group voting, how does rejected work,
212 # TODO(marcink): with group voting, how does rejected work,
213 # do we ever get rejected state ?
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 return ChangesetStatus.STATUS_APPROVED
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 return ChangesetStatus.STATUS_REJECTED
219 return ChangesetStatus.STATUS_REJECTED
220
220
221 return ChangesetStatus.STATUS_UNDER_REVIEW
221 return ChangesetStatus.STATUS_UNDER_REVIEW
@@ -354,34 +354,37 b' class ChangesetStatusModel(BaseModel):'
354 Session().add(new_status)
354 Session().add(new_status)
355 return new_statuses
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 def reviewers_statuses(self, pull_request):
381 def reviewers_statuses(self, pull_request):
358 _commit_statuses = self.get_statuses(
382 _commit_statuses = self.get_statuses(
359 pull_request.source_repo,
383 pull_request.source_repo,
360 pull_request=pull_request,
384 pull_request=pull_request,
361 with_revisions=True)
385 with_revisions=True)
362
386
363 commit_statuses = collections.defaultdict(list)
387 return self.aggregate_votes_by_user(_commit_statuses, pull_request.reviewers)
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
385
388
386 def calculated_review_status(self, pull_request, reviewers_statuses=None):
389 def calculated_review_status(self, pull_request, reviewers_statuses=None):
387 """
390 """
@@ -91,8 +91,7 b' class CommentsModel(BaseModel):'
91 # group by versions, and count until, and display objects
91 # group by versions, and count until, and display objects
92
92
93 comment_groups = collections.defaultdict(list)
93 comment_groups = collections.defaultdict(list)
94 [comment_groups[
94 [comment_groups[_co.pull_request_version_id].append(_co) for _co in comments]
95 _co.pull_request_version_id].append(_co) for _co in comments]
96
95
97 def yield_comments(pos):
96 def yield_comments(pos):
98 for co in comment_groups[pos]:
97 for co in comment_groups[pos]:
@@ -229,6 +228,14 b' class CommentsModel(BaseModel):'
229
228
230 return todos
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 def _log_audit_action(self, action, action_data, auth_user, comment):
239 def _log_audit_action(self, action, action_data, auth_user, comment):
233 audit_logger.store(
240 audit_logger.store(
234 action=action,
241 action=action,
@@ -456,38 +463,54 b' class CommentsModel(BaseModel):'
456 else:
463 else:
457 action = 'repo.commit.comment.create'
464 action = 'repo.commit.comment.create'
458
465
466 comment_id = comment.comment_id
459 comment_data = comment.get_api_data()
467 comment_data = comment.get_api_data()
468
460 self._log_audit_action(
469 self._log_audit_action(
461 action, {'data': comment_data}, auth_user, comment)
470 action, {'data': comment_data}, auth_user, comment)
462
471
463 msg_url = ''
464 channel = None
472 channel = None
465 if commit_obj:
473 if commit_obj:
466 msg_url = commit_comment_url
467 repo_name = repo.repo_name
474 repo_name = repo.repo_name
468 channel = u'/repo${}$/commit/{}'.format(
475 channel = u'/repo${}$/commit/{}'.format(
469 repo_name,
476 repo_name,
470 commit_obj.raw_id
477 commit_obj.raw_id
471 )
478 )
472 elif pull_request_obj:
479 elif pull_request_obj:
473 msg_url = pr_comment_url
474 repo_name = pr_target_repo.repo_name
480 repo_name = pr_target_repo.repo_name
475 channel = u'/repo${}$/pr/{}'.format(
481 channel = u'/repo${}$/pr/{}'.format(
476 repo_name,
482 repo_name,
477 pull_request_id
483 pull_request_obj.pull_request_id
478 )
484 )
479
485
480 message = '<strong>{}</strong> {} - ' \
486 if channel:
481 '<a onclick="window.location=\'{}\';' \
487 username = user.username
482 'window.location.reload()">' \
488 message = '<strong>{}</strong> {} #{}, {}'
483 '<strong>{}</strong></a>'
489 message = message.format(
484 message = message.format(
490 username,
485 user.username, _('made a comment'), msg_url,
491 _('posted a new comment'),
486 _('Show it now'))
492 comment_id,
493 _('Refresh the page to see new comments.'))
487
494
488 channelstream.post_message(
495 message_obj = {
489 channel, message, user.username,
496 'message': message,
490 registry=get_current_registry())
497 'level': 'success',
498 'topic': '/notifications'
499 }
500
501 channelstream.post_message(
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,
513 registry=get_current_registry())
491
514
492 return comment
515 return comment
493
516
@@ -641,16 +664,16 b' class CommentsModel(BaseModel):'
641 q = self._get_inline_comments_query(repo_id, revision, pull_request)
664 q = self._get_inline_comments_query(repo_id, revision, pull_request)
642 return self._group_comments_by_path_and_line_number(q)
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 version=None):
668 version=None):
646 inline_cnt = 0
669 inline_comms = []
647 for fname, per_line_comments in inline_comments.iteritems():
670 for fname, per_line_comments in inline_comments.iteritems():
648 for lno, comments in per_line_comments.iteritems():
671 for lno, comments in per_line_comments.iteritems():
649 for comm in comments:
672 for comm in comments:
650 if not comm.outdated_at_version(version) and skip_outdated:
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 def get_outdated_comments(self, repo_id, pull_request):
678 def get_outdated_comments(self, repo_id, pull_request):
656 # TODO: johbo: Remove `repo_id`, it is not needed to find the comments
679 # TODO: johbo: Remove `repo_id`, it is not needed to find the comments
@@ -3810,6 +3810,10 b' class ChangesetComment(Base, BaseModel):'
3810 return self.display_state == self.COMMENT_OUTDATED
3810 return self.display_state == self.COMMENT_OUTDATED
3811
3811
3812 @property
3812 @property
3813 def outdated_js(self):
3814 return json.dumps(self.display_state == self.COMMENT_OUTDATED)
3815
3816 @property
3813 def immutable(self):
3817 def immutable(self):
3814 return self.immutable_state == self.OP_IMMUTABLE
3818 return self.immutable_state == self.OP_IMMUTABLE
3815
3819
@@ -3817,16 +3821,35 b' class ChangesetComment(Base, BaseModel):'
3817 """
3821 """
3818 Checks if comment is outdated for given pull request version
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 def older_than_version(self, version):
3839 def older_than_version(self, version):
3823 """
3840 """
3824 Checks if comment is made from previous version than given
3841 Checks if comment is made from previous version than given
3825 """
3842 """
3826 if version is None:
3843 if version is None:
3827 return self.pull_request_version_id is not None
3844 return self.pull_request_version != version
3828
3845
3829 return self.pull_request_version_id < version
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 @property
3854 @property
3832 def commit_id(self):
3855 def commit_id(self):
@@ -3843,7 +3866,9 b' class ChangesetComment(Base, BaseModel):'
3843
3866
3844 @property
3867 @property
3845 def is_inline(self):
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 @property
3873 @property
3849 def last_version(self):
3874 def last_version(self):
@@ -3856,6 +3881,16 b' class ChangesetComment(Base, BaseModel):'
3856 return self.get_index_from_version(
3881 return self.get_index_from_version(
3857 self.pull_request_version_id, versions)
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 def __repr__(self):
3894 def __repr__(self):
3860 if self.comment_id:
3895 if self.comment_id:
3861 return '<DB:Comment #%s>' % self.comment_id
3896 return '<DB:Comment #%s>' % self.comment_id
@@ -4134,6 +4169,23 b' class _PullRequestBase(BaseModel):'
4134 return json.dumps(self.reviewer_data)
4169 return json.dumps(self.reviewer_data)
4135
4170
4136 @property
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 def work_in_progress(self):
4189 def work_in_progress(self):
4138 """checks if pull request is work in progress by checking the title"""
4190 """checks if pull request is work in progress by checking the title"""
4139 title = self.title.upper()
4191 title = self.title.upper()
@@ -4306,6 +4358,7 b' class PullRequest(Base, _PullRequestBase'
4306 __table_args__ = (
4358 __table_args__ = (
4307 base_table_args,
4359 base_table_args,
4308 )
4360 )
4361 LATEST_VER = 'latest'
4309
4362
4310 pull_request_id = Column(
4363 pull_request_id = Column(
4311 'pull_request_id', Integer(), nullable=False, primary_key=True)
4364 'pull_request_id', Integer(), nullable=False, primary_key=True)
@@ -4364,6 +4417,10 b' class PullRequest(Base, _PullRequestBase'
4364 def pull_request_version_id(self):
4417 def pull_request_version_id(self):
4365 return getattr(pull_request_obj, 'pull_request_version_id', None)
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 attrs = StrictAttributeDict(pull_request_obj.get_api_data(with_merge_state=False))
4424 attrs = StrictAttributeDict(pull_request_obj.get_api_data(with_merge_state=False))
4368
4425
4369 attrs.author = StrictAttributeDict(
4426 attrs.author = StrictAttributeDict(
@@ -4428,6 +4485,10 b' class PullRequest(Base, _PullRequestBase'
4428 """
4485 """
4429 return self.versions.count() + 1
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 class PullRequestVersion(Base, _PullRequestBase):
4493 class PullRequestVersion(Base, _PullRequestBase):
4433 __tablename__ = 'pull_request_versions'
4494 __tablename__ = 'pull_request_versions'
@@ -4475,6 +4536,8 b' class PullRequestReviewers(Base, BaseMod'
4475 __table_args__ = (
4536 __table_args__ = (
4476 base_table_args,
4537 base_table_args,
4477 )
4538 )
4539 ROLE_REVIEWER = u'reviewer'
4540 ROLE_OBSERVER = u'observer'
4478
4541
4479 @hybrid_property
4542 @hybrid_property
4480 def reasons(self):
4543 def reasons(self):
@@ -4502,6 +4565,8 b' class PullRequestReviewers(Base, BaseMod'
4502 JsonType('list', dialect_map=dict(mysql=UnicodeText(16384)))))
4565 JsonType('list', dialect_map=dict(mysql=UnicodeText(16384)))))
4503
4566
4504 mandatory = Column("mandatory", Boolean(), nullable=False, default=False)
4567 mandatory = Column("mandatory", Boolean(), nullable=False, default=False)
4568 role = Column('role', Unicode(255), nullable=True, default=ROLE_REVIEWER)
4569
4505 user = relationship('User')
4570 user = relationship('User')
4506 pull_request = relationship('PullRequest')
4571 pull_request = relationship('PullRequest')
4507
4572
@@ -5425,8 +5490,11 b' class FileStore(Base, BaseModel):'
5425 repo_group = relationship('RepoGroup', lazy='joined')
5490 repo_group = relationship('RepoGroup', lazy='joined')
5426
5491
5427 @classmethod
5492 @classmethod
5428 def get_by_store_uid(cls, file_store_uid):
5493 def get_by_store_uid(cls, file_store_uid, safe=False):
5429 return FileStore.query().filter(FileStore.file_uid == file_store_uid).scalar()
5494 if safe:
5495 return FileStore.query().filter(FileStore.file_uid == file_store_uid).first()
5496 else:
5497 return FileStore.query().filter(FileStore.file_uid == file_store_uid).scalar()
5430
5498
5431 @classmethod
5499 @classmethod
5432 def create(cls, file_uid, filename, file_hash, file_size, file_display_name='',
5500 def create(cls, file_uid, filename, file_hash, file_size, file_display_name='',
@@ -1600,7 +1600,7 b' class PullRequestModel(BaseModel):'
1600 'source_ref': pull_request.source_ref_parts,
1600 'source_ref': pull_request.source_ref_parts,
1601 }
1601 }
1602 if pull_request.last_merge_metadata:
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 if not possible and target_ref.type == 'branch':
1605 if not possible and target_ref.type == 'branch':
1606 # NOTE(marcink): case for mercurial multiple heads on branch
1606 # NOTE(marcink): case for mercurial multiple heads on branch
@@ -55,3 +55,16 b''
55 margin: 0 auto 35px auto;
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 .btn-group-actions {
255 .btn-group-actions {
256 position: relative;
256 position: relative;
257 z-index: 100;
257 z-index: 50;
258
258
259 &:not(.open) .btn-action-switcher-container {
259 &:not(.open) .btn-action-switcher-container {
260 display: none;
260 display: none;
@@ -1078,10 +1078,16 b' input.filediff-collapse-state {'
1078 background: @color5;
1078 background: @color5;
1079 color: white;
1079 color: white;
1080 }
1080 }
1081
1081 &[op="comments"] { /* comments on file */
1082 &[op="comments"] { /* comments on file */
1082 background: @grey4;
1083 background: @grey4;
1083 color: white;
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 clear: both;
31 clear: both;
32 }
32 }
33
33
34 .display-none {
35 display: none;
36 }
37
34 .pull-right {
38 .pull-right {
35 float: right !important;
39 float: right !important;
36 }
40 }
@@ -240,14 +240,14 b' div.markdown-block ol {'
240 div.markdown-block ul.checkbox li,
240 div.markdown-block ul.checkbox li,
241 div.markdown-block ol.checkbox li {
241 div.markdown-block ol.checkbox li {
242 list-style: none !important;
242 list-style: none !important;
243 margin: 6px !important;
243 margin: 0px !important;
244 padding: 0 !important;
244 padding: 0 !important;
245 }
245 }
246
246
247 div.markdown-block ul li,
247 div.markdown-block ul li,
248 div.markdown-block ol li {
248 div.markdown-block ol li {
249 list-style: disc !important;
249 list-style: disc !important;
250 margin: 6px !important;
250 margin: 0px !important;
251 padding: 0 !important;
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 .action-link{
91 .action-link{
87 margin-left: @padding;
92 margin-left: @padding;
88 padding-left: @padding;
93 padding-left: @padding;
@@ -482,10 +487,15 b' ul.auth_plugins {'
482 text-align: left;
487 text-align: left;
483 overflow: hidden;
488 overflow: hidden;
484 white-space: pre-line;
489 white-space: pre-line;
485 }
490 padding-top: 5px
486
491 }
487 .pr-details-title {
492
488 height: 16px
493 #add_reviewer {
494 padding-top: 10px;
495 }
496
497 #add_reviewer_input {
498 padding-top: 10px
489 }
499 }
490
500
491 .pr-details-title-author-pref {
501 .pr-details-title-author-pref {
@@ -1173,9 +1183,12 b' label {'
1173 a {
1183 a {
1174 color: @grey5
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 display: none;
1189 display: none;
1178 }
1190 }
1191
1179 }
1192 }
1180
1193
1181 img {
1194 img {
@@ -1492,26 +1505,17 b' table.integrations {'
1492
1505
1493 // Pull Requests
1506 // Pull Requests
1494 .summary-details {
1507 .summary-details {
1495 width: 72%;
1508 width: 100%;
1496 }
1509 }
1497 .pr-summary {
1510 .pr-summary {
1498 border-bottom: @border-thickness solid @grey5;
1511 border-bottom: @border-thickness solid @grey5;
1499 margin-bottom: @space;
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 .reviewers {
1515 .reviewers {
1512 width: 25%;
1516 width: 98%;
1513 min-width: 200px;
1517 }
1514 }
1518
1515 .reviewers ul li {
1519 .reviewers ul li {
1516 position: relative;
1520 position: relative;
1517 width: 100%;
1521 width: 100%;
@@ -1523,18 +1527,14 b' table.integrations {'
1523 min-height: 55px;
1527 min-height: 55px;
1524 }
1528 }
1525
1529
1526 .reviewers_member {
1527 width: 100%;
1528 overflow: auto;
1529 }
1530 .reviewer_reason {
1530 .reviewer_reason {
1531 padding-left: 20px;
1531 padding-left: 20px;
1532 line-height: 1.5em;
1532 line-height: 1.5em;
1533 }
1533 }
1534 .reviewer_status {
1534 .reviewer_status {
1535 display: inline-block;
1535 display: inline-block;
1536 width: 25px;
1536 width: 20px;
1537 min-width: 25px;
1537 min-width: 20px;
1538 height: 1.2em;
1538 height: 1.2em;
1539 line-height: 1em;
1539 line-height: 1em;
1540 }
1540 }
@@ -1557,25 +1557,20 b' table.integrations {'
1557 }
1557 }
1558
1558
1559 .reviewer_member_mandatory {
1559 .reviewer_member_mandatory {
1560 position: absolute;
1561 left: 15px;
1562 top: 8px;
1563 width: 16px;
1560 width: 16px;
1564 font-size: 11px;
1561 font-size: 11px;
1565 margin: 0;
1562 margin: 0;
1566 padding: 0;
1563 padding: 0;
1567 color: black;
1564 color: black;
1565 opacity: 0.4;
1568 }
1566 }
1569
1567
1570 .reviewer_member_mandatory_remove,
1568 .reviewer_member_mandatory_remove,
1571 .reviewer_member_remove {
1569 .reviewer_member_remove {
1572 position: absolute;
1573 right: 0;
1574 top: 0;
1575 width: 16px;
1570 width: 16px;
1576 margin-bottom: 10px;
1577 padding: 0;
1571 padding: 0;
1578 color: black;
1572 color: black;
1573 cursor: pointer;
1579 }
1574 }
1580
1575
1581 .reviewer_member_mandatory_remove {
1576 .reviewer_member_mandatory_remove {
@@ -1593,6 +1588,9 b' table.integrations {'
1593 cursor: pointer;
1588 cursor: pointer;
1594 }
1589 }
1595 .pr-details-title {
1590 .pr-details-title {
1591 height: 20px;
1592 line-height: 20px;
1593
1596 padding-bottom: 8px;
1594 padding-bottom: 8px;
1597 border-bottom: @border-thickness solid @grey5;
1595 border-bottom: @border-thickness solid @grey5;
1598
1596
@@ -1617,7 +1615,7 b' table.integrations {'
1617 text-decoration: line-through;
1615 text-decoration: line-through;
1618 }
1616 }
1619
1617
1620 .todo-table {
1618 .todo-table, .comments-table {
1621 width: 100%;
1619 width: 100%;
1622
1620
1623 td {
1621 td {
@@ -1627,7 +1625,8 b' table.integrations {'
1627 .td-todo-number {
1625 .td-todo-number {
1628 text-align: left;
1626 text-align: left;
1629 white-space: nowrap;
1627 white-space: nowrap;
1630 width: 15%;
1628 width: 1%;
1629 padding-right: 2px;
1631 }
1630 }
1632
1631
1633 .td-todo-gravatar {
1632 .td-todo-gravatar {
@@ -1651,10 +1650,13 b' table.integrations {'
1651 text-overflow: ellipsis;
1650 text-overflow: ellipsis;
1652 }
1651 }
1653
1652
1653 table.group_members {
1654 width: 100%
1655 }
1656
1654 .group_members {
1657 .group_members {
1655 margin-top: 0;
1658 margin-top: 0;
1656 padding: 0;
1659 padding: 0;
1657 list-style: outside none none;
1658
1660
1659 img {
1661 img {
1660 height: @gravatar-size;
1662 height: @gravatar-size;
@@ -1698,7 +1700,7 b' table.integrations {'
1698 }
1700 }
1699
1701
1700 .reviewer_ac .ac-input {
1702 .reviewer_ac .ac-input {
1701 width: 92%;
1703 width: 100%;
1702 margin-bottom: 1em;
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 #help_kb .modal-content{
2776 #help_kb .modal-content{
2775 max-width: 750px;
2777 max-width: 800px;
2776 margin: 10% auto;
2778 margin: 10% auto;
2777
2779
2778 table{
2780 table{
@@ -3069,4 +3071,141 b' form.markup-form {'
3069
3071
3070 .pr-hovercard-title {
3072 .pr-hovercard-title {
3071 padding-top: 5px;
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 &.main_filter_input {
791 &.main_filter_input {
792 padding: 5px 10px;
792 padding: 5px 10px;
793 min-width: 340px;
793
794 color: @grey7;
794 color: @grey7;
795 background: @black;
795 background: @black;
796 min-height: 18px;
796 min-height: 18px;
@@ -800,11 +800,34 b' input {'
800 color: @grey2 !important;
800 color: @grey2 !important;
801 background: white !important;
801 background: white !important;
802 }
802 }
803
803 &:focus {
804 &:focus {
804 color: @grey2 !important;
805 color: @grey2 !important;
805 background: white !important;
806 background: white !important;
806 }
807 }
808
809 min-width: 360px;
810
811 @media screen and (max-width: 1600px) {
812 min-width: 300px;
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 }
807 }
829 }
830
808 }
831 }
809
832
810
833
@@ -168,6 +168,7 b''
168 .icon-remove:before { content: '\e810'; } /* '' */
168 .icon-remove:before { content: '\e810'; } /* '' */
169 .icon-fork:before { content: '\e811'; } /* 'ξ ‘' */
169 .icon-fork:before { content: '\e811'; } /* 'ξ ‘' */
170 .icon-more:before { content: '\e812'; } /* 'ξ ’' */
170 .icon-more:before { content: '\e812'; } /* 'ξ ’' */
171 .icon-options:before { content: '\e812'; } /* 'ξ ’' */
171 .icon-search:before { content: '\e813'; } /* 'ξ “' */
172 .icon-search:before { content: '\e813'; } /* 'ξ “' */
172 .icon-scissors:before { content: '\e814'; } /* 'ξ ”' */
173 .icon-scissors:before { content: '\e814'; } /* 'ξ ”' */
173 .icon-download:before { content: '\e815'; } /* 'ξ •' */
174 .icon-download:before { content: '\e815'; } /* 'ξ •' */
@@ -251,6 +252,7 b''
251 // TRANSFORM
252 // TRANSFORM
252 .icon-merge:before {transform: rotate(180deg);}
253 .icon-merge:before {transform: rotate(180deg);}
253 .icon-wide-mode:before {transform: rotate(90deg);}
254 .icon-wide-mode:before {transform: rotate(90deg);}
255 .icon-options:before {transform: rotate(90deg);}
254
256
255 // -- END ICON CLASSES -- //
257 // -- END ICON CLASSES -- //
256
258
@@ -131,6 +131,11 b' function setRCMouseBindings(repoName, re'
131 window.location = pyroutes.url(
131 window.location = pyroutes.url(
132 'edit_repo_perms', {'repo_name': repoName});
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 pyroutes.register('pullrequest_comment_create', '/%(repo_name)s/pull-request/%(pull_request_id)s/comment', ['repo_name', 'pull_request_id']);
246 pyroutes.register('pullrequest_comment_create', '/%(repo_name)s/pull-request/%(pull_request_id)s/comment', ['repo_name', 'pull_request_id']);
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']);
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 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']);
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 pyroutes.register('edit_repo', '/%(repo_name)s/settings', ['repo_name']);
251 pyroutes.register('edit_repo', '/%(repo_name)s/settings', ['repo_name']);
250 pyroutes.register('edit_repo_advanced', '/%(repo_name)s/settings/advanced', ['repo_name']);
252 pyroutes.register('edit_repo_advanced', '/%(repo_name)s/settings/advanced', ['repo_name']);
251 pyroutes.register('edit_repo_advanced_archive', '/%(repo_name)s/settings/advanced/archive', ['repo_name']);
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 super.connectedCallback();
28 super.connectedCallback();
29 ccLog.debug('rhodeCodeApp created');
29 ccLog.debug('rhodeCodeApp created');
30 $.Topic('/notifications').subscribe(this.handleNotifications.bind(this));
30 $.Topic('/notifications').subscribe(this.handleNotifications.bind(this));
31 $.Topic('/comment').subscribe(this.handleComment.bind(this));
31 $.Topic('/favicon/update').subscribe(this.faviconUpdate.bind(this));
32 $.Topic('/favicon/update').subscribe(this.faviconUpdate.bind(this));
32 $.Topic('/connection_controller/subscribe').subscribe(
33 $.Topic('/connection_controller/subscribe').subscribe(
33 this.subscribeToChannelTopic.bind(this));
34 this.subscribeToChannelTopic.bind(this)
35 );
36
34 // this event can be used to coordinate plugins to do their
37 // this event can be used to coordinate plugins to do their
35 // initialization before channelstream is kicked off
38 // initialization before channelstream is kicked off
36 $.Topic('/__MAIN_APP__').publish({});
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 faviconUpdate(data) {
85 faviconUpdate(data) {
75 this.shadowRoot.querySelector('rhodecode-favicon').counter = data.count;
86 this.shadowRoot.querySelector('rhodecode-favicon').counter = data.count;
76 }
87 }
@@ -95,6 +106,7 b' export class RhodecodeApp extends Polyme'
95 }
106 }
96 // append any additional channels registered in other plugins
107 // append any additional channels registered in other plugins
97 $.Topic('/connection_controller/subscribe').processPrepared();
108 $.Topic('/connection_controller/subscribe').processPrepared();
109
98 channelstreamConnection.connect();
110 channelstreamConnection.connect();
99 }
111 }
100 }
112 }
@@ -157,8 +169,7 b' export class RhodecodeApp extends Polyme'
157
169
158 handleConnected(event) {
170 handleConnected(event) {
159 var channelstreamConnection = this.getChannelStreamConnection();
171 var channelstreamConnection = this.getChannelStreamConnection();
160 channelstreamConnection.set('channelsState',
172 channelstreamConnection.set('channelsState', event.detail.channels_info);
161 event.detail.channels_info);
162 channelstreamConnection.set('userState', event.detail.state);
173 channelstreamConnection.set('userState', event.detail.state);
163 channelstreamConnection.set('channels', event.detail.channels);
174 channelstreamConnection.set('channels', event.detail.channels);
164 this.propagageChannelsState();
175 this.propagageChannelsState();
@@ -296,16 +296,25 b' var tooltipActivate = function () {'
296 // we set a variable so the data is only loaded once via Ajax, not every time the tooltip opens
296 // we set a variable so the data is only loaded once via Ajax, not every time the tooltip opens
297 if ($origin.data('loaded') !== true) {
297 if ($origin.data('loaded') !== true) {
298 var hovercardUrl = $origin.data('hovercardUrl');
298 var hovercardUrl = $origin.data('hovercardUrl');
299 var altHovercard =$origin.data('hovercardAlt');
299 var altHovercard = $origin.data('hovercardAlt');
300
300
301 if (hovercardUrl !== undefined && hovercardUrl !== "") {
301 if (hovercardUrl !== undefined && hovercardUrl !== "") {
302 if (hovercardUrl.substr(0,12) === 'pyroutes.url'){
302 var urlLoad = true;
303 if (hovercardUrl.substr(0, 12) === 'pyroutes.url') {
303 hovercardUrl = eval(hovercardUrl)
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
306 var loaded = loadHoverCard(hovercardUrl, altHovercard, function (data) {
312 if (urlLoad) {
307 instance.content(data);
313 var loaded = loadHoverCard(hovercardUrl, altHovercard, function (data) {
308 })
314 instance.content(data);
315 })
316 }
317
309 } else {
318 } else {
310 if ($origin.data('hovercardAltHtml')) {
319 if ($origin.data('hovercardAltHtml')) {
311 var data = atob($origin.data('hovercardAltHtml'));
320 var data = atob($origin.data('hovercardAltHtml'));
@@ -677,7 +686,9 b' var feedLifetimeOptions = function(query'
677 query.callback(data);
686 query.callback(data);
678 };
687 };
679
688
680
689 /*
690 * Retrievew via templateContext.session_attrs.key
691 * */
681 var storeUserSessionAttr = function (key, val) {
692 var storeUserSessionAttr = function (key, val) {
682
693
683 var postData = {
694 var postData = {
@@ -558,7 +558,7 b' var CommentsController = function() {'
558 return false;
558 return false;
559 };
559 };
560
560
561 this.showVersion = function (comment_id, comment_history_id) {
561 this.showVersion = function (comment_id, comment_history_id) {
562
562
563 var historyViewUrl = pyroutes.url(
563 var historyViewUrl = pyroutes.url(
564 'repo_commit_comment_history_view',
564 'repo_commit_comment_history_view',
@@ -585,7 +585,7 b' var CommentsController = function() {'
585 successRenderCommit,
585 successRenderCommit,
586 failRenderCommit
586 failRenderCommit
587 );
587 );
588 };
588 };
589
589
590 this.getLineNumber = function(node) {
590 this.getLineNumber = function(node) {
591 var $node = $(node);
591 var $node = $(node);
@@ -670,8 +670,20 b' var CommentsController = function() {'
670
670
671 var success = function(response) {
671 var success = function(response) {
672 $comment.remove();
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 return false;
684 return false;
674 };
685 };
686
675 var failure = function(jqXHR, textStatus, errorThrown) {
687 var failure = function(jqXHR, textStatus, errorThrown) {
676 var prefix = "Error while deleting this comment.\n"
688 var prefix = "Error while deleting this comment.\n"
677 var message = formatErrorMessage(jqXHR, textStatus, errorThrown, prefix);
689 var message = formatErrorMessage(jqXHR, textStatus, errorThrown, prefix);
@@ -682,6 +694,9 b' var CommentsController = function() {'
682 return false;
694 return false;
683 };
695 };
684 ajaxPOST(url, postData, success, failure);
696 ajaxPOST(url, postData, success, failure);
697
698
699
685 }
700 }
686
701
687 this.deleteComment = function(node) {
702 this.deleteComment = function(node) {
@@ -727,6 +742,15 b' var CommentsController = function() {'
727 $filediff.find('.hide-line-comments').removeClass('hide-line-comments');
742 $filediff.find('.hide-line-comments').removeClass('hide-line-comments');
728 $filediff.toggleClass('hide-comments');
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 return false;
754 return false;
731 };
755 };
732
756
@@ -747,7 +771,7 b' var CommentsController = function() {'
747 var cm = commentForm.getCmInstance();
771 var cm = commentForm.getCmInstance();
748
772
749 if (resolvesCommentId){
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 setTimeout(function() {
777 setTimeout(function() {
@@ -1077,9 +1101,15 b' var CommentsController = function() {'
1077 updateSticky()
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 commentForm.setActionButtonsDisabled(false);
1109 commentForm.setActionButtonsDisabled(false);
1081
1110
1082 };
1111 };
1112
1083 var submitFailCallback = function(jqXHR, textStatus, errorThrown) {
1113 var submitFailCallback = function(jqXHR, textStatus, errorThrown) {
1084 var prefix = "Error while editing comment.\n"
1114 var prefix = "Error while editing comment.\n"
1085 var message = formatErrorMessage(jqXHR, textStatus, errorThrown, prefix);
1115 var message = formatErrorMessage(jqXHR, textStatus, errorThrown, prefix);
@@ -1209,6 +1239,11 b' var CommentsController = function() {'
1209 updateSticky()
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 commentForm.setActionButtonsDisabled(false);
1247 commentForm.setActionButtonsDisabled(false);
1213
1248
1214 };
1249 };
@@ -35,4 +35,75 b' var quick_repo_menu = function() {'
35 }, function() {
35 }, function() {
36 hide_quick_repo_menus();
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 var self = this;
98 var self = this;
99 this.$reviewRulesContainer = $('#review_rules');
99 this.$reviewRulesContainer = $('#review_rules');
100 this.$rulesList = this.$reviewRulesContainer.find('.pr-reviewer-rules');
100 this.$rulesList = this.$reviewRulesContainer.find('.pr-reviewer-rules');
101 this.$userRule = $('.pr-user-rule-container');
101 this.forbidReviewUsers = undefined;
102 this.forbidReviewUsers = undefined;
102 this.$reviewMembers = $('#review_members');
103 this.$reviewMembers = $('#review_members');
103 this.currentRequest = null;
104 this.currentRequest = null;
104 this.diffData = null;
105 this.diffData = null;
106 this.enabledRules = [];
107
105 //dummy handler, we might register our own later
108 //dummy handler, we might register our own later
106 this.diffDataHandler = function(data){};
109 this.diffDataHandler = function(data){};
107
110
@@ -116,14 +119,17 b' ReviewersController = function () {'
116
119
117 this.hideReviewRules = function () {
120 this.hideReviewRules = function () {
118 self.$reviewRulesContainer.hide();
121 self.$reviewRulesContainer.hide();
122 $(self.$userRule.selector).hide();
119 };
123 };
120
124
121 this.showReviewRules = function () {
125 this.showReviewRules = function () {
122 self.$reviewRulesContainer.show();
126 self.$reviewRulesContainer.show();
127 $(self.$userRule.selector).show();
123 };
128 };
124
129
125 this.addRule = function (ruleText) {
130 this.addRule = function (ruleText) {
126 self.showReviewRules();
131 self.showReviewRules();
132 self.enabledRules.push(ruleText);
127 return '<div>- {0}</div>'.format(ruleText)
133 return '<div>- {0}</div>'.format(ruleText)
128 };
134 };
129
135
@@ -179,6 +185,7 b' ReviewersController = function () {'
179 _gettext('Reviewers picked from source code changes.'))
185 _gettext('Reviewers picked from source code changes.'))
180 )
186 )
181 }
187 }
188
182 if (data.rules.forbid_adding_reviewers) {
189 if (data.rules.forbid_adding_reviewers) {
183 $('#add_reviewer_input').remove();
190 $('#add_reviewer_input').remove();
184 self.$rulesList.append(
191 self.$rulesList.append(
@@ -186,6 +193,7 b' ReviewersController = function () {'
186 _gettext('Adding new reviewers is forbidden.'))
193 _gettext('Adding new reviewers is forbidden.'))
187 )
194 )
188 }
195 }
196
189 if (data.rules.forbid_author_to_review) {
197 if (data.rules.forbid_author_to_review) {
190 self.forbidReviewUsers.push(data.rules_data.pr_author);
198 self.forbidReviewUsers.push(data.rules_data.pr_author);
191 self.$rulesList.append(
199 self.$rulesList.append(
@@ -193,6 +201,7 b' ReviewersController = function () {'
193 _gettext('Author is not allowed to be a reviewer.'))
201 _gettext('Author is not allowed to be a reviewer.'))
194 )
202 )
195 }
203 }
204
196 if (data.rules.forbid_commit_author_to_review) {
205 if (data.rules.forbid_commit_author_to_review) {
197
206
198 if (data.rules_data.forbidden_users) {
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 return self.forbidReviewUsers
226 return self.forbidReviewUsers
212 };
227 };
213
228
@@ -264,8 +279,11 b' ReviewersController = function () {'
264 $('#user').show(); // show user autocomplete after load
279 $('#user').show(); // show user autocomplete after load
265
280
266 var commitElements = data["diff_info"]['commits'];
281 var commitElements = data["diff_info"]['commits'];
282
267 if (commitElements.length === 0) {
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 } else {
288 } else {
271 // un-lock PR button, so we cannot send PR before it's calculated
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 this.addReviewMember = function (reviewer_obj, reasons, mandatory) {
329 this.addReviewMember = function (reviewer_obj, reasons, mandatory) {
312 var members = self.$reviewMembers.get(0);
313 var id = reviewer_obj.user_id;
330 var id = reviewer_obj.user_id;
314 var username = reviewer_obj.username;
331 var username = reviewer_obj.username;
315
332
@@ -318,10 +335,10 b' ReviewersController = function () {'
318
335
319 // register IDS to check if we don't have this ID already in
336 // register IDS to check if we don't have this ID already in
320 var currentIds = [];
337 var currentIds = [];
321 var _els = self.$reviewMembers.find('li').toArray();
338
322 for (el in _els) {
339 $.each(self.$reviewMembers.find('.reviewer_entry'), function (index, value) {
323 currentIds.push(_els[el].id)
340 currentIds.push($(value).data('reviewerUserId'))
324 }
341 })
325
342
326 var userAllowedReview = function (userId) {
343 var userAllowedReview = function (userId) {
327 var allowed = true;
344 var allowed = true;
@@ -339,20 +356,23 b' ReviewersController = function () {'
339 alert(_gettext('User `{0}` not allowed to be a reviewer').format(username));
356 alert(_gettext('User `{0}` not allowed to be a reviewer').format(username));
340 } else {
357 } else {
341 // only add if it's not there
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 if (alreadyReviewer) {
361 if (alreadyReviewer) {
345 alert(_gettext('User `{0}` already in reviewers').format(username));
362 alert(_gettext('User `{0}` already in reviewers').format(username));
346 } else {
363 } else {
347 members.innerHTML += renderTemplate('reviewMemberEntry', {
364 var reviewerEntry = renderTemplate('reviewMemberEntry', {
348 'member': reviewer_obj,
365 'member': reviewer_obj,
349 'mandatory': mandatory,
366 'mandatory': mandatory,
367 'reasons': reasons,
350 'allowed_to_update': true,
368 'allowed_to_update': true,
351 'review_status': 'not_reviewed',
369 'review_status': 'not_reviewed',
352 'review_status_label': _gettext('Not Reviewed'),
370 'review_status_label': _gettext('Not Reviewed'),
353 'reasons': reasons,
371 'user_group': reviewer_obj.user_group,
354 'create': true
372 'create': true,
355 });
373 'rule_show': true,
374 })
375 $(self.$reviewMembers.selector).append(reviewerEntry);
356 tooltipActivate();
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 var self = this;
500 var self = this;
481 this.$verSource = $('input[name=ver_source]');
501 this.$verSource = $('input[name=ver_source]');
482 this.$verTarget = $('input[name=ver_target]');
502 this.$verTarget = $('input[name=ver_target]');
@@ -596,25 +616,10 b' VersionController = function () {'
596 return false
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 var self = this;
623 var self = this;
619 this.$updateCommits = $('#update_commits');
624 this.$updateCommits = $('#update_commits');
620 this.$updateCommitsSwitcher = $('#update_commits_switcher');
625 this.$updateCommitsSwitcher = $('#update_commits_switcher');
@@ -656,4 +661,230 b' UpdatePrController = function () {'
656 templateContext.repo_name,
661 templateContext.repo_name,
657 templateContext.pull_request_data.pull_request_id, force);
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 # RhodeCode Enterprise Edition, including its added features, Support services,
18 # RhodeCode Enterprise Edition, including its added features, Support services,
19 # and proprietary license terms, please see https://rhodecode.com/licenses/
19 # and proprietary license terms, please see https://rhodecode.com/licenses/
20 import io
20 import io
21 import math
21 import re
22 import re
22 import os
23 import os
23 import datetime
24 import datetime
@@ -196,6 +197,72 b' def write_metadata_if_needed(event):'
196 pass
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 def write_js_routes_if_enabled(event):
266 def write_js_routes_if_enabled(event):
200 registry = event.app.registry
267 registry = event.app.registry
201
268
@@ -38,10 +38,12 b''
38 <div class="main">
38 <div class="main">
39 ${next.main()}
39 ${next.main()}
40 </div>
40 </div>
41
41 </div>
42 </div>
42 <!-- END CONTENT -->
43 <!-- END CONTENT -->
43
44
44 </div>
45 </div>
46
45 <!-- FOOTER -->
47 <!-- FOOTER -->
46 <div id="footer">
48 <div id="footer">
47 <div id="footer-inner" class="title wrapper">
49 <div id="footer-inner" class="title wrapper">
@@ -699,9 +701,6 b''
699 notice_messages, notice_level = c.rhodecode_user.get_notice_messages()
701 notice_messages, notice_level = c.rhodecode_user.get_notice_messages()
700 notice_display = 'none' if len(notice_messages) == 0 else ''
702 notice_display = 'none' if len(notice_messages) == 0 else ''
701 %>
703 %>
702 <style>
703
704 </style>
705
704
706 <ul id="quick" class="main_nav navigation horizontal-list">
705 <ul id="quick" class="main_nav navigation horizontal-list">
707 ## notice box for important system messages
706 ## notice box for important system messages
@@ -1200,6 +1199,7 b''
1200 ('g p', 'Goto pull requests page'),
1199 ('g p', 'Goto pull requests page'),
1201 ('g o', 'Goto repository settings'),
1200 ('g o', 'Goto repository settings'),
1202 ('g O', 'Goto repository access permissions settings'),
1201 ('g O', 'Goto repository access permissions settings'),
1202 ('t s', 'Toggle sidebar on some pages'),
1203 ]
1203 ]
1204 %>
1204 %>
1205 %for key, desc in elems:
1205 %for key, desc in elems:
@@ -1219,3 +1219,36 b''
1219 </div><!-- /.modal-content -->
1219 </div><!-- /.modal-content -->
1220 </div><!-- /.modal-dialog -->
1220 </div><!-- /.modal-dialog -->
1221 </div><!-- /.modal -->
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 <%namespace name="base" file="/base/base.mako"/>
4 <%namespace name="base" file="/base/base.mako"/>
5 <%namespace name="diff_block" file="/changeset/diff_block.mako"/>
5 <%namespace name="diff_block" file="/changeset/diff_block.mako"/>
6 <%namespace name="file_base" file="/files/base.mako"/>
6 <%namespace name="file_base" file="/files/base.mako"/>
7 <%namespace name="sidebar" file="/base/sidebar.mako"/>
8
7
9
8 <%def name="title()">
10 <%def name="title()">
9 ${_('{} Commit').format(c.repo_name)} - ${h.show_id(c.commit)}
11 ${_('{} Commit').format(c.repo_name)} - ${h.show_id(c.commit)}
@@ -100,22 +102,6 b''
100 % endif
102 % endif
101 </div>
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 <span id="parent_link" class="tag tagtag">
105 <span id="parent_link" class="tag tagtag">
120 <a href="#parentCommit" title="${_('Parent Commit')}"><i class="icon-left icon-no-margin"></i>${_('parent')}</a>
106 <a href="#parentCommit" title="${_('Parent Commit')}"><i class="icon-left icon-no-margin"></i>${_('parent')}</a>
121 </span>
107 </span>
@@ -123,7 +109,9 b''
123 <span id="child_link" class="tag tagtag">
109 <span id="child_link" class="tag tagtag">
124 <a href="#childCommit" title="${_('Child Commit')}">${_('child')}<i class="icon-right icon-no-margin"></i></a>
110 <a href="#childCommit" title="${_('Child Commit')}">${_('child')}<i class="icon-right icon-no-margin"></i></a>
125 </span>
111 </span>
112
126 </div>
113 </div>
114
127 </div>
115 </div>
128 </div>
116 </div>
129
117
@@ -160,7 +148,9 b''
160 <%namespace name="cbdiffs" file="/codeblocks/diffs.mako"/>
148 <%namespace name="cbdiffs" file="/codeblocks/diffs.mako"/>
161 ${cbdiffs.render_diffset_menu(c.changes[c.commit.raw_id], commit=c.commit)}
149 ${cbdiffs.render_diffset_menu(c.changes[c.commit.raw_id], commit=c.commit)}
162 ${cbdiffs.render_diffset(
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 </div>
154 </div>
165
155
166 ## template for inline comment form
156 ## template for inline comment form
@@ -169,7 +159,7 b''
169 ## comments heading with count
159 ## comments heading with count
170 <div class="comments-heading">
160 <div class="comments-heading">
171 <i class="icon-comment"></i>
161 <i class="icon-comment"></i>
172 ${_('Comments')} ${len(c.comments)}
162 ${_('General Comments')} ${len(c.comments)}
173 </div>
163 </div>
174
164
175 ## render comments
165 ## render comments
@@ -180,123 +170,262 b''
180 h.commit_status(c.rhodecode_db_repo, c.commit.raw_id))}
170 h.commit_status(c.rhodecode_db_repo, c.commit.raw_id))}
181 </div>
171 </div>
182
172
183 ## FORM FOR MAKING JS ACTION AS CHANGESET COMMENTS
173 ### NAV SIDEBAR
184 <script type="text/javascript">
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
185
186 $(document).ready(function() {
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>
187
198
188 var boxmax = parseInt($('#trimmed_message_box').css('max-height'), 10);
199 ## REVIEWERS
189 if($('#trimmed_message_box').height() === boxmax){
200 <div class="right-sidebar-expanded-state pr-details-title">
190 $('#message_expand').show();
201 <span class="tooltip sidebar-heading" title="${vote_title}">
191 }
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>
192
236
193 $('#message_expand').on('click', function(e){
237 <div class="right-sidebar-expanded-state pr-details-content">
194 $('#trimmed_message_box').css('max-height', 'none');
238 % if c.unresolved_comments + c.resolved_comments:
195 $(this).hide();
239 ${sidebar.comments_table(c.unresolved_comments + c.resolved_comments, len(c.unresolved_comments), todo_comments=True, is_pr=False)}
196 });
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>
197
268
198 $('.show-inline-comments').on('click', function(e){
269 <div class="right-sidebar-expanded-state pr-details-content">
199 var boxid = $(this).attr('data-comment-id');
270 % if c.inline_comments_flat + c.comments:
200 var button = $(this);
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>
201
289
202 if(button.hasClass("comments-visible")) {
290 ## FORM FOR MAKING JS ACTION AS CHANGESET COMMENTS
203 $('#{0} .inline-comments'.format(boxid)).each(function(index){
291 <script type="text/javascript">
204 $(this).hide();
292 window.setReviewersData = ${c.commit_set_reviewers_data_json | n};
293
294 $(document).ready(function () {
295 var boxmax = parseInt($('#trimmed_message_box').css('max-height'), 10);
296
297 if ($('#trimmed_message_box').height() === boxmax) {
298 $('#message_expand').show();
299 }
300
301 $('#message_expand').on('click', function (e) {
302 $('#trimmed_message_box').css('max-height', 'none');
303 $(this).hide();
304 });
305
306 $('.show-inline-comments').on('click', function (e) {
307 var boxid = $(this).attr('data-comment-id');
308 var button = $(this);
309
310 if (button.hasClass("comments-visible")) {
311 $('#{0} .inline-comments'.format(boxid)).each(function (index) {
312 $(this).hide();
205 });
313 });
206 button.removeClass("comments-visible");
314 button.removeClass("comments-visible");
207 } else {
315 } else {
208 $('#{0} .inline-comments'.format(boxid)).each(function(index){
316 $('#{0} .inline-comments'.format(boxid)).each(function (index) {
209 $(this).show();
317 $(this).show();
210 });
318 });
211 button.addClass("comments-visible");
319 button.addClass("comments-visible");
212 }
320 }
213 });
321 });
214
322
215 // next links
323 // next links
216 $('#child_link').on('click', function(e){
324 $('#child_link').on('click', function (e) {
217 // fetch via ajax what is going to be the next link, if we have
325 // fetch via ajax what is going to be the next link, if we have
218 // >1 links show them to user to choose
326 // >1 links show them to user to choose
219 if(!$('#child_link').hasClass('disabled')){
327 if (!$('#child_link').hasClass('disabled')) {
220 $.ajax({
328 $.ajax({
221 url: '${h.route_path('repo_commit_children',repo_name=c.repo_name, commit_id=c.commit.raw_id)}',
329 url: '${h.route_path('repo_commit_children',repo_name=c.repo_name, commit_id=c.commit.raw_id)}',
222 success: function(data) {
330 success: function (data) {
223 if(data.results.length === 0){
331 if (data.results.length === 0) {
224 $('#child_link').html("${_('No Child Commits')}").addClass('disabled');
332 $('#child_link').html("${_('No Child Commits')}").addClass('disabled');
225 }
333 }
226 if(data.results.length === 1){
334 if (data.results.length === 1) {
227 var commit = data.results[0];
335 var commit = data.results[0];
228 window.location = pyroutes.url('repo_commit', {'repo_name': '${c.repo_name}','commit_id': commit.raw_id});
336 window.location = pyroutes.url('repo_commit', {
229 }
337 'repo_name': '${c.repo_name}',
230 else if(data.results.length === 2){
338 'commit_id': commit.raw_id
231 $('#child_link').addClass('disabled');
339 });
232 $('#child_link').addClass('double');
340 } else if (data.results.length === 2) {
341 $('#child_link').addClass('disabled');
342 $('#child_link').addClass('double');
233
343
234 var _html = '';
344 var _html = '';
235 _html +='<a title="__title__" href="__url__"><span class="tag branchtag"><i class="icon-code-fork"></i>__branch__</span> __rev__</a> '
345 _html += '<a title="__title__" href="__url__"><span class="tag branchtag"><i class="icon-code-fork"></i>__branch__</span> __rev__</a> '
236 .replace('__branch__', data.results[0].branch)
346 .replace('__branch__', data.results[0].branch)
237 .replace('__rev__','r{0}:{1}'.format(data.results[0].revision, data.results[0].raw_id.substr(0,6)))
347 .replace('__rev__', 'r{0}:{1}'.format(data.results[0].revision, data.results[0].raw_id.substr(0, 6)))
238 .replace('__title__', data.results[0].message)
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', {
240 _html +=' | ';
350 'repo_name': '${c.repo_name}',
241 _html +='<a title="__title__" href="__url__"><span class="tag branchtag"><i class="icon-code-fork"></i>__branch__</span> __rev__</a> '
351 'commit_id': data.results[0].raw_id
242 .replace('__branch__', data.results[1].branch)
352 }));
243 .replace('__rev__','r{0}:{1}'.format(data.results[1].revision, data.results[1].raw_id.substr(0,6)))
353 _html += ' | ';
244 .replace('__title__', data.results[1].message)
354 _html += '<a title="__title__" href="__url__"><span class="tag branchtag"><i class="icon-code-fork"></i>__branch__</span> __rev__</a> '
245 .replace('__url__', pyroutes.url('repo_commit', {'repo_name': '${c.repo_name}','commit_id': data.results[1].raw_id}));
355 .replace('__branch__', data.results[1].branch)
246 $('#child_link').html(_html);
356 .replace('__rev__', 'r{0}:{1}'.format(data.results[1].revision, data.results[1].raw_id.substr(0, 6)))
247 }
357 .replace('__title__', data.results[1].message)
358 .replace('__url__', pyroutes.url('repo_commit', {
359 'repo_name': '${c.repo_name}',
360 'commit_id': data.results[1].raw_id
361 }));
362 $('#child_link').html(_html);
363 }
248 }
364 }
249 });
365 });
250 e.preventDefault();
366 e.preventDefault();
251 }
367 }
252 });
368 });
253
369
254 // prev links
370 // prev links
255 $('#parent_link').on('click', function(e){
371 $('#parent_link').on('click', function (e) {
256 // fetch via ajax what is going to be the next link, if we have
372 // fetch via ajax what is going to be the next link, if we have
257 // >1 links show them to user to choose
373 // >1 links show them to user to choose
258 if(!$('#parent_link').hasClass('disabled')){
374 if (!$('#parent_link').hasClass('disabled')) {
259 $.ajax({
375 $.ajax({
260 url: '${h.route_path("repo_commit_parents",repo_name=c.repo_name, commit_id=c.commit.raw_id)}',
376 url: '${h.route_path("repo_commit_parents",repo_name=c.repo_name, commit_id=c.commit.raw_id)}',
261 success: function(data) {
377 success: function (data) {
262 if(data.results.length === 0){
378 if (data.results.length === 0) {
263 $('#parent_link').html('${_('No Parent Commits')}').addClass('disabled');
379 $('#parent_link').html('${_('No Parent Commits')}').addClass('disabled');
264 }
380 }
265 if(data.results.length === 1){
381 if (data.results.length === 1) {
266 var commit = data.results[0];
382 var commit = data.results[0];
267 window.location = pyroutes.url('repo_commit', {'repo_name': '${c.repo_name}','commit_id': commit.raw_id});
383 window.location = pyroutes.url('repo_commit', {
268 }
384 'repo_name': '${c.repo_name}',
269 else if(data.results.length === 2){
385 'commit_id': commit.raw_id
270 $('#parent_link').addClass('disabled');
386 });
271 $('#parent_link').addClass('double');
387 } else if (data.results.length === 2) {
388 $('#parent_link').addClass('disabled');
389 $('#parent_link').addClass('double');
272
390
273 var _html = '';
391 var _html = '';
274 _html +='<a title="__title__" href="__url__"><span class="tag branchtag"><i class="icon-code-fork"></i>__branch__</span> __rev__</a>'
392 _html += '<a title="__title__" href="__url__"><span class="tag branchtag"><i class="icon-code-fork"></i>__branch__</span> __rev__</a>'
275 .replace('__branch__', data.results[0].branch)
393 .replace('__branch__', data.results[0].branch)
276 .replace('__rev__','r{0}:{1}'.format(data.results[0].revision, data.results[0].raw_id.substr(0,6)))
394 .replace('__rev__', 'r{0}:{1}'.format(data.results[0].revision, data.results[0].raw_id.substr(0, 6)))
277 .replace('__title__', data.results[0].message)
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', {
279 _html +=' | ';
397 'repo_name': '${c.repo_name}',
280 _html +='<a title="__title__" href="__url__"><span class="tag branchtag"><i class="icon-code-fork"></i>__branch__</span> __rev__</a>'
398 'commit_id': data.results[0].raw_id
281 .replace('__branch__', data.results[1].branch)
399 }));
282 .replace('__rev__','r{0}:{1}'.format(data.results[1].revision, data.results[1].raw_id.substr(0,6)))
400 _html += ' | ';
283 .replace('__title__', data.results[1].message)
401 _html += '<a title="__title__" href="__url__"><span class="tag branchtag"><i class="icon-code-fork"></i>__branch__</span> __rev__</a>'
284 .replace('__url__', pyroutes.url('repo_commit', {'repo_name': '${c.repo_name}','commit_id': data.results[1].raw_id}));
402 .replace('__branch__', data.results[1].branch)
285 $('#parent_link').html(_html);
403 .replace('__rev__', 'r{0}:{1}'.format(data.results[1].revision, data.results[1].raw_id.substr(0, 6)))
286 }
404 .replace('__title__', data.results[1].message)
405 .replace('__url__', pyroutes.url('repo_commit', {
406 'repo_name': '${c.repo_name}',
407 'commit_id': data.results[1].raw_id
408 }));
409 $('#parent_link').html(_html);
410 }
287 }
411 }
288 });
412 });
289 e.preventDefault();
413 e.preventDefault();
290 }
414 }
291 });
415 });
292
416
293 // browse tree @ revision
417 // browse tree @ revision
294 $('#files_link').on('click', function(e){
418 $('#files_link').on('click', function (e) {
295 window.location = '${h.route_path('repo_files:default_path',repo_name=c.repo_name, commit_id=c.commit.raw_id)}';
419 window.location = '${h.route_path('repo_files:default_path',repo_name=c.repo_name, commit_id=c.commit.raw_id)}';
296 e.preventDefault();
420 e.preventDefault();
297 });
421 });
298
422
299 })
423 ReviewersPanel.init(null, setReviewersData);
300 </script>
424
425 var channel = '${c.commit_broadcast_channel}';
426 new ReviewerPresenceController(channel)
427
428 })
429 </script>
301
430
302 </%def>
431 </%def>
@@ -10,12 +10,18 b''
10
10
11 <%namespace name="base" file="/base/base.mako"/>
11 <%namespace name="base" file="/base/base.mako"/>
12 <%def name="comment_block(comment, inline=False, active_pattern_entries=None)">
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 <% latest_ver = len(getattr(c, 'versions', [])) %>
19 <% latest_ver = len(getattr(c, 'versions', [])) %>
20
15 % if inline:
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 % else:
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 % endif
25 % endif
20
26
21 <div class="comment
27 <div class="comment
@@ -70,9 +76,9 b''
70 status_change_title = 'Status of review for commit {}'.format(h.short_id(comment.commit_id))
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 <div class="changeset-status-lbl tooltip" title="${status_change_title}">
80 <div class="changeset-status-lbl tooltip" title="${status_change_title}">
75 ${comment.status_change[0].status_lbl}
81 ${comment.review_status_lbl}
76 </div>
82 </div>
77 % else:
83 % else:
78 <div>
84 <div>
@@ -153,69 +159,90 b''
153 </div>
159 </div>
154 %endif
160 %endif
155
161
156 <a class="permalink" href="#comment-${comment.comment_id}"> &para;</a>
157
158 <div class="comment-links-block">
162 <div class="comment-links-block">
159
163
160 % if inline:
164 % if inline:
161 <a class="pr-version-inline" href="${request.current_route_path(_query=dict(version=comment.pull_request_version_id), _anchor='comment-{}'.format(comment.comment_id))}">
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 % if outdated_at_ver:
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)}">
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>
164 outdated ${'v{}'.format(pr_index_ver)} |
168 <code class="action-divider">|</code>
165 </code>
169 % elif comment_ver:
166 % elif pr_index_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>
167 <code class="tooltip pr-version-num" title="${_('Comment from pull request version v{0}, latest v{1}').format(pr_index_ver, latest_ver)}">
171 <code class="action-divider">|</code>
168 ${'v{}'.format(pr_index_ver)} |
169 </code>
170 % endif
172 % endif
171 </a>
173 </a>
172 % else:
174 % else:
173 % if pr_index_ver:
175 % if comment_ver:
174
176
175 % if comment.outdated:
177 % if comment.outdated:
176 <a class="pr-version"
178 <a class="pr-version"
177 href="?version=${comment.pull_request_version_id}#comment-${comment.comment_id}"
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)}
181 ${_('Outdated comment from pull request version v{0}, latest v{1}').format(comment_ver, latest_ver)}
180 </a> |
182 </a>
183 <code class="action-divider">|</code>
181 % else:
184 % else:
182 <a class="tooltip pr-version"
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 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)}"
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">
189 <code class="pr-version-num">${'v{}'.format(comment_ver)}</code>
187 ${'v{}'.format(pr_index_ver)}
190 </a>
188 </code>
191 <code class="action-divider">|</code>
189 </a> |
190 % endif
192 % endif
191
193
192 % endif
194 % endif
193 % endif
195 % endif
194
196
195 ## show delete comment if it's not a PR (regular comments) or it's PR that is not closed
197 <details class="details-reset details-inline-block">
196 ## only super-admin, repo admin OR comment owner can delete, also hide delete if currently viewed comment is outdated
198 <summary class="noselect"><i class="icon-options cursor-pointer"></i></summary>
197 %if not outdated_at_ver and (not comment.pull_request or (comment.pull_request and not comment.pull_request.is_closed())):
199 <details-menu class="details-dropdown">
198 ## permissions to delete
200
199 %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):
201 <div class="dropdown-item">
200 <a onclick="return Rhodecode.comments.editComment(this);"
202 ${_('Comment')} #${comment.comment_id}
201 class="edit-comment">${_('Edit')}</a>
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>
202 | <a onclick="return Rhodecode.comments.deleteComment(this);"
204 </div>
203 class="delete-comment">${_('Delete')}</a>
204 %else:
205 <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>
207 %endif
208 %else:
209 <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>
211 %endif
212
205
206 ## show delete comment if it's not a PR (regular comments) or it's PR that is not closed
207 ## only super-admin, repo admin OR comment owner can delete, also hide delete if currently viewed comment is outdated
208 %if not outdated_at_ver and (not comment.pull_request or (comment.pull_request and not comment.pull_request.is_closed())):
209 ## permissions to delete
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):
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>
218 %else:
219 <div class="dropdown-divider"></div>
220 <div class="dropdown-item">
221 <a class="tooltip edit-comment link-disabled" disabled="disabled" title="${_('Action unavailable')}">${_('Edit')}</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>
226 %endif
227 %else:
228 <div class="dropdown-divider"></div>
229 <div class="dropdown-item">
230 <a class="tooltip edit-comment link-disabled" disabled="disabled" title="${_('Action unavailable')}">${_('Edit')}</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>
235 %endif
236 </details-menu>
237 </details>
238
239 <code class="action-divider">|</code>
213 % if outdated_at_ver:
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>
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>
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>
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 % else:
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>
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>
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>
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 % endif
246 % endif
220
247
221 </div>
248 </div>
@@ -102,6 +102,11 b''
102 <%namespace name="diff_block" file="/changeset/diff_block.mako"/>
102 <%namespace name="diff_block" file="/changeset/diff_block.mako"/>
103
103
104 %for commit in c.commit_ranges:
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 ${cbdiffs.render_diffset_menu(c.changes[commit.raw_id])}
110 ${cbdiffs.render_diffset_menu(c.changes[commit.raw_id])}
106 ${cbdiffs.render_diffset(
111 ${cbdiffs.render_diffset(
107 diffset=c.changes[commit.raw_id],
112 diffset=c.changes[commit.raw_id],
@@ -61,6 +61,8 b" return '%s_%s_%i' % (h.md5_safe(commit+f"
61 diffset_container_id = h.md5(diffset.target_ref)
61 diffset_container_id = h.md5(diffset.target_ref)
62 collapse_all = len(diffset.files) > collapse_when_files_over
62 collapse_all = len(diffset.files) > collapse_when_files_over
63 active_pattern_entries = h.get_active_pattern_entries(getattr(c, 'repo_name', None))
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 %if use_comments:
68 %if use_comments:
@@ -159,45 +161,45 b" return '%s_%s_%i' % (h.md5_safe(commit+f"
159 </div>
161 </div>
160 % endif
162 % endif
161
163
162 ## comments
164 ## ## comments
163 <div class="pull-right">
165 ## <div class="pull-right">
164 <div class="comments-number" style="padding-left: 10px">
166 ## <div class="comments-number" style="padding-left: 10px">
165 % if hasattr(c, 'comments') and hasattr(c, 'inline_cnt'):
167 ## % if hasattr(c, 'comments') and hasattr(c, 'inline_cnt'):
166 <i class="icon-comment" style="color: #949494">COMMENTS:</i>
168 ## <i class="icon-comment" style="color: #949494">COMMENTS:</i>
167 % if c.comments:
169 ## % if c.comments:
168 <a href="#comments">${_ungettext("{} General", "{} General", len(c.comments)).format(len(c.comments))}</a>,
170 ## <a href="#comments">${_ungettext("{} General", "{} General", len(c.comments)).format(len(c.comments))}</a>,
169 % else:
171 ## % else:
170 ${_('0 General')}
172 ## ${_('0 General')}
171 % endif
173 ## % endif
172
174 ##
173 % if c.inline_cnt:
175 ## % if c.inline_cnt:
174 <a href="#" onclick="return Rhodecode.comments.nextComment();"
176 ## <a href="#" onclick="return Rhodecode.comments.nextComment();"
175 id="inline-comments-counter">${_ungettext("{} Inline", "{} Inline", c.inline_cnt).format(c.inline_cnt)}
177 ## id="inline-comments-counter">${_ungettext("{} Inline", "{} Inline", c.inline_cnt).format(c.inline_cnt)}
176 </a>
178 ## </a>
177 % else:
179 ## % else:
178 ${_('0 Inline')}
180 ## ${_('0 Inline')}
179 % endif
181 ## % endif
180 % endif
182 ## % endif
181
183 ##
182 % if pull_request_menu:
184 ## % if pull_request_menu:
183 <%
185 ## <%
184 outdated_comm_count_ver = pull_request_menu['outdated_comm_count_ver']
186 ## outdated_comm_count_ver = pull_request_menu['outdated_comm_count_ver']
185 %>
187 ## %>
186
188 ##
187 % if outdated_comm_count_ver:
189 ## % if outdated_comm_count_ver:
188 <a href="#" onclick="showOutdated(); Rhodecode.comments.nextOutdatedComment(); return false;">
190 ## <a href="#" onclick="showOutdated(); Rhodecode.comments.nextOutdatedComment(); return false;">
189 (${_("{} Outdated").format(outdated_comm_count_ver)})
191 ## (${_("{} Outdated").format(outdated_comm_count_ver)})
190 </a>
192 ## </a>
191 <a href="#" class="showOutdatedComments" onclick="showOutdated(this); return false;"> | ${_('show outdated')}</a>
193 ## <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>
194 ## <a href="#" class="hideOutdatedComments" style="display: none" onclick="hideOutdated(this); return false;"> | ${_('hide outdated')}</a>
193 % else:
195 ## % else:
194 (${_("{} Outdated").format(outdated_comm_count_ver)})
196 ## (${_("{} Outdated").format(outdated_comm_count_ver)})
195 % endif
197 ## % endif
196
198 ##
197 % endif
199 ## % endif
198
200 ##
199 </div>
201 ## </div>
200 </div>
202 ## </div>
201
203
202 </div>
204 </div>
203
205
@@ -208,13 +210,6 b" return '%s_%s_%i' % (h.md5_safe(commit+f"
208 <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>
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 </h2>
211 </h2>
210 </div>
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 % endif
213 % endif
219
214
220 <div id="todo-box">
215 <div id="todo-box">
@@ -239,6 +234,43 b" return '%s_%s_%i' % (h.md5_safe(commit+f"
239 <% over_lines_changed_limit = False %>
234 <% over_lines_changed_limit = False %>
240 %for i, filediff in enumerate(diffset.files):
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 lines_changed = filediff.patch['stats']['added'] + filediff.patch['stats']['deleted']
275 lines_changed = filediff.patch['stats']['added'] + filediff.patch['stats']['deleted']
244 over_lines_changed_limit = lines_changed > lines_changed_limit
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 total_file_comments = [_c for _c in h.itertools.chain.from_iterable(file_comments) if not _c.outdated]
290 total_file_comments = [_c for _c in h.itertools.chain.from_iterable(file_comments) if not _c.outdated]
259 %>
291 %>
260 <div class="filediff-collapse-indicator icon-"></div>
292 <div class="filediff-collapse-indicator icon-"></div>
261 <span class="pill-group pull-right" >
293
294 ## Comments/Options PILL
295 <span class="pill-group pull-right">
262 <span class="pill" op="comments">
296 <span class="pill" op="comments">
263
264 <i class="icon-comment"></i> ${len(total_file_comments)}
297 <i class="icon-comment"></i> ${len(total_file_comments)}
265 </span>
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 </span>
323 </span>
267 ${diff_ops(filediff)}
324
325 ${diff_ops(final_file_name, filediff)}
268
326
269 </label>
327 </label>
270
328
@@ -463,43 +521,15 b" return '%s_%s_%i' % (h.md5_safe(commit+f"
463 </div>
521 </div>
464 </%def>
522 </%def>
465
523
466 <%def name="diff_ops(filediff)">
524 <%def name="diff_ops(file_name, filediff)">
467 <%
525 <%
468 from rhodecode.lib.diffs import NEW_FILENODE, DEL_FILENODE, \
526 from rhodecode.lib.diffs import NEW_FILENODE, DEL_FILENODE, \
469 MOD_FILENODE, RENAMED_FILENODE, CHMOD_FILENODE, BIN_FILENODE, COPIED_FILENODE
527 MOD_FILENODE, RENAMED_FILENODE, CHMOD_FILENODE, BIN_FILENODE, COPIED_FILENODE
470 %>
528 %>
471 <span class="pill">
529 <span class="pill">
472 <i class="icon-file-text"></i>
530 <i class="icon-file-text"></i>
473 %if filediff.source_file_path and filediff.target_file_path:
531 ${file_name}
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>
500 </span>
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 <span class="pill-group pull-right">
534 <span class="pill-group pull-right">
505
535
@@ -934,7 +964,7 b' def get_comments_for(diff_type, comments'
934 </span>
964 </span>
935 %endif
965 %endif
936 % if commit or pull_request_menu:
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 <span class="cursor-pointer" onclick="scrollToPrevChunk(); return false">
968 <span class="cursor-pointer" onclick="scrollToPrevChunk(); return false">
939 <i class="icon-angle-up"></i>
969 <i class="icon-angle-up"></i>
940 </span>
970 </span>
@@ -21,8 +21,9 b''
21 ## to speed up lookups cache some functions before the loop
21 ## to speed up lookups cache some functions before the loop
22 <%
22 <%
23 active_patterns = h.get_active_pattern_entries(c.repo_name)
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 %for commit in c.commit_ranges:
27 %for commit in c.commit_ranges:
27 <tr id="row-${commit.raw_id}"
28 <tr id="row-${commit.raw_id}"
28 commit_id="${commit.raw_id}"
29 commit_id="${commit.raw_id}"
@@ -1,6 +1,10 b''
1 <%text>
1 <%text>
2 <div style="display: none">
2 <div style="display: none">
3
3
4 <script>
5 var CG = new ColorGenerator();
6 </script>
7
4 <script id="ejs_gravatarWithUser" type="text/template" class="ejsTemplate">
8 <script id="ejs_gravatarWithUser" type="text/template" class="ejsTemplate">
5
9
6 <%
10 <%
@@ -34,38 +38,41 b" var data_hovercard_url = pyroutes.url('h"
34
38
35 </script>
39 </script>
36
40
37 <script>
38 var CG = new ColorGenerator();
39 </script>
40
41 <script id="ejs_reviewMemberEntry" type="text/template" class="ejsTemplate">
41 <script id="ejs_reviewMemberEntry" type="text/template" class="ejsTemplate">
42 <%
43 if (create) {
44 var edit_visibility = 'visible';
45 } else {
46 var edit_visibility = 'hidden';
47 }
42
48
43 <li id="reviewer_<%= member.user_id %>" class="reviewer_entry">
49 if (member.user_group && member.user_group.vote_rule) {
44 <%
50 var reviewGroup = '<i class="icon-user-group"></i>';
45 if (create) {
51 var reviewGroupColor = CG.asRGB(CG.getColor(member.user_group.vote_rule));
46 var edit_visibility = 'visible';
52 } else {
47 } else {
53 var reviewGroup = null;
48 var edit_visibility = 'hidden';
54 var reviewGroupColor = 'transparent';
49 }
55 }
56 var rule_show = rule_show || false;
50
57
51 if (member.user_group && member.user_group.vote_rule) {
58 if (rule_show) {
52 var groupStyle = 'border-left: 1px solid '+CG.asRGB(CG.getColor(member.user_group.vote_rule));
59 var rule_visibility = 'table-cell';
53 } else {
60 } else {
54 var groupStyle = 'border-left: 1px solid white';
61 var rule_visibility = 'none';
55 }
62 }
56 %>
57
63
58 <div class="reviewers_member" style="<%= groupStyle%>" >
64 %>
65
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 <div class="reviewer_status tooltip" title="<%= review_status_label %>">
69 <div class="reviewer_status tooltip" title="<%= review_status_label %>">
60 <i class="icon-circle review-status-<%= review_status %>"></i>
70 <i class="icon-circle review-status-<%= review_status %>"></i>
61 </div>
71 </div>
72 </td>
73
74 <td>
62 <div id="reviewer_<%= member.user_id %>_name" class="reviewer_name">
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 renderTemplate('gravatarWithUser', {
77 renderTemplate('gravatarWithUser', {
71 'size': 16,
78 'size': 16,
@@ -77,12 +84,44 b' var CG = new ColorGenerator();'
77 'gravatar_url': member.gravatar_link
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 </div>
90 </div>
91 </td>
81
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>
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 <input type="hidden" name="__start__" value="reviewer:mapping">
122 <input type="hidden" name="__start__" value="reviewer:mapping">
83
123
84
124 <%if (member.user_group && member.user_group.vote_rule) { %>
85 <%if (member.user_group && member.user_group.vote_rule) {%>
86 <div class="reviewer_reason">
125 <div class="reviewer_reason">
87
126
88 <%if (member.user_group.vote_rule == -1) {%>
127 <%if (member.user_group.vote_rule == -1) {%>
@@ -91,7 +130,7 b' var CG = new ColorGenerator();'
91 - group votes required: <%= member.user_group.vote_rule %>
130 - group votes required: <%= member.user_group.vote_rule %>
92 <%}%>
131 <%}%>
93 </div>
132 </div>
94 <%}%>
133 <%} %>
95
134
96 <input type="hidden" name="__start__" value="reasons:sequence">
135 <input type="hidden" name="__start__" value="reasons:sequence">
97 <% for (var i = 0; i < reasons.length; i++) { %>
136 <% for (var i = 0; i < reasons.length; i++) { %>
@@ -99,37 +138,24 b' var CG = new ColorGenerator();'
99 <div class="reviewer_reason">- <%= reason %></div>
138 <div class="reviewer_reason">- <%= reason %></div>
100 <input type="hidden" name="reason" value="<%= reason %>">
139 <input type="hidden" name="reason" value="<%= reason %>">
101 <% } %>
140 <% } %>
102 <input type="hidden" name="__end__" value="reasons:sequence">
141 <input type="hidden" name="__end__" value="reasons:sequence">
103
142
104 <input type="hidden" name="__start__" value="rules:sequence">
143 <input type="hidden" name="__start__" value="rules:sequence">
105 <% for (var i = 0; i < member.rules.length; i++) { %>
144 <% for (var i = 0; i < member.rules.length; i++) { %>
106 <% var rule = member.rules[i] %>
145 <% var rule = member.rules[i] %>
107 <input type="hidden" name="rule_id" value="<%= rule %>">
146 <input type="hidden" name="rule_id" value="<%= rule %>">
108 <% } %>
147 <% } %>
109 <input type="hidden" name="__end__" value="rules:sequence">
148 <input type="hidden" name="__end__" value="rules:sequence">
110
149
111 <input id="reviewer_<%= member.user_id %>_input" type="hidden" value="<%= member.user_id %>" name="user_id" />
150 <input id="reviewer_<%= member.user_id %>_input" type="hidden" value="<%= member.user_id %>" name="user_id" />
112 <input type="hidden" name="mandatory" value="<%= mandatory %>"/>
151 <input type="hidden" name="mandatory" value="<%= mandatory %>"/>
113
152
114 <input type="hidden" name="__end__" value="reviewer:mapping">
153 <input type="hidden" name="__end__" value="reviewer:mapping">
115
154 </td>
116 <% if (mandatory) { %>
155 </tr>
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>
129
156
130 </script>
157 </script>
131
158
132
133 <script id="ejs_commentVersion" type="text/template" class="ejsTemplate">
159 <script id="ejs_commentVersion" type="text/template" class="ejsTemplate">
134
160
135 <%
161 <%
@@ -158,8 +184,56 b' if (show_disabled) {'
158 </script>
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 </div>
229 </div>
162
230
231 </script>
232
233 ##// END OF EJS Templates
234 </div>
235
236
163 <script>
237 <script>
164 // registers the templates into global cache
238 // registers the templates into global cache
165 registerTemplates();
239 registerTemplates();
@@ -360,13 +360,13 b' text_monospace = "\'Menlo\', \'Liberation M'
360
360
361 div.markdown-block ul.checkbox li, div.markdown-block ol.checkbox li {
361 div.markdown-block ul.checkbox li, div.markdown-block ol.checkbox li {
362 list-style: none !important;
362 list-style: none !important;
363 margin: 6px !important;
363 margin: 0px !important;
364 padding: 0 !important
364 padding: 0 !important
365 }
365 }
366
366
367 div.markdown-block ul li, div.markdown-block ol li {
367 div.markdown-block ul li, div.markdown-block ol li {
368 list-style: disc !important;
368 list-style: disc !important;
369 margin: 6px !important;
369 margin: 0px !important;
370 padding: 0 !important
370 padding: 0 !important
371 }
371 }
372
372
@@ -19,21 +19,74 b''
19 <div class="box">
19 <div class="box">
20 ${h.secure_form(h.route_path('pullrequest_create', repo_name=c.repo_name, _query=request.GET.mixed()), id='pull_request_form', request=request)}
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 <div class="summary-details block-left">
24 <div class="summary-details block-left">
25
25
26
27 <div class="pr-details-title">
28 ${_('New pull request')}
29 </div>
30
31 <div class="form" style="padding-top: 10px">
26 <div class="form" style="padding-top: 10px">
32 <!-- fields -->
33
27
34 <div class="fields" >
28 <div class="fields" >
35
29
36 <div class="field">
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
89 <div class="field">
37 <div class="label">
90 <div class="label">
38 <label for="pullrequest_title">${_('Title')}:</label>
91 <label for="pullrequest_title">${_('Title')}:</label>
39 </div>
92 </div>
@@ -43,8 +96,9 b''
43 <p class="help-block">
96 <p class="help-block">
44 Start the title with WIP: to prevent accidental merge of Work In Progress pull request before it's ready.
97 Start the title with WIP: to prevent accidental merge of Work In Progress pull request before it's ready.
45 </p>
98 </p>
46 </div>
99 </div>
47
100
101 ## DESC
48 <div class="field">
102 <div class="field">
49 <div class="label label-textarea">
103 <div class="label label-textarea">
50 <label for="pullrequest_desc">${_('Description')}:</label>
104 <label for="pullrequest_desc">${_('Description')}:</label>
@@ -55,39 +109,49 b''
55 </div>
109 </div>
56 </div>
110 </div>
57
111
112 ## REVIEWERS
58 <div class="field">
113 <div class="field">
59 <div class="label label-textarea">
114 <div class="label label-textarea">
60 <label for="commit_flow">${_('Commit flow')}:</label>
115 <label for="pullrequest_reviewers">${_('Reviewers')}:</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}
70 </div>
116 </div>
71 <div class="content">
117 <div class="content">
72 ${h.hidden('source_repo')}
118 ## REVIEW RULES
73 ${h.hidden('source_ref')}
119 <div id="review_rules" style="display: none" class="reviewers-title">
74 </div>
120 <div class="pr-details-title">
121 ${_('Reviewer rules')}
122 </div>
123 <div class="pr-reviewer-rules">
124 ## review rules will be appended here, by default reviewers logic
125 </div>
126 </div>
75
127
76 ##OTHER, most Probably the PARENT OF THIS FORK
128 ## REVIEWERS
77 <div class="content">
129 <div class="reviewers-title">
78 ## filled with JS
130 <div class="pr-details-title">
79 <div id="target_repo_desc"></div>
131 ${_('Pull request reviewers')}
80 </div>
132 <span class="calculate-reviewers"> - ${_('loading...')}</span>
133 </div>
134 </div>
135 <div id="reviewers" class="pr-details-content reviewers">
136 ## members goes here, filled via JS based on initial selection !
137 <input type="hidden" name="__start__" value="review_members:sequence">
138 <table id="review_members" class="group_members">
139 ## This content is loaded via JS and ReviewersPanel
140 </table>
141 <input type="hidden" name="__end__" value="review_members:sequence">
81
142
82 <div class="content">
143 <div id="add_reviewer_input" class='ac'>
83 ${h.hidden('target_repo')}
144 <div class="reviewer_ac">
84 ${h.hidden('target_ref')}
145 ${h.text('user', class_='ac-input', placeholder=_('Add reviewer or reviewer group'))}
85 <span id="target_ref_loading" style="display: none">
146 <div id="reviewers_container"></div>
86 ${_('Loading refs...')}
147 </div>
87 </span>
148 </div>
149
150 </div>
88 </div>
151 </div>
89 </div>
152 </div>
90
153
154 ## SUBMIT
91 <div class="field">
155 <div class="field">
92 <div class="label label-textarea">
156 <div class="label label-textarea">
93 <label for="pullrequest_submit"></label>
157 <label for="pullrequest_submit"></label>
@@ -96,66 +160,14 b''
96 <div class="pr-submit-button">
160 <div class="pr-submit-button">
97 <input id="pr_submit" class="btn" name="save" type="submit" value="${_('Submit Pull Request')}">
161 <input id="pr_submit" class="btn" name="save" type="submit" value="${_('Submit Pull Request')}">
98 </div>
162 </div>
99 <div id="pr_open_message"></div>
100 </div>
163 </div>
101 </div>
164 </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 ## REVIEW RULES
123 <div id="review_rules" style="display: none" class="reviewers-title block-right">
124 <div class="pr-details-title">
125 ${_('Reviewer rules')}
126 </div>
127 <div class="pr-reviewer-rules">
128 ## review rules will be appended here, by default reviewers logic
129 </div>
130 </div>
131
132 ## REVIEWERS
133 <div class="reviewers-title block-right">
134 <div class="pr-details-title">
135 ${_('Pull request reviewers')}
136 <span class="calculate-reviewers"> - ${_('loading...')}</span>
137 </div>
138 </div>
139 <div id="reviewers" class="block-right pr-details-content reviewers">
140 ## members goes here, filled via JS based on initial selection !
141 <input type="hidden" name="__start__" value="review_members:sequence">
142 <ul id="review_members" class="group_members"></ul>
143 <input type="hidden" name="__end__" value="review_members:sequence">
144 <div id="add_reviewer_input" class='ac'>
145 <div class="reviewer_ac">
146 ${h.text('user', class_='ac-input', placeholder=_('Add reviewer or reviewer group'))}
147 <div id="reviewers_container"></div>
148 </div>
149 </div>
165 </div>
150 </div>
166 </div>
151 </div>
167 </div>
168
152 </div>
169 </div>
153 <div class="box">
170
154 <div>
155 ## overview pulled by ajax
156 <div id="pull_request_overview"></div>
157 </div>
158 </div>
159 ${h.end_form()}
171 ${h.end_form()}
160 </div>
172 </div>
161
173
@@ -243,8 +255,6 b''
243
255
244 var diffDataHandler = function(data) {
256 var diffDataHandler = function(data) {
245
257
246 $('#pull_request_overview').html(data);
247
248 var commitElements = data['commits'];
258 var commitElements = data['commits'];
249 var files = data['files'];
259 var files = data['files'];
250 var added = data['stats'][0]
260 var added = data['stats'][0]
@@ -303,27 +313,33 b''
303
313
304 msg += '<input type="hidden" name="__end__" value="revisions:sequence">'
314 msg += '<input type="hidden" name="__end__" value="revisions:sequence">'
305 msg += _ngettext(
315 msg += _ngettext(
306 'This pull requests will consist of <strong>{0} commit</strong>.',
316 'Compare summary: <strong>{0} commit</strong>',
307 'This pull requests will consist of <strong>{0} commits</strong>.',
317 'Compare summary: <strong>{0} commits</strong>',
308 commitElements.length).format(commitElements.length)
318 commitElements.length).format(commitElements.length)
309
319
310 msg += '\n';
320 msg += '';
311 msg += _ngettext(
321 msg += _ngettext(
312 '<strong>{0} file</strong> changed, ',
322 '<strong>, and {0} file</strong> changed.',
313 '<strong>{0} files</strong> changed, ',
323 '<strong>, and {0} files</strong> changed.',
314 files.length).format(files.length)
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 if (commitElements.length) {
330 if (commitElements.length) {
320 var commitsLink = '<a href="#pull_request_overview"><strong>{0}</strong></a>'.format(commitElements.length);
331 var commitsLink = '<a href="#pull_request_overview"><strong>{0}</strong></a>'.format(commitElements.length);
321 prButtonLock(false, msg.replace('__COMMITS__', commitsLink), 'compare');
332 prButtonLock(false, msg.replace('__COMMITS__', commitsLink), 'compare');
322 }
333 }
323 else {
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 reviewersController = new ReviewersController();
345 reviewersController = new ReviewersController();
@@ -429,10 +445,12 b''
429
445
430 var targetRepoChanged = function(repoData) {
446 var targetRepoChanged = function(repoData) {
431 // generate new DESC of target repo displayed next to select
447 // generate new DESC of target repo displayed next to select
448
449 $('#target_repo_desc').html(repoData['description']);
450
432 var prLink = pyroutes.url('pullrequest_new', {'repo_name': repoData['name']});
451 var prLink = pyroutes.url('pullrequest_new', {'repo_name': repoData['name']});
433 $('#target_repo_desc').html(
452 var title = _gettext('Switch target repository with the source.')
434 "<strong>${_('Target repository')}</strong>: {0}. <a href=\"{1}\">Switch base, and use as source.</a>".format(repoData['description'], prLink)
453 $('#switch_base').html("<a class=\"tooltip\" title=\"{0}\" href=\"{1}\">Switch sides</a>".format(title, prLink))
435 );
436
454
437 // generate dynamic select2 for refs.
455 // generate dynamic select2 for refs.
438 initTargetRefs(repoData['refs']['select2_refs'],
456 initTargetRefs(repoData['refs']['select2_refs'],
This diff has been collapsed as it changes many lines, (808 lines changed) Show them Hide them
@@ -1,6 +1,8 b''
1 <%inherit file="/base/base.mako"/>
1 <%inherit file="/base/base.mako"/>
2 <%namespace name="base" file="/base/base.mako"/>
2 <%namespace name="base" file="/base/base.mako"/>
3 <%namespace name="dt" file="/data_table/_dt_elements.mako"/>
3 <%namespace name="dt" file="/data_table/_dt_elements.mako"/>
4 <%namespace name="sidebar" file="/base/sidebar.mako"/>
5
4
6
5 <%def name="title()">
7 <%def name="title()">
6 ${_('{} Pull Request !{}').format(c.repo_name, c.pull_request.pull_request_id)}
8 ${_('{} Pull Request !{}').format(c.repo_name, c.pull_request.pull_request_id)}
@@ -21,12 +23,19 b''
21 ${self.repo_menu(active='showpullrequest')}
23 ${self.repo_menu(active='showpullrequest')}
22 </%def>
24 </%def>
23
25
26
24 <%def name="main()">
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 <script type="text/javascript">
34 <script type="text/javascript">
27 // TODO: marcink switch this to pyroutes
35 // TODO: marcink switch this to pyroutes
28 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__')}";
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 templateContext.pull_request_data.pull_request_id = ${c.pull_request.pull_request_id};
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 </script>
39 </script>
31
40
32 <div class="box">
41 <div class="box">
@@ -79,7 +88,7 b''
79 </div>
88 </div>
80
89
81 <div id="pr-desc" class="input" title="${_('Rendered using {} renderer').format(c.renderer)}">
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 </div>
92 </div>
84
93
85 <div id="pr-desc-edit" class="input textarea" style="display: none;">
94 <div id="pr-desc-edit" class="input textarea" style="display: none;">
@@ -89,29 +98,6 b''
89
98
90 <div id="summary" class="fields pr-details-content">
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 ## source
101 ## source
116 <div class="field">
102 <div class="field">
117 <div class="label-pr-detail">
103 <div class="label-pr-detail">
@@ -136,7 +122,7 b''
136
122
137 ${_('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>
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 <i class="icon-angle-down">more details</i>
126 <i class="icon-angle-down">more details</i>
141 </a>
127 </a>
142
128
@@ -231,7 +217,7 b''
231 </code>
217 </code>
232 </td>
218 </td>
233 <td>
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 <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}"/>
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 </td>
222 </td>
237 <td>
223 <td>
@@ -280,159 +266,12 b''
280
266
281 </div>
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
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 </div>
432
269
433 </div>
270 </div>
434
271
435 <div class="box">
272 </div>
273
274 <div class="box">
436
275
437 % if c.state_progressing:
276 % if c.state_progressing:
438
277
@@ -484,9 +323,9 b''
484 <div class="compare_view_commits_title">
323 <div class="compare_view_commits_title">
485 % if not c.compare_mode:
324 % if not c.compare_mode:
486
325
487 % if c.at_version_pos:
326 % if c.at_version_index:
488 <h4>
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 </h4>
329 </h4>
491 % endif
330 % endif
492
331
@@ -539,10 +378,11 b''
539 </div>
378 </div>
540
379
541 % if not c.missing_commits:
380 % if not c.missing_commits:
381 ## COMPARE RANGE DIFF MODE
542 % if c.compare_mode:
382 % if c.compare_mode:
543 % if c.at_version:
383 % if c.at_version:
544 <h4>
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 </h4>
386 </h4>
547
387
548 <div class="subtitle-compare">
388 <div class="subtitle-compare">
@@ -597,7 +437,7 b''
597 </td>
437 </td>
598 <td class="mid td-description">
438 <td class="mid td-description">
599 <div class="log-container truncate-wrap">
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 </div>
441 </div>
602 </td>
442 </td>
603 </tr>
443 </tr>
@@ -608,19 +448,13 b''
608
448
609 % endif
449 % endif
610
450
451 ## Regular DIFF
611 % else:
452 % else:
612 <%include file="/compare/compare_commits.mako" />
453 <%include file="/compare/compare_commits.mako" />
613 % endif
454 % endif
614
455
615 <div class="cs_files">
456 <div class="cs_files">
616 <%namespace name="cbdiffs" file="/codeblocks/diffs.mako"/>
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 pr_menu_data = {
460 pr_menu_data = {
@@ -667,7 +501,7 b''
667 ## comments heading with count
501 ## comments heading with count
668 <div class="comments-heading">
502 <div class="comments-heading">
669 <i class="icon-comment"></i>
503 <i class="icon-comment"></i>
670 ${_('Comments')} ${len(c.comments)}
504 ${_('General Comments')} ${len(c.comments)}
671 </div>
505 </div>
672
506
673 ## render general comments
507 ## render general comments
@@ -704,218 +538,456 b''
704 % endif
538 % endif
705 </div>
539 </div>
706
540
707 <script type="text/javascript">
708
709 versionController = new VersionController();
710 versionController.init();
711
541
712 reviewersController = new ReviewersController();
542 ### NAV SIDEBAR
713 commitsController = new CommitsController();
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">
714
548
715 updateController = new UpdatePrController();
549 </a>
550 </div>
551
552 ## CONTENT
553 <div class="sidebar-content">
716
554
717 $(function () {
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 %>
718
561
719 // custom code mirror
562 <div class="tooltip right-sidebar-collapsed-state" style="display: none" onclick="toggleSidebar(); return false" title="${vote_title}">
720 var codeMirrorInstance = $('#pr-description-input').get(0).MarkupForm.cm;
563 <i class="icon-circle review-status-${c.pull_request_review_status}"></i>
564 ${len(c.allowed_reviewers)}
565 </div>
721
566
722 var PRDetails = {
567 ## REVIEW RULES
723 editButton: $('#open_edit_pullrequest'),
568 <div id="review_rules" style="display: none" class="">
724 closeButton: $('#close_edit_pullrequest'),
569 <div class="right-sidebar-expanded-state pr-details-title">
725 deleteButton: $('#delete_pullrequest'),
570 <span class="sidebar-heading">
726 viewFields: $('#pr-desc, #pr-title'),
571 ${_('Reviewer rules')}
727 editFields: $('#pr-desc-edit, #pr-title-edit, .pr-save'),
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>
728
580
729 init: function () {
581 ## REVIEWERS
730 var that = this;
582 <div class="right-sidebar-expanded-state pr-details-title">
731 this.editButton.on('click', function (e) {
583 <span class="tooltip sidebar-heading" title="${vote_title}">
732 that.edit();
584 <i class="icon-circle review-status-${c.pull_request_review_status}"></i>
733 });
585 ${_('Reviewers')}
734 this.closeButton.on('click', function (e) {
586 </span>
735 that.view();
587 %if c.allowed_to_update:
736 });
588 <span id="open_edit_reviewers" class="block-right action_button last-item">${_('Edit')}</span>
737 },
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
738
607
739 edit: function (event) {
608 %if not c.pull_request.is_closed():
740 this.viewFields.hide();
609 <div id="add_reviewer" class="ac" style="display: none;">
741 this.editButton.hide();
610 %if c.allowed_to_update:
742 this.deleteButton.hide();
611 % if not c.forbid_adding_reviewers:
743 this.closeButton.show();
612 <div id="add_reviewer_input" class="reviewer_ac">
744 this.editFields.show();
613 ${h.text('user', class_='ac-input', placeholder=_('Add reviewer or reviewer group'))}
745 codeMirrorInstance.refresh();
614 <div id="reviewers_container"></div>
746 },
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>
747
625
748 view: function (event) {
626 ## ## OBSERVERS
749 this.editButton.show();
627 ## <div class="sidebar-element clear-both">
750 this.deleteButton.show();
628 ## <div class="tooltip right-sidebar-collapsed-state" style="display: none" onclick="toggleSidebar(); return false" title="${_('Observers')}">
751 this.editFields.hide();
629 ## <i class="icon-eye"></i>
752 this.closeButton.hide();
630 ## 0
753 this.viewFields.show();
631 ## </div>
754 }
632 ##
755 };
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>
756
643
757 var ReviewersPanel = {
644 ## TODOs
758 editButton: $('#open_edit_reviewers'),
645 <div class="sidebar-element clear-both">
759 closeButton: $('#close_edit_reviewers'),
646 <div class="tooltip right-sidebar-collapsed-state" style="display: none" onclick="toggleSidebar(); return false" title="TODOs">
760 addButton: $('#add_reviewer'),
647 <i class="icon-flag-filled"></i>
761 removeButtons: $('.reviewer_member_remove,.reviewer_member_mandatory_remove'),
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">
762
668
763 init: function () {
669 % if c.at_version:
764 var self = this;
670 <table>
765 this.editButton.on('click', function (e) {
671 <tr>
766 self.edit();
672 <td class="unresolved-todo-text">${_('TODOs unavailable when browsing versions')}.</td>
767 });
673 </tr>
768 this.closeButton.on('click', function (e) {
674 </table>
769 self.close();
675 % else:
770 });
676 % if c.unresolved_comments + c.resolved_comments:
771 },
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>
772
690
773 edit: function (event) {
691 ## COMMENTS
774 this.editButton.hide();
692 <div class="sidebar-element clear-both">
775 this.closeButton.show();
693 <div class="tooltip right-sidebar-collapsed-state" style="display: none" onclick="toggleSidebar(); return false" title="${_('Comments')}">
776 this.addButton.show();
694 <i class="icon-comment" style="color: #949494"></i>
777 this.removeButtons.css('visibility', 'visible');
695 <span id="comments-count">${len(c.inline_comments_flat+c.comments)}</span>
778 // review rules
696 <span class="display-none" id="general-comments-count">${len(c.comments)}</span>
779 reviewersController.loadReviewRules(
697 <span class="display-none" id="inline-comments-count">${len(c.inline_comments_flat)}</span>
780 ${c.pull_request.reviewer_data_json | n});
698 </div>
781 },
782
699
783 close: function (event) {
700 <div class="right-sidebar-expanded-state pr-details-title">
784 this.editButton.show();
701 <span class="sidebar-heading noselect" onclick="refreshComments(); return false">
785 this.closeButton.hide();
702 <i class="icon-comment" style="color: #949494"></i>
786 this.addButton.hide();
703 ${_('Comments')}
787 this.removeButtons.css('visibility', 'hidden');
704
788 // hide review rules
705 ## % if outdated_comm_count_ver:
789 reviewersController.hideReviewRules()
706 ## <a href="#" onclick="showOutdated(); Rhodecode.comments.nextOutdatedComment(); return false;">
790 }
707 ## (${_("{} Outdated").format(outdated_comm_count_ver)})
791 };
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>
792
711
793 PRDetails.init();
712 ## % else:
794 ReviewersPanel.init();
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>
795
725
796 showOutdated = function (self) {
726 <div class="right-sidebar-expanded-state pr-details-content">
797 $('.comment-inline.comment-outdated').show();
727 % if c.inline_comments_flat + c.comments:
798 $('.filediff-outdated').show();
728 ${sidebar.comments_table(c.inline_comments_flat + c.comments, len(c.inline_comments_flat+c.comments))}
799 $('.showOutdatedComments').hide();
729 % else:
800 $('.hideOutdatedComments').show();
730 <table>
801 };
731 <tr>
732 <td>
733 ${_('No Comments yet')}
734 </td>
735 </tr>
736 </table>
737 % endif
738 </div>
739
740 </div>
802
741
803 hideOutdated = function (self) {
742 ## Referenced Tickets
804 $('.comment-inline.comment-outdated').hide();
743 <div class="sidebar-element clear-both">
805 $('.filediff-outdated').hide();
744 <div class="tooltip right-sidebar-collapsed-state" style="display: none" onclick="toggleSidebar(); return false" title="${_('Referenced Tickets')}">
806 $('.hideOutdatedComments').hide();
745 <i class="icon-info-circled"></i>
807 $('.showOutdatedComments').show();
746 ${(len(c.referenced_desc_issues) + len(c.referenced_commit_issues))}
808 };
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>
809
757
810 refreshMergeChecks = function () {
758 <tr><td><code>${_('In pull request description')}:</code></td></tr>
811 var loadUrl = "${request.current_route_path(_query=dict(merge_checks=1))}";
759 % if c.referenced_desc_issues:
812 $('.pull-request-merge').css('opacity', 0.3);
760 % for ticket_dict in c.referenced_desc_issues:
813 $('.action-buttons-extra').css('opacity', 0.3);
761 <tr>
814
762 <td>
815 $('.pull-request-merge').load(
763 <a href="${ticket_dict.get('url')}">
816 loadUrl, function () {
764 ${ticket_dict.get('id')}
817 $('.pull-request-merge').css('opacity', 1);
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
818
776
819 $('.action-buttons-extra').css('opacity', 1);
777 <tr><td style="padding-top: 10px"><code>${_('In commit messages')}:</code></td></tr>
820 }
778 % if c.referenced_commit_issues:
821 );
779 % for ticket_dict in c.referenced_commit_issues:
822 };
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>
823
796
824 closePullRequest = function (status) {
797 </div>
825 if (!confirm(_gettext('Are you sure to close this pull request without merging?'))) {
798 </div>
826 return false;
799
827 }
800 </div>
828 // inject closing flag
801
829 $('.action-buttons-extra').append('<input type="hidden" class="close-pr-input" id="close_pull_request" value="1">');
802 </div>
830 $(generalCommentForm.statusChange).select2("val", status).trigger('change');
803 </aside>
831 $(generalCommentForm.submitForm).submit();
804
832 };
805 ## This JS needs to be at the end
806 <script type="text/javascript">
807
808 versionController = new VersionController();
809 versionController.init();
810
811 reviewersController = new ReviewersController();
812 commitsController = new CommitsController();
833
813
834 $('#show-outdated-comments').on('click', function (e) {
814 updateController = new UpdatePrController();
835 var button = $(this);
815
836 var outdated = $('.comment-outdated');
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";
821
822 // custom code mirror
823 var codeMirrorInstance = $('#pr-description-input').get(0).MarkupForm.cm;
837
824
838 if (button.html() === "(Show)") {
825 var PRDetails = {
839 button.html("(Hide)");
826 editButton: $('#open_edit_pullrequest'),
840 outdated.show();
827 closeButton: $('#close_edit_pullrequest'),
841 } else {
828 deleteButton: $('#delete_pullrequest'),
842 button.html("(Show)");
829 viewFields: $('#pr-desc, #pr-title'),
843 outdated.hide();
830 editFields: $('#pr-desc-edit, #pr-title-edit, .pr-save'),
844 }
831
845 });
832 init: function () {
833 var that = this;
834 this.editButton.on('click', function (e) {
835 that.edit();
836 });
837 this.closeButton.on('click', function (e) {
838 that.view();
839 });
840 },
846
841
847 $('.show-inline-comments').on('change', function (e) {
842 edit: function (event) {
848 var show = 'none';
843 var cmInstance = $('#pr-description-input').get(0).MarkupForm.cm;
849 var target = e.currentTarget;
844 this.viewFields.hide();
850 if (target.checked) {
845 this.editButton.hide();
851 show = ''
846 this.deleteButton.hide();
852 }
847 this.closeButton.show();
853 var boxid = $(target).attr('id_for');
848 this.editFields.show();
854 var comments = $('#{0} .inline-comments'.format(boxid));
849 cmInstance.refresh();
855 var fn_display = function (idx) {
850 },
856 $(this).css('display', show);
851
857 };
852 view: function (event) {
858 $(comments).each(fn_display);
853 this.editButton.show();
859 var btns = $('#{0} .inline-comments-button'.format(boxid));
854 this.deleteButton.show();
860 $(btns).each(fn_display);
855 this.editFields.hide();
861 });
856 this.closeButton.hide();
857 this.viewFields.show();
858 }
859 };
860
861 PRDetails.init();
862 ReviewersPanel.init(reviewerRulesData, setReviewersData);
863
864 window.showOutdated = function (self) {
865 $('.comment-inline.comment-outdated').show();
866 $('.filediff-outdated').show();
867 $('.showOutdatedComments').hide();
868 $('.hideOutdatedComments').show();
869 };
862
870
863 $('#merge_pull_request_form').submit(function () {
871 window.hideOutdated = function (self) {
864 if (!$('#merge_pull_request').attr('disabled')) {
872 $('.comment-inline.comment-outdated').hide();
865 $('#merge_pull_request').attr('disabled', 'disabled');
873 $('.filediff-outdated').hide();
866 }
874 $('.hideOutdatedComments').hide();
867 return true;
875 $('.showOutdatedComments').show();
868 });
876 };
877
878 window.refreshMergeChecks = function () {
879 var loadUrl = "${request.current_route_path(_query=dict(merge_checks=1))}";
880 $('.pull-request-merge').css('opacity', 0.3);
881 $('.action-buttons-extra').css('opacity', 0.3);
882
883 $('.pull-request-merge').load(
884 loadUrl, function () {
885 $('.pull-request-merge').css('opacity', 1);
886
887 $('.action-buttons-extra').css('opacity', 1);
888 }
889 );
890 };
869
891
870 $('#edit_pull_request').on('click', function (e) {
892 window.closePullRequest = function (status) {
871 var title = $('#pr-title-input').val();
893 if (!confirm(_gettext('Are you sure to close this pull request without merging?'))) {
872 var description = codeMirrorInstance.getValue();
894 return false;
873 var renderer = $('#pr-renderer-input').val();
895 }
874 editPullRequest(
896 // inject closing flag
875 "${c.repo_name}", "${c.pull_request.pull_request_id}",
897 $('.action-buttons-extra').append('<input type="hidden" class="close-pr-input" id="close_pull_request" value="1">');
876 title, description, renderer);
898 $(generalCommentForm.statusChange).select2("val", status).trigger('change');
877 });
899 $(generalCommentForm.submitForm).submit();
900 };
901
902 //TODO this functionality is now missing
903 $('#show-outdated-comments').on('click', function (e) {
904 var button = $(this);
905 var outdated = $('.comment-outdated');
878
906
879 $('#update_pull_request').on('click', function (e) {
907 if (button.html() === "(Show)") {
880 $(this).attr('disabled', 'disabled');
908 button.html("(Hide)");
881 $(this).addClass('disabled');
909 outdated.show();
882 $(this).html(_gettext('Saving...'));
910 } else {
883 reviewersController.updateReviewers(
911 button.html("(Show)");
884 "${c.repo_name}", "${c.pull_request.pull_request_id}");
912 outdated.hide();
885 });
913 }
914 });
886
915
916 $('#merge_pull_request_form').submit(function () {
917 if (!$('#merge_pull_request').attr('disabled')) {
918 $('#merge_pull_request').attr('disabled', 'disabled');
919 }
920 return true;
921 });
887
922
888 // fixing issue with caches on firefox
923 $('#edit_pull_request').on('click', function (e) {
889 $('#update_commits').removeAttr("disabled");
924 var title = $('#pr-title-input').val();
925 var description = codeMirrorInstance.getValue();
926 var renderer = $('#pr-renderer-input').val();
927 editPullRequest(
928 "${c.repo_name}", "${c.pull_request.pull_request_id}",
929 title, description, renderer);
930 });
890
931
891 $('.show-inline-comments').on('click', function (e) {
932 $('#update_pull_request').on('click', function (e) {
892 var boxid = $(this).attr('data-comment-id');
933 $(this).attr('disabled', 'disabled');
893 var button = $(this);
934 $(this).addClass('disabled');
935 $(this).html(_gettext('Saving...'));
936 reviewersController.updateReviewers(
937 "${c.repo_name}", "${c.pull_request.pull_request_id}");
938 });
939
940 // fixing issue with caches on firefox
941 $('#update_commits').removeAttr("disabled");
942
943 $('.show-inline-comments').on('click', function (e) {
944 var boxid = $(this).attr('data-comment-id');
945 var button = $(this);
946
947 if (button.hasClass("comments-visible")) {
948 $('#{0} .inline-comments'.format(boxid)).each(function (index) {
949 $(this).hide();
950 });
951 button.removeClass("comments-visible");
952 } else {
953 $('#{0} .inline-comments'.format(boxid)).each(function (index) {
954 $(this).show();
955 });
956 button.addClass("comments-visible");
957 }
958 });
894
959
895 if (button.hasClass("comments-visible")) {
960 $('.show-inline-comments').on('change', function (e) {
896 $('#{0} .inline-comments'.format(boxid)).each(function (index) {
961 var show = 'none';
897 $(this).hide();
962 var target = e.currentTarget;
898 });
963 if (target.checked) {
899 button.removeClass("comments-visible");
964 show = ''
900 } else {
965 }
901 $('#{0} .inline-comments'.format(boxid)).each(function (index) {
966 var boxid = $(target).attr('id_for');
902 $(this).show();
967 var comments = $('#{0} .inline-comments'.format(boxid));
903 });
968 var fn_display = function (idx) {
904 button.addClass("comments-visible");
969 $(this).css('display', show);
905 }
970 };
906 });
971 $(comments).each(fn_display);
972 var btns = $('#{0} .inline-comments-button'.format(boxid));
973 $(btns).each(fn_display);
974 });
907
975
908 // register submit callback on commentForm form to track TODOs
976 // register submit callback on commentForm form to track TODOs
909 window.commentFormGlobalSubmitSuccessCallback = function () {
977 window.commentFormGlobalSubmitSuccessCallback = function () {
910 refreshMergeChecks();
978 refreshMergeChecks();
911 };
979 };
980
981 ReviewerAutoComplete('#user');
912
982
913 ReviewerAutoComplete('#user');
983 })();
914
984
915 })
985 $(document).ready(function () {
916
986
917 </script>
987 var channel = '${c.pr_broadcast_channel}';
988 new ReviewerPresenceController(channel)
918
989
919 </div>
990 })
991 </script>
920
992
921 </%def>
993 </%def>
@@ -948,8 +948,8 b' def assert_inline_comments(pull_request,'
948 if visible is not None:
948 if visible is not None:
949 inline_comments = CommentsModel().get_inline_comments(
949 inline_comments = CommentsModel().get_inline_comments(
950 pull_request.target_repo.repo_id, pull_request=pull_request)
950 pull_request.target_repo.repo_id, pull_request=pull_request)
951 inline_cnt = CommentsModel().get_inline_comments_count(
951 inline_cnt = len(CommentsModel().get_inline_comments_as_list(
952 inline_comments)
952 inline_comments))
953 assert inline_cnt == visible
953 assert inline_cnt == visible
954 if outdated is not None:
954 if outdated is not None:
955 outdated_comments = CommentsModel().get_outdated_comments(
955 outdated_comments = CommentsModel().get_outdated_comments(
General Comments 0
You need to be logged in to leave comments. Login now