##// END OF EJS Templates
repo-switcher: new unified search box for filtering/accessing users, repos and repo groups....
marcink -
r2774:a8ecef4e default
parent child Browse files
Show More
@@ -1,40 +1,62 b''
1 1 # -*- coding: utf-8 -*-
2 2
3 3 # Copyright (C) 2016-2018 RhodeCode GmbH
4 4 #
5 5 # This program is free software: you can redistribute it and/or modify
6 6 # it under the terms of the GNU Affero General Public License, version 3
7 7 # (only), as published by the Free Software Foundation.
8 8 #
9 9 # This program is distributed in the hope that it will be useful,
10 10 # but WITHOUT ANY WARRANTY; without even the implied warranty of
11 11 # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
12 12 # GNU General Public License for more details.
13 13 #
14 14 # You should have received a copy of the GNU Affero General Public License
15 15 # along with this program. If not, see <http://www.gnu.org/licenses/>.
16 16 #
17 17 # This program is dual-licensed. If you wish to learn more about the
18 18 # RhodeCode Enterprise Edition, including its added features, Support services,
19 19 # and proprietary license terms, please see https://rhodecode.com/licenses/
20 20
21 21
22 def assert_and_get_content(result):
22 def assert_and_get_main_filter_content(result):
23 23 repos = []
24 24 groups = []
25 25 commits = []
26 users = []
27 for data_item in result:
28 assert data_item['id']
29 assert data_item['value']
30 assert data_item['value_display']
31 assert data_item['url']
32
33 if data_item['type'] == 'search':
34 assert data_item['value_display'].startswith('Full text search for:')
35 elif data_item['type'] == 'repo':
36 repos.append(data_item)
37 elif data_item['type'] == 'repo_group':
38 groups.append(data_item)
39 elif data_item['type'] == 'user':
40 users.append(data_item)
41 elif data_item['type'] == 'commit':
42 commits.append(data_item)
43 else:
44 raise Exception('invalid type `%s`' % data_item['type'])
45
46 return repos, groups, users, commits
47
48
49 def assert_and_get_repo_list_content(result):
50 repos = []
26 51 for data in result:
27 52 for data_item in data['children']:
28 53 assert data_item['id']
29 54 assert data_item['text']
30 55 assert data_item['url']
56
31 57 if data_item['type'] == 'repo':
32 58 repos.append(data_item)
33 elif data_item['type'] == 'group':
34 groups.append(data_item)
35 elif data_item['type'] == 'commit':
36 commits.append(data_item)
37 59 else:
38 60 raise Exception('invalid type %s' % data_item['type'])
39 61
40 return repos, groups, commits No newline at end of file
62 return repos
@@ -1,151 +1,180 b''
1 1 # -*- coding: utf-8 -*-
2 2
3 3 # Copyright (C) 2016-2018 RhodeCode GmbH
4 4 #
5 5 # This program is free software: you can redistribute it and/or modify
6 6 # it under the terms of the GNU Affero General Public License, version 3
7 7 # (only), as published by the Free Software Foundation.
8 8 #
9 9 # This program is distributed in the hope that it will be useful,
10 10 # but WITHOUT ANY WARRANTY; without even the implied warranty of
11 11 # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
12 12 # GNU General Public License for more details.
13 13 #
14 14 # You should have received a copy of the GNU Affero General Public License
15 15 # along with this program. If not, see <http://www.gnu.org/licenses/>.
16 16 #
17 17 # This program is dual-licensed. If you wish to learn more about the
18 18 # RhodeCode Enterprise Edition, including its added features, Support services,
19 19 # and proprietary license terms, please see https://rhodecode.com/licenses/
20 20
21 21 import json
22 22
23 23 import pytest
24 24
25 from . import assert_and_get_content
25 from . import assert_and_get_main_filter_content
26 26 from rhodecode.tests import TestController, TEST_USER_ADMIN_LOGIN
27 27 from rhodecode.tests.fixture import Fixture
28 28
29 29 from rhodecode.lib.utils import map_groups
30 30 from rhodecode.model.repo import RepoModel
31 31 from rhodecode.model.repo_group import RepoGroupModel
32 32 from rhodecode.model.db import Session, Repository, RepoGroup
33 33
34 34 fixture = Fixture()
35 35
36 36
37 37 def route_path(name, params=None, **kwargs):
38 38 import urllib
39 39
40 40 base_url = {
41 41 'goto_switcher_data': '/_goto_data',
42 42 }[name].format(**kwargs)
43 43
44 44 if params:
45 45 base_url = '{}?{}'.format(base_url, urllib.urlencode(params))
46 46 return base_url
47 47
48 48
49 49 class TestGotoSwitcherData(TestController):
50 50
51 51 required_repos_with_groups = [
52 52 'abc',
53 53 'abc-fork',
54 54 'forks/abcd',
55 55 'abcd',
56 56 'abcde',
57 57 'a/abc',
58 58 'aa/abc',
59 59 'aaa/abc',
60 60 'aaaa/abc',
61 61 'repos_abc/aaa/abc',
62 62 'abc_repos/abc',
63 63 'abc_repos/abcd',
64 64 'xxx/xyz',
65 65 'forked-abc/a/abc'
66 66 ]
67 67
68 68 @pytest.fixture(autouse=True, scope='class')
69 69 def prepare(self, request, baseapp):
70 70 for repo_and_group in self.required_repos_with_groups:
71 71 # create structure of groups and return the last group
72 72
73 73 repo_group = map_groups(repo_and_group)
74 74
75 75 RepoModel()._create_repo(
76 76 repo_and_group, 'hg', 'test-ac', TEST_USER_ADMIN_LOGIN,
77 77 repo_group=getattr(repo_group, 'group_id', None))
78 78
79 79 Session().commit()
80 80
81 81 request.addfinalizer(self.cleanup)
82 82
83 83 def cleanup(self):
84 84 # first delete all repos
85 85 for repo_and_groups in self.required_repos_with_groups:
86 86 repo = Repository.get_by_repo_name(repo_and_groups)
87 87 if repo:
88 88 RepoModel().delete(repo)
89 89 Session().commit()
90 90
91 91 # then delete all empty groups
92 92 for repo_and_groups in self.required_repos_with_groups:
93 93 if '/' in repo_and_groups:
94 94 r_group = repo_and_groups.rsplit('/', 1)[0]
95 95 repo_group = RepoGroup.get_by_group_name(r_group)
96 96 if not repo_group:
97 97 continue
98 98 parents = repo_group.parents
99 99 RepoGroupModel().delete(repo_group, force_delete=True)
100 100 Session().commit()
101 101
102 102 for el in reversed(parents):
103 103 RepoGroupModel().delete(el, force_delete=True)
104 104 Session().commit()
105 105
106 def test_returns_list_of_repos_and_groups(self, xhr_header):
106 def test_empty_query(self, xhr_header):
107 107 self.log_user()
108 108
109 109 response = self.app.get(
110 110 route_path('goto_switcher_data'),
111 111 extra_environ=xhr_header, status=200)
112 result = json.loads(response.body)['results']
113
114 repos, groups, commits = assert_and_get_content(result)
112 result = json.loads(response.body)['suggestions']
115 113
116 assert len(repos) == len(Repository.get_all())
117 assert len(groups) == len(RepoGroup.get_all())
118 assert len(commits) == 0
114 assert result == []
119 115
120 116 def test_returns_list_of_repos_and_groups_filtered(self, xhr_header):
121 117 self.log_user()
122 118
123 119 response = self.app.get(
124 120 route_path('goto_switcher_data'),
125 121 params={'query': 'abc'},
126 122 extra_environ=xhr_header, status=200)
127 result = json.loads(response.body)['results']
123 result = json.loads(response.body)['suggestions']
128 124
129 repos, groups, commits = assert_and_get_content(result)
125 repos, groups, users, commits = assert_and_get_main_filter_content(result)
130 126
131 127 assert len(repos) == 13
132 128 assert len(groups) == 5
129 assert len(users) == 0
133 130 assert len(commits) == 0
134 131
132 def test_returns_list_of_users_filtered(self, xhr_header):
133 self.log_user()
134
135 response = self.app.get(
136 route_path('goto_switcher_data'),
137 params={'query': 'user:admin'},
138 extra_environ=xhr_header, status=200)
139 result = json.loads(response.body)['suggestions']
140
141 repos, groups, users, commits = assert_and_get_main_filter_content(result)
142
143 assert len(repos) == 0
144 assert len(groups) == 0
145 assert len(users) == 1
146 assert len(commits) == 0
147
148 def test_returns_list_of_commits_filtered(self, xhr_header):
149 self.log_user()
150
151 response = self.app.get(
152 route_path('goto_switcher_data'),
153 params={'query': 'commit:e8'},
154 extra_environ=xhr_header, status=200)
155 result = json.loads(response.body)['suggestions']
156
157 repos, groups, users, commits = assert_and_get_main_filter_content(result)
158
159 assert len(repos) == 0
160 assert len(groups) == 0
161 assert len(users) == 0
162 assert len(commits) == 5
163
135 164 def test_returns_list_of_properly_sorted_and_filtered(self, xhr_header):
136 165 self.log_user()
137 166
138 167 response = self.app.get(
139 168 route_path('goto_switcher_data'),
140 169 params={'query': 'abc'},
141 170 extra_environ=xhr_header, status=200)
142 result = json.loads(response.body)['results']
171 result = json.loads(response.body)['suggestions']
143 172
144 repos, groups, commits = assert_and_get_content(result)
173 repos, groups, users, commits = assert_and_get_main_filter_content(result)
145 174
146 test_repos = [x['text'] for x in repos[:4]]
175 test_repos = [x['value_display'] for x in repos[:4]]
147 176 assert ['abc', 'abcd', 'a/abc', 'abcde'] == test_repos
148 177
149 test_groups = [x['text'] for x in groups[:4]]
178 test_groups = [x['value_display'] for x in groups[:4]]
150 179 assert ['abc_repos', 'repos_abc',
151 180 'forked-abc', 'forked-abc/a'] == test_groups
@@ -1,103 +1,95 b''
1 1 # -*- coding: utf-8 -*-
2 2
3 3 # Copyright (C) 2016-2018 RhodeCode GmbH
4 4 #
5 5 # This program is free software: you can redistribute it and/or modify
6 6 # it under the terms of the GNU Affero General Public License, version 3
7 7 # (only), as published by the Free Software Foundation.
8 8 #
9 9 # This program is distributed in the hope that it will be useful,
10 10 # but WITHOUT ANY WARRANTY; without even the implied warranty of
11 11 # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
12 12 # GNU General Public License for more details.
13 13 #
14 14 # You should have received a copy of the GNU Affero General Public License
15 15 # along with this program. If not, see <http://www.gnu.org/licenses/>.
16 16 #
17 17 # This program is dual-licensed. If you wish to learn more about the
18 18 # RhodeCode Enterprise Edition, including its added features, Support services,
19 19 # and proprietary license terms, please see https://rhodecode.com/licenses/
20 20
21 21 import json
22 22
23 from . import assert_and_get_content
23 from . import assert_and_get_repo_list_content
24 24 from rhodecode.tests import TestController
25 25 from rhodecode.tests.fixture import Fixture
26 26 from rhodecode.model.db import Repository
27 27
28 28 fixture = Fixture()
29 29
30 30
31 31 def route_path(name, params=None, **kwargs):
32 32 import urllib
33 33
34 34 base_url = {
35 35 'repo_list_data': '/_repos',
36 36 }[name].format(**kwargs)
37 37
38 38 if params:
39 39 base_url = '{}?{}'.format(base_url, urllib.urlencode(params))
40 40 return base_url
41 41
42 42
43 43 class TestRepoListData(TestController):
44 44
45 45 def test_returns_list_of_repos_and_groups(self, xhr_header):
46 46 self.log_user()
47 47
48 48 response = self.app.get(
49 49 route_path('repo_list_data'),
50 50 extra_environ=xhr_header, status=200)
51 51 result = json.loads(response.body)['results']
52 52
53 repos, groups, commits = assert_and_get_content(result)
53 repos = assert_and_get_repo_list_content(result)
54 54
55 55 assert len(repos) == len(Repository.get_all())
56 assert len(groups) == 0
57 assert len(commits) == 0
58 56
59 57 def test_returns_list_of_repos_and_groups_filtered(self, xhr_header):
60 58 self.log_user()
61 59
62 60 response = self.app.get(
63 61 route_path('repo_list_data'),
64 62 params={'query': 'vcs_test_git'},
65 63 extra_environ=xhr_header, status=200)
66 64 result = json.loads(response.body)['results']
67 65
68 repos, groups, commits = assert_and_get_content(result)
66 repos = assert_and_get_repo_list_content(result)
69 67
70 68 assert len(repos) == len(Repository.query().filter(
71 69 Repository.repo_name.ilike('%vcs_test_git%')).all())
72 assert len(groups) == 0
73 assert len(commits) == 0
74 70
75 71 def test_returns_list_of_repos_and_groups_filtered_with_type(self, xhr_header):
76 72 self.log_user()
77 73
78 74 response = self.app.get(
79 75 route_path('repo_list_data'),
80 76 params={'query': 'vcs_test_git', 'repo_type': 'git'},
81 77 extra_environ=xhr_header, status=200)
82 78 result = json.loads(response.body)['results']
83 79
84 repos, groups, commits = assert_and_get_content(result)
80 repos = assert_and_get_repo_list_content(result)
85 81
86 82 assert len(repos) == len(Repository.query().filter(
87 83 Repository.repo_name.ilike('%vcs_test_git%')).all())
88 assert len(groups) == 0
89 assert len(commits) == 0
90 84
91 85 def test_returns_list_of_repos_non_ascii_query(self, xhr_header):
92 86 self.log_user()
93 87 response = self.app.get(
94 88 route_path('repo_list_data'),
95 89 params={'query': 'Δ‡_vcs_test_Δ…', 'repo_type': 'git'},
96 90 extra_environ=xhr_header, status=200)
97 91 result = json.loads(response.body)['results']
98 92
99 repos, groups, commits = assert_and_get_content(result)
93 repos = assert_and_get_repo_list_content(result)
100 94
101 95 assert len(repos) == 0
102 assert len(groups) == 0
103 assert len(commits) == 0
@@ -1,325 +1,374 b''
1 1 # -*- coding: utf-8 -*-
2 2
3 3 # Copyright (C) 2016-2018 RhodeCode GmbH
4 4 #
5 5 # This program is free software: you can redistribute it and/or modify
6 6 # it under the terms of the GNU Affero General Public License, version 3
7 7 # (only), as published by the Free Software Foundation.
8 8 #
9 9 # This program is distributed in the hope that it will be useful,
10 10 # but WITHOUT ANY WARRANTY; without even the implied warranty of
11 11 # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
12 12 # GNU General Public License for more details.
13 13 #
14 14 # You should have received a copy of the GNU Affero General Public License
15 15 # along with this program. If not, see <http://www.gnu.org/licenses/>.
16 16 #
17 17 # This program is dual-licensed. If you wish to learn more about the
18 18 # RhodeCode Enterprise Edition, including its added features, Support services,
19 19 # and proprietary license terms, please see https://rhodecode.com/licenses/
20 20
21 21 import re
22 22 import logging
23 import collections
23 24
24 25 from pyramid.view import view_config
25 26
26 27 from rhodecode.apps._base import BaseAppView
27 28 from rhodecode.lib import helpers as h
28 29 from rhodecode.lib.auth import (
29 30 LoginRequired, NotAnonymous, HasRepoGroupPermissionAnyDecorator)
30 31 from rhodecode.lib.index import searcher_from_config
31 32 from rhodecode.lib.utils2 import safe_unicode, str2bool
32 33 from rhodecode.lib.ext_json import json
33 34 from rhodecode.model.db import (
34 func, or_, in_filter_generator, Repository, RepoGroup)
35 func, or_, in_filter_generator, Repository, RepoGroup, User, UserGroup)
35 36 from rhodecode.model.repo import RepoModel
36 37 from rhodecode.model.repo_group import RepoGroupModel
37 38 from rhodecode.model.scm import RepoGroupList, RepoList
38 39 from rhodecode.model.user import UserModel
39 40 from rhodecode.model.user_group import UserGroupModel
40 41
41 42 log = logging.getLogger(__name__)
42 43
43 44
44 45 class HomeView(BaseAppView):
45 46
46 47 def load_default_context(self):
47 48 c = self._get_local_tmpl_context()
48 49 c.user = c.auth_user.get_instance()
49 50
50 51 return c
51 52
52 53 @LoginRequired()
53 54 @view_config(
54 55 route_name='user_autocomplete_data', request_method='GET',
55 56 renderer='json_ext', xhr=True)
56 57 def user_autocomplete_data(self):
57 58 self.load_default_context()
58 59 query = self.request.GET.get('query')
59 60 active = str2bool(self.request.GET.get('active') or True)
60 61 include_groups = str2bool(self.request.GET.get('user_groups'))
61 62 expand_groups = str2bool(self.request.GET.get('user_groups_expand'))
62 63 skip_default_user = str2bool(self.request.GET.get('skip_default_user'))
63 64
64 65 log.debug('generating user list, query:%s, active:%s, with_groups:%s',
65 66 query, active, include_groups)
66 67
67 68 _users = UserModel().get_users(
68 69 name_contains=query, only_active=active)
69 70
70 71 def maybe_skip_default_user(usr):
71 72 if skip_default_user and usr['username'] == UserModel.cls.DEFAULT_USER:
72 73 return False
73 74 return True
74 75 _users = filter(maybe_skip_default_user, _users)
75 76
76 77 if include_groups:
77 78 # extend with user groups
78 79 _user_groups = UserGroupModel().get_user_groups(
79 80 name_contains=query, only_active=active,
80 81 expand_groups=expand_groups)
81 82 _users = _users + _user_groups
82 83
83 84 return {'suggestions': _users}
84 85
85 86 @LoginRequired()
86 87 @NotAnonymous()
87 88 @view_config(
88 89 route_name='user_group_autocomplete_data', request_method='GET',
89 90 renderer='json_ext', xhr=True)
90 91 def user_group_autocomplete_data(self):
91 92 self.load_default_context()
92 93 query = self.request.GET.get('query')
93 94 active = str2bool(self.request.GET.get('active') or True)
94 95 expand_groups = str2bool(self.request.GET.get('user_groups_expand'))
95 96
96 97 log.debug('generating user group list, query:%s, active:%s',
97 98 query, active)
98 99
99 100 _user_groups = UserGroupModel().get_user_groups(
100 101 name_contains=query, only_active=active,
101 102 expand_groups=expand_groups)
102 103 _user_groups = _user_groups
103 104
104 105 return {'suggestions': _user_groups}
105 106
106 107 def _get_repo_list(self, name_contains=None, repo_type=None, limit=20):
108 org_query = name_contains
107 109 allowed_ids = self._rhodecode_user.repo_acl_ids(
108 110 ['repository.read', 'repository.write', 'repository.admin'],
109 111 cache=False, name_filter=name_contains) or [-1]
110 112
111 113 query = Repository.query()\
112 114 .order_by(func.length(Repository.repo_name))\
113 115 .order_by(Repository.repo_name)\
114 116 .filter(or_(
115 117 # generate multiple IN to fix limitation problems
116 118 *in_filter_generator(Repository.repo_id, allowed_ids)
117 119 ))
118 120
119 121 if repo_type:
120 122 query = query.filter(Repository.repo_type == repo_type)
121 123
122 124 if name_contains:
123 125 ilike_expression = u'%{}%'.format(safe_unicode(name_contains))
124 126 query = query.filter(
125 127 Repository.repo_name.ilike(ilike_expression))
126 128 query = query.limit(limit)
127 129
128 acl_repo_iter = query
130 acl_iter = query
129 131
130 132 return [
131 133 {
132 134 'id': obj.repo_name,
135 'value': org_query,
136 'value_display': obj.repo_name,
133 137 'text': obj.repo_name,
134 138 'type': 'repo',
135 'obj': {'repo_type': obj.repo_type, 'private': obj.private,
136 'repo_id': obj.repo_id},
139 'repo_id': obj.repo_id,
140 'repo_type': obj.repo_type,
141 'private': obj.private,
137 142 'url': h.route_path('repo_summary', repo_name=obj.repo_name)
138 143 }
139 for obj in acl_repo_iter]
144 for obj in acl_iter]
140 145
141 146 def _get_repo_group_list(self, name_contains=None, limit=20):
147 org_query = name_contains
142 148 allowed_ids = self._rhodecode_user.repo_group_acl_ids(
143 149 ['group.read', 'group.write', 'group.admin'],
144 150 cache=False, name_filter=name_contains) or [-1]
145 151
146 152 query = RepoGroup.query()\
147 153 .order_by(func.length(RepoGroup.group_name))\
148 154 .order_by(RepoGroup.group_name) \
149 155 .filter(or_(
150 156 # generate multiple IN to fix limitation problems
151 157 *in_filter_generator(RepoGroup.group_id, allowed_ids)
152 158 ))
153 159
154 160 if name_contains:
155 161 ilike_expression = u'%{}%'.format(safe_unicode(name_contains))
156 162 query = query.filter(
157 163 RepoGroup.group_name.ilike(ilike_expression))
158 164 query = query.limit(limit)
159 165
160 acl_repo_iter = query
166 acl_iter = query
161 167
162 168 return [
163 169 {
164 170 'id': obj.group_name,
165 'text': obj.group_name,
166 'type': 'group',
167 'obj': {},
171 'value': org_query,
172 'value_display': obj.group_name,
173 'type': 'repo_group',
168 174 'url': h.route_path(
169 175 'repo_group_home', repo_group_name=obj.group_name)
170 176 }
171 for obj in acl_repo_iter]
177 for obj in acl_iter]
178
179 def _get_user_list(self, name_contains=None, limit=20):
180 org_query = name_contains
181 if not name_contains:
182 return []
183
184 name_contains = re.compile('(?:user:)(.+)').findall(name_contains)
185 if len(name_contains) != 1:
186 return []
187 name_contains = name_contains[0]
188
189 query = User.query()\
190 .order_by(func.length(User.username))\
191 .order_by(User.username) \
192 .filter(User.username != User.DEFAULT_USER)
172 193
173 def _get_hash_commit_list(self, auth_user, query=None):
194 if name_contains:
195 ilike_expression = u'%{}%'.format(safe_unicode(name_contains))
196 query = query.filter(
197 User.username.ilike(ilike_expression))
198 query = query.limit(limit)
199
200 acl_iter = query
201
202 return [
203 {
204 'id': obj.user_id,
205 'value': org_query,
206 'value_display': obj.username,
207 'type': 'user',
208 'icon_link': h.gravatar_url(obj.email, 30),
209 'url': h.route_path(
210 'user_profile', username=obj.username)
211 }
212 for obj in acl_iter]
213
214 def _get_hash_commit_list(self, auth_user, query):
215 org_query = query
174 216 if not query or len(query) < 3:
175 217 return []
176 218
177 219 commit_hashes = re.compile('(?:commit:)([0-9a-f]{2,40})').findall(query)
178 220
179 221 if len(commit_hashes) != 1:
180 222 return []
181
182 commit_hash_prefix = commit_hashes[0]
223 commit_hash = commit_hashes[0]
183 224
184 225 searcher = searcher_from_config(self.request.registry.settings)
185 226 result = searcher.search(
186 'commit_id:%s*' % commit_hash_prefix, 'commit', auth_user,
227 'commit_id:%s*' % commit_hash, 'commit', auth_user,
187 228 raise_on_exc=False)
188 229
189 230 return [
190 231 {
191 232 'id': entry['commit_id'],
192 'text': entry['commit_id'],
233 'value': org_query,
234 'value_display': 'repo `{}` commit: {}'.format(
235 entry['repository'], entry['commit_id']),
193 236 'type': 'commit',
194 'obj': {'repo': entry['repository']},
237 'repo': entry['repository'],
195 238 'url': h.route_path(
196 239 'repo_commit',
197 240 repo_name=entry['repository'], commit_id=entry['commit_id'])
198 241 }
199 242 for entry in result['results']]
200 243
201 244 @LoginRequired()
202 245 @view_config(
203 246 route_name='repo_list_data', request_method='GET',
204 247 renderer='json_ext', xhr=True)
205 248 def repo_list_data(self):
206 249 _ = self.request.translate
207 250 self.load_default_context()
208 251
209 252 query = self.request.GET.get('query')
210 253 repo_type = self.request.GET.get('repo_type')
211 254 log.debug('generating repo list, query:%s, repo_type:%s',
212 255 query, repo_type)
213 256
214 257 res = []
215 258 repos = self._get_repo_list(query, repo_type=repo_type)
216 259 if repos:
217 260 res.append({
218 261 'text': _('Repositories'),
219 262 'children': repos
220 263 })
221 264
222 265 data = {
223 266 'more': False,
224 267 'results': res
225 268 }
226 269 return data
227 270
228 271 @LoginRequired()
229 272 @view_config(
230 273 route_name='goto_switcher_data', request_method='GET',
231 274 renderer='json_ext', xhr=True)
232 275 def goto_switcher_data(self):
233 276 c = self.load_default_context()
234 277
235 278 _ = self.request.translate
236 279
237 280 query = self.request.GET.get('query')
238 log.debug('generating goto switcher list, query %s', query)
281 log.debug('generating main filter data, query %s', query)
239 282
283 default_search_val = 'Full text search for: `{}`'.format(query)
240 284 res = []
241 repo_groups = self._get_repo_group_list(query)
242 if repo_groups:
285 if not query:
286 return {'suggestions': res}
287
243 288 res.append({
244 'text': _('Groups'),
245 'children': repo_groups
289 'id': -1,
290 'value': query,
291 'value_display': default_search_val,
292 'type': 'search',
293 'url': h.route_path(
294 'search', _query={'q': query})
246 295 })
247 296
297 repo_groups = self._get_repo_group_list(query)
298 for serialized_repo_group in repo_groups:
299 res.append(serialized_repo_group)
300
248 301 repos = self._get_repo_list(query)
249 if repos:
250 res.append({
251 'text': _('Repositories'),
252 'children': repos
253 })
302 for serialized_repo in repos:
303 res.append(serialized_repo)
304
305 # TODO(marcink): permissions for that ?
306 users = self._get_user_list(query)
307 for serialized_user in users:
308 res.append(serialized_user)
254 309
255 310 commits = self._get_hash_commit_list(c.auth_user, query)
256 311 if commits:
257 unique_repos = {}
312 unique_repos = collections.OrderedDict()
258 313 for commit in commits:
259 unique_repos.setdefault(commit['obj']['repo'], []
260 ).append(commit)
314 repo_name = commit['repo']
315 unique_repos.setdefault(repo_name, []).append(commit)
261 316
262 for repo in unique_repos:
263 res.append({
264 'text': _('Commits in %(repo)s') % {'repo': repo},
265 'children': unique_repos[repo]
266 })
317 for repo, commits in unique_repos.items():
318 for commit in commits:
319 res.append(commit)
267 320
268 data = {
269 'more': False,
270 'results': res
271 }
272 return data
321 return {'suggestions': res}
273 322
274 323 def _get_groups_and_repos(self, repo_group_id=None):
275 324 # repo groups groups
276 325 repo_group_list = RepoGroup.get_all_repo_groups(group_id=repo_group_id)
277 326 _perms = ['group.read', 'group.write', 'group.admin']
278 327 repo_group_list_acl = RepoGroupList(repo_group_list, perm_set=_perms)
279 328 repo_group_data = RepoGroupModel().get_repo_groups_as_dict(
280 329 repo_group_list=repo_group_list_acl, admin=False)
281 330
282 331 # repositories
283 332 repo_list = Repository.get_all_repos(group_id=repo_group_id)
284 333 _perms = ['repository.read', 'repository.write', 'repository.admin']
285 334 repo_list_acl = RepoList(repo_list, perm_set=_perms)
286 335 repo_data = RepoModel().get_repos_as_dict(
287 336 repo_list=repo_list_acl, admin=False)
288 337
289 338 return repo_data, repo_group_data
290 339
291 340 @LoginRequired()
292 341 @view_config(
293 342 route_name='home', request_method='GET',
294 343 renderer='rhodecode:templates/index.mako')
295 344 def main_page(self):
296 345 c = self.load_default_context()
297 346 c.repo_group = None
298 347
299 348 repo_data, repo_group_data = self._get_groups_and_repos()
300 349 # json used to render the grids
301 350 c.repos_data = json.dumps(repo_data)
302 351 c.repo_groups_data = json.dumps(repo_group_data)
303 352
304 353 return self._get_template_context(c)
305 354
306 355 @LoginRequired()
307 356 @HasRepoGroupPermissionAnyDecorator(
308 357 'group.read', 'group.write', 'group.admin')
309 358 @view_config(
310 359 route_name='repo_group_home', request_method='GET',
311 360 renderer='rhodecode:templates/index_repo_group.mako')
312 361 @view_config(
313 362 route_name='repo_group_home_slash', request_method='GET',
314 363 renderer='rhodecode:templates/index_repo_group.mako')
315 364 def repo_group_main_page(self):
316 365 c = self.load_default_context()
317 366 c.repo_group = self.request.db_repo_group
318 367 repo_data, repo_group_data = self._get_groups_and_repos(
319 368 c.repo_group.group_id)
320 369
321 370 # json used to render the grids
322 371 c.repos_data = json.dumps(repo_data)
323 372 c.repo_groups_data = json.dumps(repo_group_data)
324 373
325 374 return self._get_template_context(c)
@@ -1,1298 +1,1301 b''
1 1 # -*- coding: utf-8 -*-
2 2
3 3 # Copyright (C) 2011-2018 RhodeCode GmbH
4 4 #
5 5 # This program is free software: you can redistribute it and/or modify
6 6 # it under the terms of the GNU Affero General Public License, version 3
7 7 # (only), as published by the Free Software Foundation.
8 8 #
9 9 # This program is distributed in the hope that it will be useful,
10 10 # but WITHOUT ANY WARRANTY; without even the implied warranty of
11 11 # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
12 12 # GNU General Public License for more details.
13 13 #
14 14 # You should have received a copy of the GNU Affero General Public License
15 15 # along with this program. If not, see <http://www.gnu.org/licenses/>.
16 16 #
17 17 # This program is dual-licensed. If you wish to learn more about the
18 18 # RhodeCode Enterprise Edition, including its added features, Support services,
19 19 # and proprietary license terms, please see https://rhodecode.com/licenses/
20 20
21 21 import logging
22 22 import collections
23 23
24 24 import formencode
25 25 import formencode.htmlfill
26 26 import peppercorn
27 27 from pyramid.httpexceptions import (
28 28 HTTPFound, HTTPNotFound, HTTPForbidden, HTTPBadRequest)
29 29 from pyramid.view import view_config
30 30 from pyramid.renderers import render
31 31
32 32 from rhodecode import events
33 33 from rhodecode.apps._base import RepoAppView, DataGridAppView
34 34
35 35 from rhodecode.lib import helpers as h, diffs, codeblocks, channelstream
36 36 from rhodecode.lib.base import vcs_operation_context
37 37 from rhodecode.lib.diffs import load_cached_diff, cache_diff, diff_cache_exist
38 38 from rhodecode.lib.ext_json import json
39 39 from rhodecode.lib.auth import (
40 40 LoginRequired, HasRepoPermissionAny, HasRepoPermissionAnyDecorator,
41 41 NotAnonymous, CSRFRequired)
42 42 from rhodecode.lib.utils2 import str2bool, safe_str, safe_unicode
43 43 from rhodecode.lib.vcs.backends.base import EmptyCommit, UpdateFailureReason
44 44 from rhodecode.lib.vcs.exceptions import (CommitDoesNotExistError,
45 45 RepositoryRequirementError, EmptyRepositoryError)
46 46 from rhodecode.model.changeset_status import ChangesetStatusModel
47 47 from rhodecode.model.comment import CommentsModel
48 48 from rhodecode.model.db import (func, or_, PullRequest, PullRequestVersion,
49 49 ChangesetComment, ChangesetStatus, Repository)
50 50 from rhodecode.model.forms import PullRequestForm
51 51 from rhodecode.model.meta import Session
52 52 from rhodecode.model.pull_request import PullRequestModel, MergeCheck
53 53 from rhodecode.model.scm import ScmModel
54 54
55 55 log = logging.getLogger(__name__)
56 56
57 57
58 58 class RepoPullRequestsView(RepoAppView, DataGridAppView):
59 59
60 60 def load_default_context(self):
61 61 c = self._get_local_tmpl_context(include_app_defaults=True)
62 62 c.REVIEW_STATUS_APPROVED = ChangesetStatus.STATUS_APPROVED
63 63 c.REVIEW_STATUS_REJECTED = ChangesetStatus.STATUS_REJECTED
64 64
65 65 return c
66 66
67 67 def _get_pull_requests_list(
68 68 self, repo_name, source, filter_type, opened_by, statuses):
69 69
70 70 draw, start, limit = self._extract_chunk(self.request)
71 71 search_q, order_by, order_dir = self._extract_ordering(self.request)
72 72 _render = self.request.get_partial_renderer(
73 73 'rhodecode:templates/data_table/_dt_elements.mako')
74 74
75 75 # pagination
76 76
77 77 if filter_type == 'awaiting_review':
78 78 pull_requests = PullRequestModel().get_awaiting_review(
79 79 repo_name, source=source, opened_by=opened_by,
80 80 statuses=statuses, offset=start, length=limit,
81 81 order_by=order_by, order_dir=order_dir)
82 82 pull_requests_total_count = PullRequestModel().count_awaiting_review(
83 83 repo_name, source=source, statuses=statuses,
84 84 opened_by=opened_by)
85 85 elif filter_type == 'awaiting_my_review':
86 86 pull_requests = PullRequestModel().get_awaiting_my_review(
87 87 repo_name, source=source, opened_by=opened_by,
88 88 user_id=self._rhodecode_user.user_id, statuses=statuses,
89 89 offset=start, length=limit, order_by=order_by,
90 90 order_dir=order_dir)
91 91 pull_requests_total_count = PullRequestModel().count_awaiting_my_review(
92 92 repo_name, source=source, user_id=self._rhodecode_user.user_id,
93 93 statuses=statuses, opened_by=opened_by)
94 94 else:
95 95 pull_requests = PullRequestModel().get_all(
96 96 repo_name, source=source, opened_by=opened_by,
97 97 statuses=statuses, offset=start, length=limit,
98 98 order_by=order_by, order_dir=order_dir)
99 99 pull_requests_total_count = PullRequestModel().count_all(
100 100 repo_name, source=source, statuses=statuses,
101 101 opened_by=opened_by)
102 102
103 103 data = []
104 104 comments_model = CommentsModel()
105 105 for pr in pull_requests:
106 106 comments = comments_model.get_all_comments(
107 107 self.db_repo.repo_id, pull_request=pr)
108 108
109 109 data.append({
110 110 'name': _render('pullrequest_name',
111 111 pr.pull_request_id, pr.target_repo.repo_name),
112 112 'name_raw': pr.pull_request_id,
113 113 'status': _render('pullrequest_status',
114 114 pr.calculated_review_status()),
115 115 'title': _render(
116 116 'pullrequest_title', pr.title, pr.description),
117 117 'description': h.escape(pr.description),
118 118 'updated_on': _render('pullrequest_updated_on',
119 119 h.datetime_to_time(pr.updated_on)),
120 120 'updated_on_raw': h.datetime_to_time(pr.updated_on),
121 121 'created_on': _render('pullrequest_updated_on',
122 122 h.datetime_to_time(pr.created_on)),
123 123 'created_on_raw': h.datetime_to_time(pr.created_on),
124 124 'author': _render('pullrequest_author',
125 125 pr.author.full_contact, ),
126 126 'author_raw': pr.author.full_name,
127 127 'comments': _render('pullrequest_comments', len(comments)),
128 128 'comments_raw': len(comments),
129 129 'closed': pr.is_closed(),
130 130 })
131 131
132 132 data = ({
133 133 'draw': draw,
134 134 'data': data,
135 135 'recordsTotal': pull_requests_total_count,
136 136 'recordsFiltered': pull_requests_total_count,
137 137 })
138 138 return data
139 139
140 140 @LoginRequired()
141 141 @HasRepoPermissionAnyDecorator(
142 142 'repository.read', 'repository.write', 'repository.admin')
143 143 @view_config(
144 144 route_name='pullrequest_show_all', request_method='GET',
145 145 renderer='rhodecode:templates/pullrequests/pullrequests.mako')
146 146 def pull_request_list(self):
147 147 c = self.load_default_context()
148 148
149 149 req_get = self.request.GET
150 150 c.source = str2bool(req_get.get('source'))
151 151 c.closed = str2bool(req_get.get('closed'))
152 152 c.my = str2bool(req_get.get('my'))
153 153 c.awaiting_review = str2bool(req_get.get('awaiting_review'))
154 154 c.awaiting_my_review = str2bool(req_get.get('awaiting_my_review'))
155 155
156 156 c.active = 'open'
157 157 if c.my:
158 158 c.active = 'my'
159 159 if c.closed:
160 160 c.active = 'closed'
161 161 if c.awaiting_review and not c.source:
162 162 c.active = 'awaiting'
163 163 if c.source and not c.awaiting_review:
164 164 c.active = 'source'
165 165 if c.awaiting_my_review:
166 166 c.active = 'awaiting_my'
167 167
168 168 return self._get_template_context(c)
169 169
170 170 @LoginRequired()
171 171 @HasRepoPermissionAnyDecorator(
172 172 'repository.read', 'repository.write', 'repository.admin')
173 173 @view_config(
174 174 route_name='pullrequest_show_all_data', request_method='GET',
175 175 renderer='json_ext', xhr=True)
176 176 def pull_request_list_data(self):
177 177 self.load_default_context()
178 178
179 179 # additional filters
180 180 req_get = self.request.GET
181 181 source = str2bool(req_get.get('source'))
182 182 closed = str2bool(req_get.get('closed'))
183 183 my = str2bool(req_get.get('my'))
184 184 awaiting_review = str2bool(req_get.get('awaiting_review'))
185 185 awaiting_my_review = str2bool(req_get.get('awaiting_my_review'))
186 186
187 187 filter_type = 'awaiting_review' if awaiting_review \
188 188 else 'awaiting_my_review' if awaiting_my_review \
189 189 else None
190 190
191 191 opened_by = None
192 192 if my:
193 193 opened_by = [self._rhodecode_user.user_id]
194 194
195 195 statuses = [PullRequest.STATUS_NEW, PullRequest.STATUS_OPEN]
196 196 if closed:
197 197 statuses = [PullRequest.STATUS_CLOSED]
198 198
199 199 data = self._get_pull_requests_list(
200 200 repo_name=self.db_repo_name, source=source,
201 201 filter_type=filter_type, opened_by=opened_by, statuses=statuses)
202 202
203 203 return data
204 204
205 205 def _is_diff_cache_enabled(self, target_repo):
206 206 caching_enabled = self._get_general_setting(
207 207 target_repo, 'rhodecode_diff_cache')
208 208 log.debug('Diff caching enabled: %s', caching_enabled)
209 209 return caching_enabled
210 210
211 211 def _get_diffset(self, source_repo_name, source_repo,
212 212 source_ref_id, target_ref_id,
213 213 target_commit, source_commit, diff_limit, file_limit,
214 214 fulldiff):
215 215
216 216 vcs_diff = PullRequestModel().get_diff(
217 217 source_repo, source_ref_id, target_ref_id)
218 218
219 219 diff_processor = diffs.DiffProcessor(
220 220 vcs_diff, format='newdiff', diff_limit=diff_limit,
221 221 file_limit=file_limit, show_full_diff=fulldiff)
222 222
223 223 _parsed = diff_processor.prepare()
224 224
225 225 diffset = codeblocks.DiffSet(
226 226 repo_name=self.db_repo_name,
227 227 source_repo_name=source_repo_name,
228 228 source_node_getter=codeblocks.diffset_node_getter(target_commit),
229 229 target_node_getter=codeblocks.diffset_node_getter(source_commit),
230 230 )
231 231 diffset = self.path_filter.render_patchset_filtered(
232 232 diffset, _parsed, target_commit.raw_id, source_commit.raw_id)
233 233
234 234 return diffset
235 235
236 236 @LoginRequired()
237 237 @HasRepoPermissionAnyDecorator(
238 238 'repository.read', 'repository.write', 'repository.admin')
239 239 @view_config(
240 240 route_name='pullrequest_show', request_method='GET',
241 241 renderer='rhodecode:templates/pullrequests/pullrequest_show.mako')
242 242 def pull_request_show(self):
243 243 pull_request_id = self.request.matchdict['pull_request_id']
244 244
245 245 c = self.load_default_context()
246 246
247 247 version = self.request.GET.get('version')
248 248 from_version = self.request.GET.get('from_version') or version
249 249 merge_checks = self.request.GET.get('merge_checks')
250 250 c.fulldiff = str2bool(self.request.GET.get('fulldiff'))
251 251
252 252 (pull_request_latest,
253 253 pull_request_at_ver,
254 254 pull_request_display_obj,
255 255 at_version) = PullRequestModel().get_pr_version(
256 256 pull_request_id, version=version)
257 257 pr_closed = pull_request_latest.is_closed()
258 258
259 259 if pr_closed and (version or from_version):
260 260 # not allow to browse versions
261 261 raise HTTPFound(h.route_path(
262 262 'pullrequest_show', repo_name=self.db_repo_name,
263 263 pull_request_id=pull_request_id))
264 264
265 265 versions = pull_request_display_obj.versions()
266 266
267 267 c.at_version = at_version
268 268 c.at_version_num = (at_version
269 269 if at_version and at_version != 'latest'
270 270 else None)
271 271 c.at_version_pos = ChangesetComment.get_index_from_version(
272 272 c.at_version_num, versions)
273 273
274 274 (prev_pull_request_latest,
275 275 prev_pull_request_at_ver,
276 276 prev_pull_request_display_obj,
277 277 prev_at_version) = PullRequestModel().get_pr_version(
278 278 pull_request_id, version=from_version)
279 279
280 280 c.from_version = prev_at_version
281 281 c.from_version_num = (prev_at_version
282 282 if prev_at_version and prev_at_version != 'latest'
283 283 else None)
284 284 c.from_version_pos = ChangesetComment.get_index_from_version(
285 285 c.from_version_num, versions)
286 286
287 287 # define if we're in COMPARE mode or VIEW at version mode
288 288 compare = at_version != prev_at_version
289 289
290 290 # pull_requests repo_name we opened it against
291 291 # ie. target_repo must match
292 292 if self.db_repo_name != pull_request_at_ver.target_repo.repo_name:
293 293 raise HTTPNotFound()
294 294
295 295 c.shadow_clone_url = PullRequestModel().get_shadow_clone_url(
296 296 pull_request_at_ver)
297 297
298 298 c.pull_request = pull_request_display_obj
299 299 c.pull_request_latest = pull_request_latest
300 300
301 301 if compare or (at_version and not at_version == 'latest'):
302 302 c.allowed_to_change_status = False
303 303 c.allowed_to_update = False
304 304 c.allowed_to_merge = False
305 305 c.allowed_to_delete = False
306 306 c.allowed_to_comment = False
307 307 c.allowed_to_close = False
308 308 else:
309 309 can_change_status = PullRequestModel().check_user_change_status(
310 310 pull_request_at_ver, self._rhodecode_user)
311 311 c.allowed_to_change_status = can_change_status and not pr_closed
312 312
313 313 c.allowed_to_update = PullRequestModel().check_user_update(
314 314 pull_request_latest, self._rhodecode_user) and not pr_closed
315 315 c.allowed_to_merge = PullRequestModel().check_user_merge(
316 316 pull_request_latest, self._rhodecode_user) and not pr_closed
317 317 c.allowed_to_delete = PullRequestModel().check_user_delete(
318 318 pull_request_latest, self._rhodecode_user) and not pr_closed
319 319 c.allowed_to_comment = not pr_closed
320 320 c.allowed_to_close = c.allowed_to_merge and not pr_closed
321 321
322 322 c.forbid_adding_reviewers = False
323 323 c.forbid_author_to_review = False
324 324 c.forbid_commit_author_to_review = False
325 325
326 326 if pull_request_latest.reviewer_data and \
327 327 'rules' in pull_request_latest.reviewer_data:
328 328 rules = pull_request_latest.reviewer_data['rules'] or {}
329 329 try:
330 330 c.forbid_adding_reviewers = rules.get(
331 331 'forbid_adding_reviewers')
332 332 c.forbid_author_to_review = rules.get(
333 333 'forbid_author_to_review')
334 334 c.forbid_commit_author_to_review = rules.get(
335 335 'forbid_commit_author_to_review')
336 336 except Exception:
337 337 pass
338 338
339 339 # check merge capabilities
340 340 _merge_check = MergeCheck.validate(
341 341 pull_request_latest, user=self._rhodecode_user,
342 342 translator=self.request.translate)
343 343 c.pr_merge_errors = _merge_check.error_details
344 344 c.pr_merge_possible = not _merge_check.failed
345 345 c.pr_merge_message = _merge_check.merge_msg
346 346
347 347 c.pr_merge_info = MergeCheck.get_merge_conditions(
348 348 pull_request_latest, translator=self.request.translate)
349 349
350 350 c.pull_request_review_status = _merge_check.review_status
351 351 if merge_checks:
352 352 self.request.override_renderer = \
353 353 'rhodecode:templates/pullrequests/pullrequest_merge_checks.mako'
354 354 return self._get_template_context(c)
355 355
356 356 comments_model = CommentsModel()
357 357
358 358 # reviewers and statuses
359 359 c.pull_request_reviewers = pull_request_at_ver.reviewers_statuses()
360 360 allowed_reviewers = [x[0].user_id for x in c.pull_request_reviewers]
361 361
362 362 # GENERAL COMMENTS with versions #
363 363 q = comments_model._all_general_comments_of_pull_request(pull_request_latest)
364 364 q = q.order_by(ChangesetComment.comment_id.asc())
365 365 general_comments = q
366 366
367 367 # pick comments we want to render at current version
368 368 c.comment_versions = comments_model.aggregate_comments(
369 369 general_comments, versions, c.at_version_num)
370 370 c.comments = c.comment_versions[c.at_version_num]['until']
371 371
372 372 # INLINE COMMENTS with versions #
373 373 q = comments_model._all_inline_comments_of_pull_request(pull_request_latest)
374 374 q = q.order_by(ChangesetComment.comment_id.asc())
375 375 inline_comments = q
376 376
377 377 c.inline_versions = comments_model.aggregate_comments(
378 378 inline_comments, versions, c.at_version_num, inline=True)
379 379
380 380 # inject latest version
381 381 latest_ver = PullRequest.get_pr_display_object(
382 382 pull_request_latest, pull_request_latest)
383 383
384 384 c.versions = versions + [latest_ver]
385 385
386 386 # if we use version, then do not show later comments
387 387 # than current version
388 388 display_inline_comments = collections.defaultdict(
389 389 lambda: collections.defaultdict(list))
390 390 for co in inline_comments:
391 391 if c.at_version_num:
392 392 # pick comments that are at least UPTO given version, so we
393 393 # don't render comments for higher version
394 394 should_render = co.pull_request_version_id and \
395 395 co.pull_request_version_id <= c.at_version_num
396 396 else:
397 397 # showing all, for 'latest'
398 398 should_render = True
399 399
400 400 if should_render:
401 401 display_inline_comments[co.f_path][co.line_no].append(co)
402 402
403 403 # load diff data into template context, if we use compare mode then
404 404 # diff is calculated based on changes between versions of PR
405 405
406 406 source_repo = pull_request_at_ver.source_repo
407 407 source_ref_id = pull_request_at_ver.source_ref_parts.commit_id
408 408
409 409 target_repo = pull_request_at_ver.target_repo
410 410 target_ref_id = pull_request_at_ver.target_ref_parts.commit_id
411 411
412 412 if compare:
413 413 # in compare switch the diff base to latest commit from prev version
414 414 target_ref_id = prev_pull_request_display_obj.revisions[0]
415 415
416 416 # despite opening commits for bookmarks/branches/tags, we always
417 417 # convert this to rev to prevent changes after bookmark or branch change
418 418 c.source_ref_type = 'rev'
419 419 c.source_ref = source_ref_id
420 420
421 421 c.target_ref_type = 'rev'
422 422 c.target_ref = target_ref_id
423 423
424 424 c.source_repo = source_repo
425 425 c.target_repo = target_repo
426 426
427 427 c.commit_ranges = []
428 428 source_commit = EmptyCommit()
429 429 target_commit = EmptyCommit()
430 430 c.missing_requirements = False
431 431
432 432 source_scm = source_repo.scm_instance()
433 433 target_scm = target_repo.scm_instance()
434 434
435 435 # try first shadow repo, fallback to regular repo
436 436 try:
437 437 commits_source_repo = pull_request_latest.get_shadow_repo()
438 438 except Exception:
439 439 log.debug('Failed to get shadow repo', exc_info=True)
440 440 commits_source_repo = source_scm
441 441
442 442 c.commits_source_repo = commits_source_repo
443 443 c.ancestor = None # set it to None, to hide it from PR view
444 444
445 445 # empty version means latest, so we keep this to prevent
446 446 # double caching
447 447 version_normalized = version or 'latest'
448 448 from_version_normalized = from_version or 'latest'
449 449
450 450 cache_path = self.rhodecode_vcs_repo.get_create_shadow_cache_pr_path(
451 451 target_repo)
452 452 cache_file_path = diff_cache_exist(
453 453 cache_path, 'pull_request', pull_request_id, version_normalized,
454 454 from_version_normalized, source_ref_id, target_ref_id, c.fulldiff)
455 455
456 456 caching_enabled = self._is_diff_cache_enabled(c.target_repo)
457 457 force_recache = str2bool(self.request.GET.get('force_recache'))
458 458
459 459 cached_diff = None
460 460 if caching_enabled:
461 461 cached_diff = load_cached_diff(cache_file_path)
462 462
463 463 has_proper_commit_cache = (
464 464 cached_diff and cached_diff.get('commits')
465 465 and len(cached_diff.get('commits', [])) == 5
466 466 and cached_diff.get('commits')[0]
467 467 and cached_diff.get('commits')[3])
468 468 if not force_recache and has_proper_commit_cache:
469 469 diff_commit_cache = \
470 470 (ancestor_commit, commit_cache, missing_requirements,
471 471 source_commit, target_commit) = cached_diff['commits']
472 472 else:
473 473 diff_commit_cache = \
474 474 (ancestor_commit, commit_cache, missing_requirements,
475 475 source_commit, target_commit) = self.get_commits(
476 476 commits_source_repo,
477 477 pull_request_at_ver,
478 478 source_commit,
479 479 source_ref_id,
480 480 source_scm,
481 481 target_commit,
482 482 target_ref_id,
483 483 target_scm)
484 484
485 485 # register our commit range
486 486 for comm in commit_cache.values():
487 487 c.commit_ranges.append(comm)
488 488
489 489 c.missing_requirements = missing_requirements
490 490 c.ancestor_commit = ancestor_commit
491 491 c.statuses = source_repo.statuses(
492 492 [x.raw_id for x in c.commit_ranges])
493 493
494 494 # auto collapse if we have more than limit
495 495 collapse_limit = diffs.DiffProcessor._collapse_commits_over
496 496 c.collapse_all_commits = len(c.commit_ranges) > collapse_limit
497 497 c.compare_mode = compare
498 498
499 499 # diff_limit is the old behavior, will cut off the whole diff
500 500 # if the limit is applied otherwise will just hide the
501 501 # big files from the front-end
502 502 diff_limit = c.visual.cut_off_limit_diff
503 503 file_limit = c.visual.cut_off_limit_file
504 504
505 505 c.missing_commits = False
506 506 if (c.missing_requirements
507 507 or isinstance(source_commit, EmptyCommit)
508 508 or source_commit == target_commit):
509 509
510 510 c.missing_commits = True
511 511 else:
512 512 c.inline_comments = display_inline_comments
513 513
514 514 has_proper_diff_cache = cached_diff and cached_diff.get('commits')
515 515 if not force_recache and has_proper_diff_cache:
516 516 c.diffset = cached_diff['diff']
517 517 (ancestor_commit, commit_cache, missing_requirements,
518 518 source_commit, target_commit) = cached_diff['commits']
519 519 else:
520 520 c.diffset = self._get_diffset(
521 521 c.source_repo.repo_name, commits_source_repo,
522 522 source_ref_id, target_ref_id,
523 523 target_commit, source_commit,
524 524 diff_limit, file_limit, c.fulldiff)
525 525
526 526 # save cached diff
527 527 if caching_enabled:
528 528 cache_diff(cache_file_path, c.diffset, diff_commit_cache)
529 529
530 530 c.limited_diff = c.diffset.limited_diff
531 531
532 532 # calculate removed files that are bound to comments
533 533 comment_deleted_files = [
534 534 fname for fname in display_inline_comments
535 535 if fname not in c.diffset.file_stats]
536 536
537 537 c.deleted_files_comments = collections.defaultdict(dict)
538 538 for fname, per_line_comments in display_inline_comments.items():
539 539 if fname in comment_deleted_files:
540 540 c.deleted_files_comments[fname]['stats'] = 0
541 541 c.deleted_files_comments[fname]['comments'] = list()
542 542 for lno, comments in per_line_comments.items():
543 543 c.deleted_files_comments[fname]['comments'].extend(
544 544 comments)
545 545
546 546 # this is a hack to properly display links, when creating PR, the
547 547 # compare view and others uses different notation, and
548 548 # compare_commits.mako renders links based on the target_repo.
549 549 # We need to swap that here to generate it properly on the html side
550 550 c.target_repo = c.source_repo
551 551
552 552 c.commit_statuses = ChangesetStatus.STATUSES
553 553
554 554 c.show_version_changes = not pr_closed
555 555 if c.show_version_changes:
556 556 cur_obj = pull_request_at_ver
557 557 prev_obj = prev_pull_request_at_ver
558 558
559 559 old_commit_ids = prev_obj.revisions
560 560 new_commit_ids = cur_obj.revisions
561 561 commit_changes = PullRequestModel()._calculate_commit_id_changes(
562 562 old_commit_ids, new_commit_ids)
563 563 c.commit_changes_summary = commit_changes
564 564
565 565 # calculate the diff for commits between versions
566 566 c.commit_changes = []
567 567 mark = lambda cs, fw: list(
568 568 h.itertools.izip_longest([], cs, fillvalue=fw))
569 569 for c_type, raw_id in mark(commit_changes.added, 'a') \
570 570 + mark(commit_changes.removed, 'r') \
571 571 + mark(commit_changes.common, 'c'):
572 572
573 573 if raw_id in commit_cache:
574 574 commit = commit_cache[raw_id]
575 575 else:
576 576 try:
577 577 commit = commits_source_repo.get_commit(raw_id)
578 578 except CommitDoesNotExistError:
579 579 # in case we fail extracting still use "dummy" commit
580 580 # for display in commit diff
581 581 commit = h.AttributeDict(
582 582 {'raw_id': raw_id,
583 583 'message': 'EMPTY or MISSING COMMIT'})
584 584 c.commit_changes.append([c_type, commit])
585 585
586 586 # current user review statuses for each version
587 587 c.review_versions = {}
588 588 if self._rhodecode_user.user_id in allowed_reviewers:
589 589 for co in general_comments:
590 590 if co.author.user_id == self._rhodecode_user.user_id:
591 591 status = co.status_change
592 592 if status:
593 593 _ver_pr = status[0].comment.pull_request_version_id
594 594 c.review_versions[_ver_pr] = status[0]
595 595
596 596 return self._get_template_context(c)
597 597
598 598 def get_commits(
599 599 self, commits_source_repo, pull_request_at_ver, source_commit,
600 600 source_ref_id, source_scm, target_commit, target_ref_id, target_scm):
601 601 commit_cache = collections.OrderedDict()
602 602 missing_requirements = False
603 603 try:
604 604 pre_load = ["author", "branch", "date", "message"]
605 605 show_revs = pull_request_at_ver.revisions
606 606 for rev in show_revs:
607 607 comm = commits_source_repo.get_commit(
608 608 commit_id=rev, pre_load=pre_load)
609 609 commit_cache[comm.raw_id] = comm
610 610
611 611 # Order here matters, we first need to get target, and then
612 612 # the source
613 613 target_commit = commits_source_repo.get_commit(
614 614 commit_id=safe_str(target_ref_id))
615 615
616 616 source_commit = commits_source_repo.get_commit(
617 617 commit_id=safe_str(source_ref_id))
618 618 except CommitDoesNotExistError:
619 619 log.warning(
620 620 'Failed to get commit from `{}` repo'.format(
621 621 commits_source_repo), exc_info=True)
622 622 except RepositoryRequirementError:
623 623 log.warning(
624 624 'Failed to get all required data from repo', exc_info=True)
625 625 missing_requirements = True
626 626 ancestor_commit = None
627 627 try:
628 628 ancestor_id = source_scm.get_common_ancestor(
629 629 source_commit.raw_id, target_commit.raw_id, target_scm)
630 630 ancestor_commit = source_scm.get_commit(ancestor_id)
631 631 except Exception:
632 632 ancestor_commit = None
633 633 return ancestor_commit, commit_cache, missing_requirements, source_commit, target_commit
634 634
635 635 def assure_not_empty_repo(self):
636 636 _ = self.request.translate
637 637
638 638 try:
639 639 self.db_repo.scm_instance().get_commit()
640 640 except EmptyRepositoryError:
641 641 h.flash(h.literal(_('There are no commits yet')),
642 642 category='warning')
643 643 raise HTTPFound(
644 644 h.route_path('repo_summary', repo_name=self.db_repo.repo_name))
645 645
646 646 @LoginRequired()
647 647 @NotAnonymous()
648 648 @HasRepoPermissionAnyDecorator(
649 649 'repository.read', 'repository.write', 'repository.admin')
650 650 @view_config(
651 651 route_name='pullrequest_new', request_method='GET',
652 652 renderer='rhodecode:templates/pullrequests/pullrequest.mako')
653 653 def pull_request_new(self):
654 654 _ = self.request.translate
655 655 c = self.load_default_context()
656 656
657 657 self.assure_not_empty_repo()
658 658 source_repo = self.db_repo
659 659
660 660 commit_id = self.request.GET.get('commit')
661 661 branch_ref = self.request.GET.get('branch')
662 662 bookmark_ref = self.request.GET.get('bookmark')
663 663
664 664 try:
665 665 source_repo_data = PullRequestModel().generate_repo_data(
666 666 source_repo, commit_id=commit_id,
667 667 branch=branch_ref, bookmark=bookmark_ref,
668 668 translator=self.request.translate)
669 669 except CommitDoesNotExistError as e:
670 670 log.exception(e)
671 671 h.flash(_('Commit does not exist'), 'error')
672 672 raise HTTPFound(
673 673 h.route_path('pullrequest_new', repo_name=source_repo.repo_name))
674 674
675 675 default_target_repo = source_repo
676 676
677 677 if source_repo.parent:
678 678 parent_vcs_obj = source_repo.parent.scm_instance()
679 679 if parent_vcs_obj and not parent_vcs_obj.is_empty():
680 680 # change default if we have a parent repo
681 681 default_target_repo = source_repo.parent
682 682
683 683 target_repo_data = PullRequestModel().generate_repo_data(
684 684 default_target_repo, translator=self.request.translate)
685 685
686 686 selected_source_ref = source_repo_data['refs']['selected_ref']
687 687 title_source_ref = ''
688 688 if selected_source_ref:
689 689 title_source_ref = selected_source_ref.split(':', 2)[1]
690 690 c.default_title = PullRequestModel().generate_pullrequest_title(
691 691 source=source_repo.repo_name,
692 692 source_ref=title_source_ref,
693 693 target=default_target_repo.repo_name
694 694 )
695 695
696 696 c.default_repo_data = {
697 697 'source_repo_name': source_repo.repo_name,
698 698 'source_refs_json': json.dumps(source_repo_data),
699 699 'target_repo_name': default_target_repo.repo_name,
700 700 'target_refs_json': json.dumps(target_repo_data),
701 701 }
702 702 c.default_source_ref = selected_source_ref
703 703
704 704 return self._get_template_context(c)
705 705
706 706 @LoginRequired()
707 707 @NotAnonymous()
708 708 @HasRepoPermissionAnyDecorator(
709 709 'repository.read', 'repository.write', 'repository.admin')
710 710 @view_config(
711 711 route_name='pullrequest_repo_refs', request_method='GET',
712 712 renderer='json_ext', xhr=True)
713 713 def pull_request_repo_refs(self):
714 714 self.load_default_context()
715 715 target_repo_name = self.request.matchdict['target_repo_name']
716 716 repo = Repository.get_by_repo_name(target_repo_name)
717 717 if not repo:
718 718 raise HTTPNotFound()
719 719
720 720 target_perm = HasRepoPermissionAny(
721 721 'repository.read', 'repository.write', 'repository.admin')(
722 722 target_repo_name)
723 723 if not target_perm:
724 724 raise HTTPNotFound()
725 725
726 726 return PullRequestModel().generate_repo_data(
727 727 repo, translator=self.request.translate)
728 728
729 729 @LoginRequired()
730 730 @NotAnonymous()
731 731 @HasRepoPermissionAnyDecorator(
732 732 'repository.read', 'repository.write', 'repository.admin')
733 733 @view_config(
734 734 route_name='pullrequest_repo_destinations', request_method='GET',
735 735 renderer='json_ext', xhr=True)
736 736 def pull_request_repo_destinations(self):
737 737 _ = self.request.translate
738 738 filter_query = self.request.GET.get('query')
739 739
740 740 query = Repository.query() \
741 741 .order_by(func.length(Repository.repo_name)) \
742 742 .filter(
743 743 or_(Repository.repo_name == self.db_repo.repo_name,
744 744 Repository.fork_id == self.db_repo.repo_id))
745 745
746 746 if filter_query:
747 747 ilike_expression = u'%{}%'.format(safe_unicode(filter_query))
748 748 query = query.filter(
749 749 Repository.repo_name.ilike(ilike_expression))
750 750
751 751 add_parent = False
752 752 if self.db_repo.parent:
753 753 if filter_query in self.db_repo.parent.repo_name:
754 754 parent_vcs_obj = self.db_repo.parent.scm_instance()
755 755 if parent_vcs_obj and not parent_vcs_obj.is_empty():
756 756 add_parent = True
757 757
758 758 limit = 20 - 1 if add_parent else 20
759 759 all_repos = query.limit(limit).all()
760 760 if add_parent:
761 761 all_repos += [self.db_repo.parent]
762 762
763 763 repos = []
764 764 for obj in ScmModel().get_repos(all_repos):
765 765 repos.append({
766 766 'id': obj['name'],
767 767 'text': obj['name'],
768 768 'type': 'repo',
769 'obj': obj['dbrepo']
769 'repo_id': obj['dbrepo']['repo_id'],
770 'repo_type': obj['dbrepo']['repo_type'],
771 'private': obj['dbrepo']['private'],
772
770 773 })
771 774
772 775 data = {
773 776 'more': False,
774 777 'results': [{
775 778 'text': _('Repositories'),
776 779 'children': repos
777 780 }] if repos else []
778 781 }
779 782 return data
780 783
781 784 @LoginRequired()
782 785 @NotAnonymous()
783 786 @HasRepoPermissionAnyDecorator(
784 787 'repository.read', 'repository.write', 'repository.admin')
785 788 @CSRFRequired()
786 789 @view_config(
787 790 route_name='pullrequest_create', request_method='POST',
788 791 renderer=None)
789 792 def pull_request_create(self):
790 793 _ = self.request.translate
791 794 self.assure_not_empty_repo()
792 795 self.load_default_context()
793 796
794 797 controls = peppercorn.parse(self.request.POST.items())
795 798
796 799 try:
797 800 form = PullRequestForm(
798 801 self.request.translate, self.db_repo.repo_id)()
799 802 _form = form.to_python(controls)
800 803 except formencode.Invalid as errors:
801 804 if errors.error_dict.get('revisions'):
802 805 msg = 'Revisions: %s' % errors.error_dict['revisions']
803 806 elif errors.error_dict.get('pullrequest_title'):
804 807 msg = errors.error_dict.get('pullrequest_title')
805 808 else:
806 809 msg = _('Error creating pull request: {}').format(errors)
807 810 log.exception(msg)
808 811 h.flash(msg, 'error')
809 812
810 813 # would rather just go back to form ...
811 814 raise HTTPFound(
812 815 h.route_path('pullrequest_new', repo_name=self.db_repo_name))
813 816
814 817 source_repo = _form['source_repo']
815 818 source_ref = _form['source_ref']
816 819 target_repo = _form['target_repo']
817 820 target_ref = _form['target_ref']
818 821 commit_ids = _form['revisions'][::-1]
819 822
820 823 # find the ancestor for this pr
821 824 source_db_repo = Repository.get_by_repo_name(_form['source_repo'])
822 825 target_db_repo = Repository.get_by_repo_name(_form['target_repo'])
823 826
824 827 # re-check permissions again here
825 828 # source_repo we must have read permissions
826 829
827 830 source_perm = HasRepoPermissionAny(
828 831 'repository.read',
829 832 'repository.write', 'repository.admin')(source_db_repo.repo_name)
830 833 if not source_perm:
831 834 msg = _('Not Enough permissions to source repo `{}`.'.format(
832 835 source_db_repo.repo_name))
833 836 h.flash(msg, category='error')
834 837 # copy the args back to redirect
835 838 org_query = self.request.GET.mixed()
836 839 raise HTTPFound(
837 840 h.route_path('pullrequest_new', repo_name=self.db_repo_name,
838 841 _query=org_query))
839 842
840 843 # target repo we must have read permissions, and also later on
841 844 # we want to check branch permissions here
842 845 target_perm = HasRepoPermissionAny(
843 846 'repository.read',
844 847 'repository.write', 'repository.admin')(target_db_repo.repo_name)
845 848 if not target_perm:
846 849 msg = _('Not Enough permissions to target repo `{}`.'.format(
847 850 target_db_repo.repo_name))
848 851 h.flash(msg, category='error')
849 852 # copy the args back to redirect
850 853 org_query = self.request.GET.mixed()
851 854 raise HTTPFound(
852 855 h.route_path('pullrequest_new', repo_name=self.db_repo_name,
853 856 _query=org_query))
854 857
855 858 source_scm = source_db_repo.scm_instance()
856 859 target_scm = target_db_repo.scm_instance()
857 860
858 861 source_commit = source_scm.get_commit(source_ref.split(':')[-1])
859 862 target_commit = target_scm.get_commit(target_ref.split(':')[-1])
860 863
861 864 ancestor = source_scm.get_common_ancestor(
862 865 source_commit.raw_id, target_commit.raw_id, target_scm)
863 866
864 867 target_ref_type, target_ref_name, __ = _form['target_ref'].split(':')
865 868 target_ref = ':'.join((target_ref_type, target_ref_name, ancestor))
866 869
867 870 pullrequest_title = _form['pullrequest_title']
868 871 title_source_ref = source_ref.split(':', 2)[1]
869 872 if not pullrequest_title:
870 873 pullrequest_title = PullRequestModel().generate_pullrequest_title(
871 874 source=source_repo,
872 875 source_ref=title_source_ref,
873 876 target=target_repo
874 877 )
875 878
876 879 description = _form['pullrequest_desc']
877 880
878 881 get_default_reviewers_data, validate_default_reviewers = \
879 882 PullRequestModel().get_reviewer_functions()
880 883
881 884 # recalculate reviewers logic, to make sure we can validate this
882 885 reviewer_rules = get_default_reviewers_data(
883 886 self._rhodecode_db_user, source_db_repo,
884 887 source_commit, target_db_repo, target_commit)
885 888
886 889 given_reviewers = _form['review_members']
887 890 reviewers = validate_default_reviewers(given_reviewers, reviewer_rules)
888 891
889 892 try:
890 893 pull_request = PullRequestModel().create(
891 894 self._rhodecode_user.user_id, source_repo, source_ref,
892 895 target_repo, target_ref, commit_ids, reviewers,
893 896 pullrequest_title, description, reviewer_rules
894 897 )
895 898 Session().commit()
896 899
897 900 h.flash(_('Successfully opened new pull request'),
898 901 category='success')
899 902 except Exception:
900 903 msg = _('Error occurred during creation of this pull request.')
901 904 log.exception(msg)
902 905 h.flash(msg, category='error')
903 906
904 907 # copy the args back to redirect
905 908 org_query = self.request.GET.mixed()
906 909 raise HTTPFound(
907 910 h.route_path('pullrequest_new', repo_name=self.db_repo_name,
908 911 _query=org_query))
909 912
910 913 raise HTTPFound(
911 914 h.route_path('pullrequest_show', repo_name=target_repo,
912 915 pull_request_id=pull_request.pull_request_id))
913 916
914 917 @LoginRequired()
915 918 @NotAnonymous()
916 919 @HasRepoPermissionAnyDecorator(
917 920 'repository.read', 'repository.write', 'repository.admin')
918 921 @CSRFRequired()
919 922 @view_config(
920 923 route_name='pullrequest_update', request_method='POST',
921 924 renderer='json_ext')
922 925 def pull_request_update(self):
923 926 pull_request = PullRequest.get_or_404(
924 927 self.request.matchdict['pull_request_id'])
925 928 _ = self.request.translate
926 929
927 930 self.load_default_context()
928 931
929 932 if pull_request.is_closed():
930 933 log.debug('update: forbidden because pull request is closed')
931 934 msg = _(u'Cannot update closed pull requests.')
932 935 h.flash(msg, category='error')
933 936 return True
934 937
935 938 # only owner or admin can update it
936 939 allowed_to_update = PullRequestModel().check_user_update(
937 940 pull_request, self._rhodecode_user)
938 941 if allowed_to_update:
939 942 controls = peppercorn.parse(self.request.POST.items())
940 943
941 944 if 'review_members' in controls:
942 945 self._update_reviewers(
943 946 pull_request, controls['review_members'],
944 947 pull_request.reviewer_data)
945 948 elif str2bool(self.request.POST.get('update_commits', 'false')):
946 949 self._update_commits(pull_request)
947 950 elif str2bool(self.request.POST.get('edit_pull_request', 'false')):
948 951 self._edit_pull_request(pull_request)
949 952 else:
950 953 raise HTTPBadRequest()
951 954 return True
952 955 raise HTTPForbidden()
953 956
954 957 def _edit_pull_request(self, pull_request):
955 958 _ = self.request.translate
956 959 try:
957 960 PullRequestModel().edit(
958 961 pull_request, self.request.POST.get('title'),
959 962 self.request.POST.get('description'), self._rhodecode_user)
960 963 except ValueError:
961 964 msg = _(u'Cannot update closed pull requests.')
962 965 h.flash(msg, category='error')
963 966 return
964 967 else:
965 968 Session().commit()
966 969
967 970 msg = _(u'Pull request title & description updated.')
968 971 h.flash(msg, category='success')
969 972 return
970 973
971 974 def _update_commits(self, pull_request):
972 975 _ = self.request.translate
973 976 resp = PullRequestModel().update_commits(pull_request)
974 977
975 978 if resp.executed:
976 979
977 980 if resp.target_changed and resp.source_changed:
978 981 changed = 'target and source repositories'
979 982 elif resp.target_changed and not resp.source_changed:
980 983 changed = 'target repository'
981 984 elif not resp.target_changed and resp.source_changed:
982 985 changed = 'source repository'
983 986 else:
984 987 changed = 'nothing'
985 988
986 989 msg = _(
987 990 u'Pull request updated to "{source_commit_id}" with '
988 991 u'{count_added} added, {count_removed} removed commits. '
989 992 u'Source of changes: {change_source}')
990 993 msg = msg.format(
991 994 source_commit_id=pull_request.source_ref_parts.commit_id,
992 995 count_added=len(resp.changes.added),
993 996 count_removed=len(resp.changes.removed),
994 997 change_source=changed)
995 998 h.flash(msg, category='success')
996 999
997 1000 channel = '/repo${}$/pr/{}'.format(
998 1001 pull_request.target_repo.repo_name,
999 1002 pull_request.pull_request_id)
1000 1003 message = msg + (
1001 1004 ' - <a onclick="window.location.reload()">'
1002 1005 '<strong>{}</strong></a>'.format(_('Reload page')))
1003 1006 channelstream.post_message(
1004 1007 channel, message, self._rhodecode_user.username,
1005 1008 registry=self.request.registry)
1006 1009 else:
1007 1010 msg = PullRequestModel.UPDATE_STATUS_MESSAGES[resp.reason]
1008 1011 warning_reasons = [
1009 1012 UpdateFailureReason.NO_CHANGE,
1010 1013 UpdateFailureReason.WRONG_REF_TYPE,
1011 1014 ]
1012 1015 category = 'warning' if resp.reason in warning_reasons else 'error'
1013 1016 h.flash(msg, category=category)
1014 1017
1015 1018 @LoginRequired()
1016 1019 @NotAnonymous()
1017 1020 @HasRepoPermissionAnyDecorator(
1018 1021 'repository.read', 'repository.write', 'repository.admin')
1019 1022 @CSRFRequired()
1020 1023 @view_config(
1021 1024 route_name='pullrequest_merge', request_method='POST',
1022 1025 renderer='json_ext')
1023 1026 def pull_request_merge(self):
1024 1027 """
1025 1028 Merge will perform a server-side merge of the specified
1026 1029 pull request, if the pull request is approved and mergeable.
1027 1030 After successful merging, the pull request is automatically
1028 1031 closed, with a relevant comment.
1029 1032 """
1030 1033 pull_request = PullRequest.get_or_404(
1031 1034 self.request.matchdict['pull_request_id'])
1032 1035
1033 1036 self.load_default_context()
1034 1037 check = MergeCheck.validate(pull_request, self._rhodecode_db_user,
1035 1038 translator=self.request.translate)
1036 1039 merge_possible = not check.failed
1037 1040
1038 1041 for err_type, error_msg in check.errors:
1039 1042 h.flash(error_msg, category=err_type)
1040 1043
1041 1044 if merge_possible:
1042 1045 log.debug("Pre-conditions checked, trying to merge.")
1043 1046 extras = vcs_operation_context(
1044 1047 self.request.environ, repo_name=pull_request.target_repo.repo_name,
1045 1048 username=self._rhodecode_db_user.username, action='push',
1046 1049 scm=pull_request.target_repo.repo_type)
1047 1050 self._merge_pull_request(
1048 1051 pull_request, self._rhodecode_db_user, extras)
1049 1052 else:
1050 1053 log.debug("Pre-conditions failed, NOT merging.")
1051 1054
1052 1055 raise HTTPFound(
1053 1056 h.route_path('pullrequest_show',
1054 1057 repo_name=pull_request.target_repo.repo_name,
1055 1058 pull_request_id=pull_request.pull_request_id))
1056 1059
1057 1060 def _merge_pull_request(self, pull_request, user, extras):
1058 1061 _ = self.request.translate
1059 1062 merge_resp = PullRequestModel().merge(pull_request, user, extras=extras)
1060 1063
1061 1064 if merge_resp.executed:
1062 1065 log.debug("The merge was successful, closing the pull request.")
1063 1066 PullRequestModel().close_pull_request(
1064 1067 pull_request.pull_request_id, user)
1065 1068 Session().commit()
1066 1069 msg = _('Pull request was successfully merged and closed.')
1067 1070 h.flash(msg, category='success')
1068 1071 else:
1069 1072 log.debug(
1070 1073 "The merge was not successful. Merge response: %s",
1071 1074 merge_resp)
1072 1075 msg = PullRequestModel().merge_status_message(
1073 1076 merge_resp.failure_reason)
1074 1077 h.flash(msg, category='error')
1075 1078
1076 1079 def _update_reviewers(self, pull_request, review_members, reviewer_rules):
1077 1080 _ = self.request.translate
1078 1081 get_default_reviewers_data, validate_default_reviewers = \
1079 1082 PullRequestModel().get_reviewer_functions()
1080 1083
1081 1084 try:
1082 1085 reviewers = validate_default_reviewers(review_members, reviewer_rules)
1083 1086 except ValueError as e:
1084 1087 log.error('Reviewers Validation: {}'.format(e))
1085 1088 h.flash(e, category='error')
1086 1089 return
1087 1090
1088 1091 PullRequestModel().update_reviewers(
1089 1092 pull_request, reviewers, self._rhodecode_user)
1090 1093 h.flash(_('Pull request reviewers updated.'), category='success')
1091 1094 Session().commit()
1092 1095
1093 1096 @LoginRequired()
1094 1097 @NotAnonymous()
1095 1098 @HasRepoPermissionAnyDecorator(
1096 1099 'repository.read', 'repository.write', 'repository.admin')
1097 1100 @CSRFRequired()
1098 1101 @view_config(
1099 1102 route_name='pullrequest_delete', request_method='POST',
1100 1103 renderer='json_ext')
1101 1104 def pull_request_delete(self):
1102 1105 _ = self.request.translate
1103 1106
1104 1107 pull_request = PullRequest.get_or_404(
1105 1108 self.request.matchdict['pull_request_id'])
1106 1109 self.load_default_context()
1107 1110
1108 1111 pr_closed = pull_request.is_closed()
1109 1112 allowed_to_delete = PullRequestModel().check_user_delete(
1110 1113 pull_request, self._rhodecode_user) and not pr_closed
1111 1114
1112 1115 # only owner can delete it !
1113 1116 if allowed_to_delete:
1114 1117 PullRequestModel().delete(pull_request, self._rhodecode_user)
1115 1118 Session().commit()
1116 1119 h.flash(_('Successfully deleted pull request'),
1117 1120 category='success')
1118 1121 raise HTTPFound(h.route_path('pullrequest_show_all',
1119 1122 repo_name=self.db_repo_name))
1120 1123
1121 1124 log.warning('user %s tried to delete pull request without access',
1122 1125 self._rhodecode_user)
1123 1126 raise HTTPNotFound()
1124 1127
1125 1128 @LoginRequired()
1126 1129 @NotAnonymous()
1127 1130 @HasRepoPermissionAnyDecorator(
1128 1131 'repository.read', 'repository.write', 'repository.admin')
1129 1132 @CSRFRequired()
1130 1133 @view_config(
1131 1134 route_name='pullrequest_comment_create', request_method='POST',
1132 1135 renderer='json_ext')
1133 1136 def pull_request_comment_create(self):
1134 1137 _ = self.request.translate
1135 1138
1136 1139 pull_request = PullRequest.get_or_404(
1137 1140 self.request.matchdict['pull_request_id'])
1138 1141 pull_request_id = pull_request.pull_request_id
1139 1142
1140 1143 if pull_request.is_closed():
1141 1144 log.debug('comment: forbidden because pull request is closed')
1142 1145 raise HTTPForbidden()
1143 1146
1144 1147 allowed_to_comment = PullRequestModel().check_user_comment(
1145 1148 pull_request, self._rhodecode_user)
1146 1149 if not allowed_to_comment:
1147 1150 log.debug(
1148 1151 'comment: forbidden because pull request is from forbidden repo')
1149 1152 raise HTTPForbidden()
1150 1153
1151 1154 c = self.load_default_context()
1152 1155
1153 1156 status = self.request.POST.get('changeset_status', None)
1154 1157 text = self.request.POST.get('text')
1155 1158 comment_type = self.request.POST.get('comment_type')
1156 1159 resolves_comment_id = self.request.POST.get('resolves_comment_id', None)
1157 1160 close_pull_request = self.request.POST.get('close_pull_request')
1158 1161
1159 1162 # the logic here should work like following, if we submit close
1160 1163 # pr comment, use `close_pull_request_with_comment` function
1161 1164 # else handle regular comment logic
1162 1165
1163 1166 if close_pull_request:
1164 1167 # only owner or admin or person with write permissions
1165 1168 allowed_to_close = PullRequestModel().check_user_update(
1166 1169 pull_request, self._rhodecode_user)
1167 1170 if not allowed_to_close:
1168 1171 log.debug('comment: forbidden because not allowed to close '
1169 1172 'pull request %s', pull_request_id)
1170 1173 raise HTTPForbidden()
1171 1174 comment, status = PullRequestModel().close_pull_request_with_comment(
1172 1175 pull_request, self._rhodecode_user, self.db_repo, message=text)
1173 1176 Session().flush()
1174 1177 events.trigger(
1175 1178 events.PullRequestCommentEvent(pull_request, comment))
1176 1179
1177 1180 else:
1178 1181 # regular comment case, could be inline, or one with status.
1179 1182 # for that one we check also permissions
1180 1183
1181 1184 allowed_to_change_status = PullRequestModel().check_user_change_status(
1182 1185 pull_request, self._rhodecode_user)
1183 1186
1184 1187 if status and allowed_to_change_status:
1185 1188 message = (_('Status change %(transition_icon)s %(status)s')
1186 1189 % {'transition_icon': '>',
1187 1190 'status': ChangesetStatus.get_status_lbl(status)})
1188 1191 text = text or message
1189 1192
1190 1193 comment = CommentsModel().create(
1191 1194 text=text,
1192 1195 repo=self.db_repo.repo_id,
1193 1196 user=self._rhodecode_user.user_id,
1194 1197 pull_request=pull_request,
1195 1198 f_path=self.request.POST.get('f_path'),
1196 1199 line_no=self.request.POST.get('line'),
1197 1200 status_change=(ChangesetStatus.get_status_lbl(status)
1198 1201 if status and allowed_to_change_status else None),
1199 1202 status_change_type=(status
1200 1203 if status and allowed_to_change_status else None),
1201 1204 comment_type=comment_type,
1202 1205 resolves_comment_id=resolves_comment_id
1203 1206 )
1204 1207
1205 1208 if allowed_to_change_status:
1206 1209 # calculate old status before we change it
1207 1210 old_calculated_status = pull_request.calculated_review_status()
1208 1211
1209 1212 # get status if set !
1210 1213 if status:
1211 1214 ChangesetStatusModel().set_status(
1212 1215 self.db_repo.repo_id,
1213 1216 status,
1214 1217 self._rhodecode_user.user_id,
1215 1218 comment,
1216 1219 pull_request=pull_request
1217 1220 )
1218 1221
1219 1222 Session().flush()
1220 1223 # this is somehow required to get access to some relationship
1221 1224 # loaded on comment
1222 1225 Session().refresh(comment)
1223 1226
1224 1227 events.trigger(
1225 1228 events.PullRequestCommentEvent(pull_request, comment))
1226 1229
1227 1230 # we now calculate the status of pull request, and based on that
1228 1231 # calculation we set the commits status
1229 1232 calculated_status = pull_request.calculated_review_status()
1230 1233 if old_calculated_status != calculated_status:
1231 1234 PullRequestModel()._trigger_pull_request_hook(
1232 1235 pull_request, self._rhodecode_user, 'review_status_change')
1233 1236
1234 1237 Session().commit()
1235 1238
1236 1239 data = {
1237 1240 'target_id': h.safeid(h.safe_unicode(
1238 1241 self.request.POST.get('f_path'))),
1239 1242 }
1240 1243 if comment:
1241 1244 c.co = comment
1242 1245 rendered_comment = render(
1243 1246 'rhodecode:templates/changeset/changeset_comment_block.mako',
1244 1247 self._get_template_context(c), self.request)
1245 1248
1246 1249 data.update(comment.get_dict())
1247 1250 data.update({'rendered_text': rendered_comment})
1248 1251
1249 1252 return data
1250 1253
1251 1254 @LoginRequired()
1252 1255 @NotAnonymous()
1253 1256 @HasRepoPermissionAnyDecorator(
1254 1257 'repository.read', 'repository.write', 'repository.admin')
1255 1258 @CSRFRequired()
1256 1259 @view_config(
1257 1260 route_name='pullrequest_comment_delete', request_method='POST',
1258 1261 renderer='json_ext')
1259 1262 def pull_request_comment_delete(self):
1260 1263 pull_request = PullRequest.get_or_404(
1261 1264 self.request.matchdict['pull_request_id'])
1262 1265
1263 1266 comment = ChangesetComment.get_or_404(
1264 1267 self.request.matchdict['comment_id'])
1265 1268 comment_id = comment.comment_id
1266 1269
1267 1270 if pull_request.is_closed():
1268 1271 log.debug('comment: forbidden because pull request is closed')
1269 1272 raise HTTPForbidden()
1270 1273
1271 1274 if not comment:
1272 1275 log.debug('Comment with id:%s not found, skipping', comment_id)
1273 1276 # comment already deleted in another call probably
1274 1277 return True
1275 1278
1276 1279 if comment.pull_request.is_closed():
1277 1280 # don't allow deleting comments on closed pull request
1278 1281 raise HTTPForbidden()
1279 1282
1280 1283 is_repo_admin = h.HasRepoPermissionAny('repository.admin')(self.db_repo_name)
1281 1284 super_admin = h.HasPermissionAny('hg.admin')()
1282 1285 comment_owner = comment.author.user_id == self._rhodecode_user.user_id
1283 1286 is_repo_comment = comment.repo.repo_name == self.db_repo_name
1284 1287 comment_repo_admin = is_repo_admin and is_repo_comment
1285 1288
1286 1289 if super_admin or comment_owner or comment_repo_admin:
1287 1290 old_calculated_status = comment.pull_request.calculated_review_status()
1288 1291 CommentsModel().delete(comment=comment, auth_user=self._rhodecode_user)
1289 1292 Session().commit()
1290 1293 calculated_status = comment.pull_request.calculated_review_status()
1291 1294 if old_calculated_status != calculated_status:
1292 1295 PullRequestModel()._trigger_pull_request_hook(
1293 1296 comment.pull_request, self._rhodecode_user, 'review_status_change')
1294 1297 return True
1295 1298 else:
1296 1299 log.warning('No permissions for user %s to delete comment_id: %s',
1297 1300 self._rhodecode_db_user, comment_id)
1298 1301 raise HTTPNotFound()
@@ -1,644 +1,684 b''
1 1 // navigation.less
2 2 // For use in RhodeCode applications;
3 3 // see style guide documentation for guidelines.
4 4
5 5 // HEADER NAVIGATION
6 6
7 7 .horizontal-list {
8 8 float: right;
9 9 display: block;
10 10 margin: 0;
11 11 padding: 0;
12 12 -webkit-padding-start: 0;
13 13 text-align: left;
14 14 font-size: @navigation-fontsize;
15 15 color: @grey6;
16 16 z-index:10;
17 17
18 18 li {
19 19 line-height: 1em;
20 20 list-style-type: none;
21 21
22 22 a {
23 23 padding: 0 .5em;
24 24
25 25 &.menu_link_notifications {
26 26 .pill(7px,@rcblue);
27 27 display: inline;
28 28 margin: 0 7px 0 .7em;
29 29 font-size: @basefontsize;
30 30 color: white;
31 31
32 32 &.empty {
33 33 background-color: @grey4;
34 34 }
35 35
36 36 &:hover {
37 37 background-color: @rcdarkblue;
38 38 }
39 39 }
40 40 }
41 41 .pill_container {
42 42 margin: 1.25em 0px 0px 0px;
43 43 float: right;
44 44 }
45 45
46 46 &#quick_login_li {
47 47 &:hover {
48 48 color: @grey5;
49 49 }
50 50
51 51 a.menu_link_notifications {
52 52 color: white;
53 53 }
54 54
55 55 .user {
56 56 padding-bottom: 10px;
57 57 }
58 58
59 59 &.open {
60 60 .user {
61 61 border-bottom: 5px solid @rcblue;
62 62 }
63 63 }
64 64 }
65 65
66 66 &:before { content: none; }
67 67
68 68 &:last-child {
69 69 .menulabel {
70 70 padding-right: 0;
71 71 border-right: none;
72 72
73 73 .show_more {
74 74 padding-right: 0;
75 75 }
76 76 }
77 77
78 78 &> a {
79 79 border-bottom: none;
80 80 }
81 81 }
82 82
83 83 &.active {
84 84 border-bottom: 5px solid @rcblue;
85 85 }
86 86
87 87 &.open {
88 88
89 89 a {
90 90 color: white;
91 91 }
92 92 }
93 93
94 94 &:focus {
95 95 outline: none;
96 96 }
97 97
98 98 ul li {
99 99 display: block;
100 100
101 101 &:last-child> a {
102 102 border-bottom: none;
103 103 }
104 104
105 105 ul li:last-child a {
106 106 /* we don't expect more then 3 levels of submenu and the third
107 107 level can have different html structure */
108 108 border-bottom: none;
109 109 }
110 110 }
111 111 }
112 112
113 113 > li {
114 114 float: left;
115 115 display: block;
116 116 padding: 0;
117 117
118 118 > a,
119 119 &.has_select2 a {
120 120 display: block;
121 121 padding: 10px 0 2px;
122 122
123 123 .show_more {
124 124 margin-top: -4px;
125 125 padding-right: .5em;
126 126 }
127 127 }
128 128
129 129 .menulabel {
130 130 padding: 0 .5em;
131 131 line-height: 1em;
132 132 // for this specifically we do not use a variable
133 133 border-right: 1px solid @grey4;
134 134 }
135 135
136 136 .pr_notifications {
137 137 padding-left: .5em;
138 138 }
139 139
140 140 .pr_notifications + .menulabel {
141 141 display:inline;
142 142 padding-left: 0;
143 143 }
144 144
145 145 &:hover,
146 146 &.open,
147 147 &.active {
148 148 a {
149 149 color: @grey1;
150 150 }
151 151 }
152 152 }
153 153
154 154 pre {
155 155 margin: 0;
156 156 padding: 0;
157 157 }
158 158
159 159 .select2-container,
160 160 .menulink.childs {
161 161 position: relative;
162 162 }
163 163
164 164 #quick_login {
165 165
166 166 li a {
167 167 padding: .5em 0;
168 168 border-bottom: none;
169 169 color: @grey2;
170 170
171 171 &:hover { color: @grey1; }
172 172 }
173 173
174 174 .show_more {
175 175 padding-left: .5em;
176 176 }
177 177 }
178 178
179 179 #quick_login_link {
180 180 display: inline-block;
181 181
182 182 .gravatar {
183 183 border: 1px solid @grey2;
184 184 }
185 185
186 186 .gravatar-login {
187 187 height: 20px;
188 188 width: 20px;
189 189 margin: -8px 0;
190 190 padding: 0;
191 191 }
192 192
193 193 &:hover .user {
194 194 color: @grey6;
195 195 }
196 196 }
197 197 }
198 198 .header .horizontal-list {
199 199
200 200 li {
201 201
202 202 &#quick_login_li {
203 203 padding-left: .5em;
204 204
205 205 &:hover #quick_login_link {
206 206 color: inherit;
207 207 }
208 208 }
209 209
210 210 &:before { content: none; }
211 211 }
212 212
213 213 > li {
214 214
215 215 a {
216 216 padding: 18px 0 12px 0;
217 217 color: @nav-grey;
218 218
219 219 &.menu_link_notifications {
220 220 padding: 1px 8px;
221 221 }
222 222 }
223 223
224 224 &:hover,
225 225 &.open,
226 226 &.active {
227 227 .pill_container a {
228 228 // don't select text for the pill container, it has it' own
229 229 // hover behaviour
230 230 color: @nav-grey;
231 231 }
232 232 }
233 233
234 234 &:hover,
235 235 &.open,
236 236 &.active {
237 237 a {
238 238 color: @grey6;
239 239 }
240 240 }
241 241
242 242 .select2-dropdown-open a {
243 243 color: @grey6;
244 244 }
245 245
246 246 .repo-switcher {
247 247 padding-left: 0;
248 248
249 249 .menulabel {
250 250 padding-left: 0;
251 251 }
252 252 }
253 253 }
254 254
255 255 li ul li {
256 256 background-color:@grey2;
257 257
258 258 a {
259 259 padding: .5em 0;
260 260 border-bottom: @border-thickness solid @border-default-color;
261 261 color: @grey6;
262 262 }
263 263
264 264 &:last-child a, &.last a{
265 265 border-bottom: none;
266 266 }
267 267
268 268 &:hover {
269 269 background-color: @grey3;
270 270 }
271 271 }
272 272
273 273 .submenu {
274 274 margin-top: 5px;
275 275 }
276 276 }
277 277
278 278 // SUBMENUS
279 279 .navigation .submenu {
280 280 display: none;
281 281 }
282 282
283 283 .navigation li.open {
284
285 .submenu,
286 .repo_switcher {
284 .submenu {
287 285 display: block;
288 286 }
289 287 }
290 288
291 289 .navigation li:last-child .submenu {
292 290 right: -20px;
293 291 left: auto;
294 292 }
295 293
296 294 .submenu {
297 295 position: absolute;
298 296 top: 100%;
299 297 left: 0;
300 298 min-width: 150px;
301 299 margin: 6px 0 0;
302 300 padding: 0;
303 301 text-align: left;
304 302 font-family: @text-light;
305 303 border-radius: @border-radius;
306 304 z-index: 20;
307 305
308 306 li {
309 307 display: block;
310 308 margin: 0;
311 309 padding: 0 .5em;
312 310 line-height: 1em;
313 311 color: @grey3;
314 312 background-color: @grey6;
315 313
316 314 &:before { content: none; }
317 315
318 316 a {
319 317 display: block;
320 318 width: 100%;
321 319 padding: .5em 0;
322 320 border-right: none;
323 321 border-bottom: @border-thickness solid white;
324 322 color: @grey3;
325 323 }
326 324
327 325 ul {
328 326 display: none;
329 327 position: absolute;
330 328 top: 0;
331 329 right: 100%;
332 330 padding: 0;
333 331 z-index: 30;
334 332 }
335 333 &:hover {
336 334 background-color: @grey5;
337 335 -webkit-transition: background .3s;
338 336 -moz-transition: background .3s;
339 337 -o-transition: background .3s;
340 338 transition: background .3s;
341 339
342 340 ul {
343 341 display: block;
344 342 }
345 343 }
346 344 }
347 345 }
348 346
349 347
350 348
351 349
352 350 // repo dropdown
353 351 .quick_repo_menu {
354 352 width: 15px;
355 353 text-align: center;
356 354 position: relative;
357 355 cursor: pointer;
358 356
359 357 div {
360 358 overflow: visible !important;
361 359 }
362 360
363 361 &.sorting {
364 362 cursor: auto;
365 363 }
366 364
367 365 &:hover {
368 366 .menu_items_container {
369 367 position: absolute;
370 368 display: block;
371 369 }
372 370 .menu_items {
373 371 display: block;
374 372 }
375 373 }
376 374
377 375 i {
378 376 margin: 0;
379 377 color: @grey4;
380 378 }
381 379
382 380 .menu_items_container {
383 381 position: absolute;
384 382 top: 0;
385 383 left: 100%;
386 384 margin: 0;
387 385 padding: 0;
388 386 list-style: none;
389 387 background-color: @grey6;
390 388 z-index: 999;
391 389 text-align: left;
392 390
393 391 a {
394 392 color: @grey2;
395 393 }
396 394
397 395 ul.menu_items {
398 396 margin: 0;
399 397 padding: 0;
400 398 }
401 399
402 400 li {
403 401 margin: 0;
404 402 padding: 0;
405 403 line-height: 1em;
406 404 list-style-type: none;
407 405
408 406 &:before { content: none; }
409 407
410 408 a {
411 409 display: block;
412 410 height: 16px;
413 411 padding: 8px; //must add up to td height (28px)
414 412
415 413 &:hover {
416 414 background-color: @grey5;
417 415 -webkit-transition: background .3s;
418 416 -moz-transition: background .3s;
419 417 -o-transition: background .3s;
420 418 transition: background .3s;
421 419 }
422 420 }
423 421 }
424 422 }
425 423 }
426 424
427 425 // Header Repository Switcher
428 426 // Select2 Dropdown
429 427 #select2-drop.select2-drop.repo-switcher-dropdown {
430 428 width: auto !important;
431 429 margin-top: 5px;
432 430 padding: 1em 0;
433 431 text-align: left;
434 432 .border-radius-bottom(@border-radius);
435 433 border-color: transparent;
436 434 color: @grey6;
437 435 background-color: @grey2;
438 436
439 437 input {
440 438 min-width: 90%;
441 439 }
442 440
443 441 ul.select2-result-sub {
444 442
445 443 li {
446 444 line-height: 1em;
447 445
448 446 &:hover,
449 447 &.select2-highlighted {
450 448 background-color: @grey3;
451 449 }
452 450 }
453 451
454 452 &:before { content: none; }
455 453 }
456 454
457 455 ul.select2-results {
458 456 min-width: 200px;
459 457 margin: 0;
460 458 padding: 0;
461 459 list-style-type: none;
462 460 overflow-x: visible;
463 461 overflow-y: scroll;
464 462
465 463 li {
466 464 padding: 0 8px;
467 465 line-height: 1em;
468 466 color: @grey6;
469 467
470 468 &:before { content: none; }
471 469
472 470 &>.select2-result-label {
473 471 padding: 8px 0;
474 472 border-bottom: @border-thickness solid @grey3;
475 473 white-space: nowrap;
476 474 color: @grey5;
477 475 cursor: pointer;
478 476 }
479 477
480 478 &.select2-result-with-children {
481 479 margin: 0;
482 480 padding: 0;
483 481 }
484 482
485 483 &.select2-result-unselectable > .select2-result-label {
486 484 margin: 0 8px;
487 485 }
488 486
489 487 }
490 488 }
491 489
492 490 ul.select2-result-sub {
493 491 margin: 0;
494 492 padding: 0;
495 493
496 494 li {
497 495 display: block;
498 496 margin: 0;
499 497 border-right: none;
500 498 line-height: 1em;
501 499 font-family: @text-light;
502 500 color: @grey2;
503 501
504 502 &:before { content: none; }
505 503
506 504 &:hover {
507 505 background-color: @grey3;
508 506 }
509 507 }
510 508 }
511 509 }
512 510
513 511
514 512 #context-bar {
515 513 display: block;
516 514 margin: 0 auto;
517 515 padding: 0 @header-padding;
518 516 background-color: @grey6;
519 517 border-bottom: @border-thickness solid @grey5;
520 518
521 519 .clear {
522 520 clear: both;
523 521 }
524 522 }
525 523
526 524 ul#context-pages {
527 525 li {
528 526 line-height: 1em;
529 527
530 528 &:before { content: none; }
531 529
532 530 a {
533 531 color: @grey3;
534 532 }
535 533
536 534 &.active {
537 535 // special case, non-variable color
538 536 border-bottom: 4px solid @nav-grey;
539 537
540 538 a {
541 539 color: @grey1;
542 540 }
543 541 }
544 542 }
545 543 }
546 544
547 545 // PAGINATION
548 546
549 547 .pagination {
550 548 border: @border-thickness solid @rcblue;
551 549 color: @rcblue;
552 550
553 551 .current {
554 552 color: @grey4;
555 553 }
556 554 }
557 555
558 556 .dataTables_processing {
559 557 text-align: center;
560 558 font-size: 1.1em;
561 559 position: relative;
562 560 top: 95px;
563 561 }
564 562
565 563 .dataTables_paginate, .pagination-wh {
566 564 text-align: left;
567 565 display: inline-block;
568 566 border-left: 1px solid @rcblue;
569 567 float: none;
570 568 overflow: hidden;
571 569
572 570 .paginate_button, .pager_curpage,
573 571 .pager_link, .pg-previous, .pg-next, .pager_dotdot {
574 572 display: inline-block;
575 573 padding: @menupadding/4 @menupadding;
576 574 border: 1px solid @rcblue;
577 575 border-left: 0;
578 576 color: @rcblue;
579 577 cursor: pointer;
580 578 float: left;
581 579 }
582 580
583 581 .pager_curpage, .pager_dotdot,
584 582 .paginate_button.current, .paginate_button.disabled,
585 583 .disabled {
586 584 color: @grey3;
587 585 cursor: default;
588 586 }
589 587
590 588 .ellipsis {
591 589 display: inline-block;
592 590 text-align: left;
593 591 padding: @menupadding/4 @menupadding;
594 592 border: 1px solid @rcblue;
595 593 border-left: 0;
596 594 float: left;
597 595 }
598 596 }
599 597
600 598 // SIDEBAR
601 599
602 600 .sidebar {
603 601 .block-left;
604 602 clear: left;
605 603 max-width: @sidebar-width;
606 604 margin-right: @sidebarpadding;
607 605 padding-right: @sidebarpadding;
608 606 font-family: @text-regular;
609 607 color: @grey1;
610 608
611 609 &#graph_nodes {
612 610 clear:both;
613 611 width: auto;
614 612 margin-left: -100px;
615 613 padding: 0;
616 614 border: none;
617 615 }
618 616
619 617 .nav-pills {
620 618 margin: 0;
621 619 }
622 620
623 621 .nav {
624 622 list-style: none;
625 623 padding: 0;
626 624
627 625 li {
628 626 padding-bottom: @menupadding;
629 627 line-height: 1em;
630 628 color: @grey4;
631 629
632 630 &.active a {
633 631 color: @grey2;
634 632 }
635 633
636 634 a {
637 635 color: @grey4;
638 636 }
639 637
640 638 &:before { content: none; }
641 639 }
642 640
643 641 }
644 642 }
643
644 .main_filter_help_box {
645 padding: 7px 7px;
646 border-top: 1px solid @grey4;
647 border-right: 1px solid @grey4;
648 border-bottom: 1px solid @grey4;
649 display: inline-block;
650 vertical-align: top;
651 margin-left: -5px;
652 background: @grey3;
653 }
654
655 .main_filter_input_box {
656 display: inline-block;
657 }
658
659 .main_filter_box {
660 margin: 9px 0 0 0;
661 }
662
663 #main_filter_help {
664 background: @grey3;
665 border: 1px solid black;
666 position: absolute;
667 white-space: pre-wrap;
668 z-index: 9999;
669 color: @nav-grey;
670 margin: 1px 7px;
671 padding: 0 2px;
672 }
673
674 .main_filter_input {
675 padding: 6px;
676 min-width: 220px;
677 color: @nav-grey;
678 background: @grey3;
679 }
680
681 .main_filter_input::placeholder {
682 color: @nav-grey;
683 opacity: 1;
684 }
@@ -1,545 +1,545 b''
1 1 //
2 2 // Typography
3 3 // modified from Bootstrap
4 4 // --------------------------------------------------
5 5
6 6 // Base
7 7 body {
8 8 font-size: @basefontsize;
9 9 font-family: @text-light;
10 10 letter-spacing: .02em;
11 11 color: @grey2;
12 12 }
13 13
14 14 #content, label{
15 15 font-size: @basefontsize;
16 16 }
17 17
18 18 label {
19 19 color: @grey2;
20 20 }
21 21
22 22 ::selection { background: @rchighlightblue; }
23 23
24 24 // Headings
25 25 // -------------------------
26 26
27 27 h1, h2, h3, h4, h5, h6,
28 28 .h1, .h2, .h3, .h4, .h5, .h6 {
29 29 margin: 0 0 @textmargin 0;
30 30 padding: 0;
31 31 line-height: 1.8em;
32 32 color: @text-color;
33 33 a {
34 34 color: @rcblue;
35 35 }
36 36 }
37 37
38 38 h1, .h1 { font-size: 1.54em; font-family: @text-bold; }
39 39 h2, .h2 { font-size: 1.23em; font-family: @text-semibold; }
40 40 h3, .h3 { font-size: 1.23em; font-family: @text-regular; }
41 41 h4, .h4 { font-size: 1em; font-family: @text-bold; }
42 42 h5, .h5 { font-size: 1em; font-family: @text-bold-italic; }
43 43 h6, .h6 { font-size: 1em; font-family: @text-bold-italic; }
44 44
45 45 // Breadcrumbs
46 46 .breadcrumbs {
47 47 &:extend(h1);
48 48 margin: 0;
49 49 }
50 50
51 51 .breadcrumbs_light {
52 52 float:left;
53 53 font-size: 1.3em;
54 54 line-height: 38px;
55 55 }
56 56
57 57 // Body text
58 58 // -------------------------
59 59
60 60 p {
61 61 margin: 0 0 @textmargin 0;
62 62 padding: 0;
63 63 line-height: 2em;
64 64 }
65 65
66 66 .lead {
67 67 margin-bottom: @textmargin;
68 68 font-weight: 300;
69 69 line-height: 1.4;
70 70
71 71 @media (min-width: @screen-sm-min) {
72 72 font-size: (@basefontsize * 1.5);
73 73 }
74 74 }
75 75
76 76 a,
77 77 .link {
78 78 color: @rcblue;
79 79 text-decoration: none;
80 80 outline: none;
81 81 cursor: pointer;
82 82
83 83 &:focus {
84 84 outline: none;
85 85 }
86 86
87 87 &:hover {
88 88 color: @rcdarkblue;
89 89 }
90 90 }
91 91
92 92 img {
93 93 border: none;
94 94 outline: none;
95 95 }
96 96
97 97 strong {
98 98 font-family: @text-bold;
99 99 }
100 100
101 101 em {
102 102 font-family: @text-italic;
103 103 }
104 104
105 105 strong em,
106 106 em strong {
107 107 font-family: @text-bold-italic;
108 108 }
109 109
110 110 //TODO: lisa: b and i are depreciated, but we are still using them in places.
111 111 // Should probably make some decision whether to keep or lose these.
112 112 b {
113 113
114 114 }
115 115
116 116 i {
117 117 font-style: normal;
118 118 }
119 119
120 120 label {
121 121 color: @text-color;
122 122
123 123 input[type="checkbox"] {
124 124 margin-right: 1em;
125 125 }
126 126 input[type="radio"] {
127 127 margin-right: 1em;
128 128 }
129 129 }
130 130
131 131 code,
132 132 .code {
133 133 font-size: .95em;
134 134 font-family: @text-code;
135 135 color: @grey3;
136 136
137 137 a {
138 138 color: lighten(@rcblue,10%)
139 139 }
140 140 }
141 141
142 142 pre {
143 143 margin: 0;
144 144 padding: 0;
145 145 border: 0;
146 146 outline: 0;
147 147 font-size: @basefontsize*.95;
148 148 line-height: 1.4em;
149 149 font-family: @text-code;
150 150 color: @grey3;
151 151 }
152 152
153 153 // Emphasis & misc
154 154 // -------------------------
155 155
156 156 small,
157 157 .small {
158 158 font-size: 75%;
159 159 font-weight: normal;
160 160 line-height: 1em;
161 161 }
162 162
163 163 mark,
164 164 .mark {
165 165 background-color: @rclightblue;
166 166 padding: .2em;
167 167 }
168 168
169 169 // Alignment
170 170 .text-left { text-align: left; }
171 171 .text-right { text-align: right; }
172 172 .text-center { text-align: center; }
173 173 .text-justify { text-align: justify; }
174 174 .text-nowrap { white-space: nowrap; }
175 175
176 176 // Transformation
177 177 .text-lowercase { text-transform: lowercase; }
178 178 .text-uppercase { text-transform: uppercase; }
179 179 .text-capitalize { text-transform: capitalize; }
180 180
181 181 // Contextual colors
182 182 .text-muted {
183 183 color: @grey4;
184 184 }
185 185 .text-primary {
186 186 color: @rcblue;
187 187 }
188 188 .text-success {
189 189 color: @alert1;
190 190 }
191 191 .text-info {
192 192 color: @alert4;
193 193 }
194 194 .text-warning {
195 195 color: @alert3;
196 196 }
197 197 .text-danger {
198 198 color: @alert2;
199 199 }
200 200
201 201 // Contextual backgrounds
202 202 .bg-primary {
203 203 background-color: white;
204 204 }
205 205 .bg-success {
206 206 background-color: @alert1;
207 207 }
208 208 .bg-info {
209 209 background-color: @alert4;
210 210 }
211 211 .bg-warning {
212 212 background-color: @alert3;
213 213 }
214 214 .bg-danger {
215 215 background-color: @alert2;
216 216 }
217 217
218 218
219 219 // Page header
220 220 // -------------------------
221 221
222 222 .page-header {
223 223 margin: @pagepadding 0 @textmargin;
224 224 border-bottom: @border-thickness solid @grey5;
225 225 }
226 226
227 227 .title {
228 228 clear: both;
229 229 float: left;
230 230 width: 100%;
231 margin: @pagepadding 0 @pagepadding;
231 margin: @pagepadding/2 0 @pagepadding;
232 232
233 233 .breadcrumbs{
234 234 float: left;
235 235 clear: both;
236 236 width: 700px;
237 237 margin: 0;
238 238
239 239 .q_filter_box {
240 240 margin-right: @padding;
241 241 }
242 242 }
243 243
244 244 h1 a {
245 245 color: @rcblue;
246 246 }
247 247
248 248 input{
249 249 margin-right: @padding;
250 250 }
251 251
252 252 h5, .h5 {
253 253 color: @grey1;
254 254 margin-bottom: @space;
255 255
256 256 span {
257 257 display: inline-block;
258 258 }
259 259 }
260 260
261 261 p {
262 262 margin-bottom: 0;
263 263 }
264 264
265 265 .links {
266 266 float: right;
267 267 display: inline;
268 268 margin: 0;
269 269 padding-left: 0;
270 270 list-style: none;
271 271 text-align: right;
272 272
273 273 li:before { content: none; }
274 274 li { float: right; }
275 275 a {
276 276 display: inline-block;
277 277 margin-left: @textmargin/2;
278 278 }
279 279 }
280 280
281 281 .title-content {
282 282 float: left;
283 283 margin: 0;
284 284 padding: 0;
285 285
286 286 & + .breadcrumbs {
287 287 margin-top: @padding;
288 288 }
289 289
290 290 & + .links {
291 291 margin-top: -@button-padding;
292 292
293 293 & + .breadcrumbs {
294 294 margin-top: @padding;
295 295 }
296 296 }
297 297 }
298 298
299 299 .title-main {
300 300 font-size: @repo-title-fontsize;
301 301 }
302 302
303 303 .title-description {
304 304 margin-top: .5em;
305 305 }
306 306
307 307 .q_filter_box {
308 308 width: 200px;
309 309 }
310 310
311 311 }
312 312
313 313 #readme .title {
314 314 text-transform: none;
315 315 }
316 316
317 317 // Lists
318 318 // -------------------------
319 319
320 320 // Unordered and Ordered lists
321 321 ul,
322 322 ol {
323 323 margin-top: 0;
324 324 margin-bottom: @textmargin;
325 325 ul,
326 326 ol {
327 327 margin-bottom: 0;
328 328 }
329 329 }
330 330
331 331 li {
332 332 line-height: 2em;
333 333 }
334 334
335 335 ul li {
336 336 position: relative;
337 337 display: block;
338 338 list-style-type: none;
339 339
340 340 &:before {
341 341 content: "\2014\00A0";
342 342 position: absolute;
343 343 top: 0;
344 344 left: -1.25em;
345 345 }
346 346
347 347 p:first-child {
348 348 display:inline;
349 349 }
350 350 }
351 351
352 352 // List options
353 353
354 354 // Unstyled keeps list items block level, just removes default browser padding and list-style
355 355 .list-unstyled {
356 356 padding-left: 0;
357 357 list-style: none;
358 358 li:before { content: none; }
359 359 }
360 360
361 361 // Inline turns list items into inline-block
362 362 .list-inline {
363 363 .list-unstyled();
364 364 margin-left: -5px;
365 365
366 366 > li {
367 367 display: inline-block;
368 368 padding-left: 5px;
369 369 padding-right: 5px;
370 370 }
371 371 }
372 372
373 373 // Description Lists
374 374
375 375 dl {
376 376 margin-top: 0; // Remove browser default
377 377 margin-bottom: @textmargin;
378 378 }
379 379
380 380 dt,
381 381 dd {
382 382 line-height: 1.4em;
383 383 }
384 384
385 385 dt {
386 386 margin: @textmargin 0 0 0;
387 387 font-family: @text-bold;
388 388 }
389 389
390 390 dd {
391 391 margin-left: 0; // Undo browser default
392 392 }
393 393
394 394 // Horizontal description lists
395 395 // Defaults to being stacked without any of the below styles applied, until the
396 396 // grid breakpoint is reached (default of ~768px).
397 397 // These are used in forms as well; see style guide.
398 398 // TODO: lisa: These should really not be used in forms.
399 399
400 400 .dl-horizontal {
401 401
402 402 overflow: hidden;
403 403 margin-bottom: @space;
404 404
405 405 dt, dd {
406 406 float: left;
407 407 margin: 5px 0 5px 0;
408 408 }
409 409
410 410 dt {
411 411 clear: left;
412 412 width: @label-width - @form-vertical-margin;
413 413 }
414 414
415 415 dd {
416 416 &:extend(.clearfix all); // Clear the floated `dt` if an empty `dd` is present
417 417 margin-left: @form-vertical-margin;
418 418 max-width: @form-max-width - (@label-width - @form-vertical-margin) - @form-vertical-margin;
419 419 }
420 420
421 421 pre {
422 422 margin: 0;
423 423 }
424 424
425 425 &.settings {
426 426 dt {
427 427 text-align: left;
428 428 }
429 429 }
430 430
431 431 @media (min-width: 768px) {
432 432 dt {
433 433 float: left;
434 434 width: 180px;
435 435 clear: left;
436 436 text-align: right;
437 437 }
438 438 dd {
439 439 margin-left: 20px;
440 440 }
441 441 }
442 442 }
443 443
444 444
445 445 // Misc
446 446 // -------------------------
447 447
448 448 // Abbreviations and acronyms
449 449 abbr[title],
450 450 abbr[data-original-title] {
451 451 cursor: help;
452 452 border-bottom: @border-thickness dotted @grey4;
453 453 }
454 454 .initialism {
455 455 font-size: 90%;
456 456 text-transform: uppercase;
457 457 }
458 458
459 459 // Blockquotes
460 460 blockquote {
461 461 padding: 1em 2em;
462 462 margin: 0 0 2em;
463 463 font-size: @basefontsize;
464 464 border-left: 2px solid @grey6;
465 465
466 466 p,
467 467 ul,
468 468 ol {
469 469 &:last-child {
470 470 margin-bottom: 0;
471 471 }
472 472 }
473 473
474 474 footer,
475 475 small,
476 476 .small {
477 477 display: block;
478 478 font-size: 80%;
479 479
480 480 &:before {
481 481 content: '\2014 \00A0'; // em dash, nbsp
482 482 }
483 483 }
484 484 }
485 485
486 486 // Opposite alignment of blockquote
487 487 //
488 488 .blockquote-reverse,
489 489 blockquote.pull-right {
490 490 padding-right: 15px;
491 491 padding-left: 0;
492 492 border-right: 5px solid @grey6;
493 493 border-left: 0;
494 494 text-align: right;
495 495
496 496 // Account for citation
497 497 footer,
498 498 small,
499 499 .small {
500 500 &:before { content: ''; }
501 501 &:after {
502 502 content: '\00A0 \2014'; // nbsp, em dash
503 503 }
504 504 }
505 505 }
506 506
507 507 // Addresses
508 508 address {
509 509 margin-bottom: 2em;
510 510 font-style: normal;
511 511 line-height: 1.8em;
512 512 }
513 513
514 514 .error-message {
515 515 display: block;
516 516 margin: @padding/3 0;
517 517 color: @alert2;
518 518 }
519 519
520 520 .issue-tracker-link {
521 521 color: @rcblue;
522 522 }
523 523
524 524 .info_text{
525 525 font-size: @basefontsize;
526 526 color: @grey4;
527 527 font-family: @text-regular;
528 528 }
529 529
530 530 // help block text
531 531 .help-block {
532 532 display: block;
533 533 margin: 0 0 @padding;
534 534 color: @grey4;
535 535 font-family: @text-light;
536 536 &.pre-formatting {
537 537 white-space: pre;
538 538 }
539 539 }
540 540
541 541 .error-message {
542 542 display: block;
543 543 margin: @padding/3 0;
544 544 color: @alert2;
545 545 }
@@ -1,101 +1,101 b''
1 1 // Global keyboard bindings
2 2
3 3 function setRCMouseBindings(repoName, repoLandingRev) {
4 4
5 5 /** custom callback for supressing mousetrap from firing */
6 6 Mousetrap.stopCallback = function(e, element) {
7 7 // if the element has the class "mousetrap" then no need to stop
8 8 if ((' ' + element.className + ' ').indexOf(' mousetrap ') > -1) {
9 9 return false;
10 10 }
11 11
12 12 // stop for input, select, and textarea
13 13 return element.tagName == 'INPUT' || element.tagName == 'SELECT' || element.tagName == 'TEXTAREA' || element.isContentEditable;
14 14 };
15 15
16 16 // general help "?"
17 17 Mousetrap.bind(['?'], function(e) {
18 18 $('#help_kb').modal({});
19 19 });
20 20
21 21 // / open the quick filter
22 22 Mousetrap.bind(['/'], function(e) {
23 $('#repo_switcher').select2('open');
23 $('#main_filter').get(0).focus();
24 24
25 25 // return false to prevent default browser behavior
26 26 // and stop event from bubbling
27 27 return false;
28 28 });
29 29
30 30 // ctrl/command+b, show the the main bar
31 31 Mousetrap.bind(['command+b', 'ctrl+b'], function(e) {
32 32 var $headerInner = $('#header-inner'),
33 33 $content = $('#content');
34 34 if ($headerInner.hasClass('hover') && $content.hasClass('hover')) {
35 35 $headerInner.removeClass('hover');
36 36 $content.removeClass('hover');
37 37 } else {
38 38 $headerInner.addClass('hover');
39 39 $content.addClass('hover');
40 40 }
41 41 return false;
42 42 });
43 43
44 44 // general nav g + action
45 45 Mousetrap.bind(['g h'], function(e) {
46 46 window.location = pyroutes.url('home');
47 47 });
48 48 Mousetrap.bind(['g g'], function(e) {
49 49 window.location = pyroutes.url('gists_show', {'private': 1});
50 50 });
51 51 Mousetrap.bind(['g G'], function(e) {
52 52 window.location = pyroutes.url('gists_show', {'public': 1});
53 53 });
54 54 Mousetrap.bind(['n g'], function(e) {
55 55 window.location = pyroutes.url('gists_new');
56 56 });
57 57 Mousetrap.bind(['n r'], function(e) {
58 58 window.location = pyroutes.url('repo_new');
59 59 });
60 60
61 61 if (repoName && repoName != '') {
62 62 // nav in repo context
63 63 Mousetrap.bind(['g s'], function(e) {
64 64 window.location = pyroutes.url(
65 65 'repo_summary', {'repo_name': repoName});
66 66 });
67 67 Mousetrap.bind(['g c'], function(e) {
68 68 window.location = pyroutes.url(
69 69 'repo_changelog', {'repo_name': repoName});
70 70 });
71 71 Mousetrap.bind(['g F'], function(e) {
72 72 window.location = pyroutes.url(
73 73 'repo_files',
74 74 {
75 75 'repo_name': repoName,
76 76 'commit_id': repoLandingRev,
77 77 'f_path': '',
78 78 'search': '1'
79 79 });
80 80 });
81 81 Mousetrap.bind(['g f'], function(e) {
82 82 window.location = pyroutes.url(
83 83 'repo_files',
84 84 {
85 85 'repo_name': repoName,
86 86 'commit_id': repoLandingRev,
87 87 'f_path': ''
88 88 });
89 89 });
90 90 Mousetrap.bind(['g o'], function(e) {
91 91 window.location = pyroutes.url(
92 92 'edit_repo', {'repo_name': repoName});
93 93 });
94 94 Mousetrap.bind(['g O'], function(e) {
95 95 window.location = pyroutes.url(
96 96 'edit_repo_perms', {'repo_name': repoName});
97 97 });
98 98 }
99 99 }
100 100
101 101 setRCMouseBindings(templateContext.repo_name, templateContext.repo_landing_commit);
@@ -1,188 +1,188 b''
1 1 <div class="panel panel-default">
2 2 <div class="panel-heading">
3 3 <h3 class="panel-title">${_('Authentication Tokens')}</h3>
4 4 </div>
5 5 <div class="panel-body">
6 6 <div class="apikeys_wrap">
7 7 <p>
8 8 ${_('Each token can have a role. Token with a role can be used only in given context, '
9 9 'e.g. VCS tokens can be used together with the authtoken auth plugin for git/hg/svn operations only.')}
10 10 </p>
11 11 <table class="rctable auth_tokens">
12 12 <tr>
13 13 <th>${_('Token')}</th>
14 14 <th>${_('Scope')}</th>
15 15 <th>${_('Description')}</th>
16 16 <th>${_('Role')}</th>
17 17 <th>${_('Expiration')}</th>
18 18 <th>${_('Action')}</th>
19 19 </tr>
20 20 %if c.user_auth_tokens:
21 21 %for auth_token in c.user_auth_tokens:
22 22 <tr class="${'expired' if auth_token.expired else ''}">
23 23 <td class="truncate-wrap td-authtoken">
24 24 <div class="user_auth_tokens truncate autoexpand">
25 25 <code>${auth_token.api_key}</code>
26 26 </div>
27 27 </td>
28 28 <td class="td">${auth_token.scope_humanized}</td>
29 29 <td class="td-wrap">${auth_token.description}</td>
30 30 <td class="td-tags">
31 31 <span class="tag disabled">${auth_token.role_humanized}</span>
32 32 </td>
33 33 <td class="td-exp">
34 34 %if auth_token.expires == -1:
35 35 ${_('never')}
36 36 %else:
37 37 %if auth_token.expired:
38 38 <span style="text-decoration: line-through">${h.age_component(h.time_to_utcdatetime(auth_token.expires))}</span>
39 39 %else:
40 40 ${h.age_component(h.time_to_utcdatetime(auth_token.expires))}
41 41 %endif
42 42 %endif
43 43 </td>
44 44 <td class="td-action">
45 45 ${h.secure_form(h.route_path('my_account_auth_tokens_delete'), request=request)}
46 46 ${h.hidden('del_auth_token', auth_token.user_api_key_id)}
47 47 <button class="btn btn-link btn-danger" type="submit"
48 48 onclick="return confirm('${_('Confirm to remove this auth token: %s') % auth_token.token_obfuscated}');">
49 49 ${_('Delete')}
50 50 </button>
51 51 ${h.end_form()}
52 52 </td>
53 53 </tr>
54 54 %endfor
55 55 %else:
56 56 <tr><td><div class="ip">${_('No additional auth tokens specified')}</div></td></tr>
57 57 %endif
58 58 </table>
59 59 </div>
60 60
61 61 <div class="user_auth_tokens">
62 62 ${h.secure_form(h.route_path('my_account_auth_tokens_add'), request=request)}
63 63 <div class="form form-vertical">
64 64 <!-- fields -->
65 65 <div class="fields">
66 66 <div class="field">
67 67 <div class="label">
68 68 <label for="new_email">${_('New authentication token')}:</label>
69 69 </div>
70 70 <div class="input">
71 71 ${h.text('description', class_='medium', placeholder=_('Description'))}
72 72 ${h.hidden('lifetime')}
73 73 ${h.select('role', '', c.role_options)}
74 74
75 75 % if c.allow_scoped_tokens:
76 76 ${h.hidden('scope_repo_id')}
77 77 % else:
78 78 ${h.select('scope_repo_id_disabled', '', ['Scopes available in EE edition'], disabled='disabled')}
79 79 % endif
80 80 </div>
81 81 <p class="help-block">
82 82 ${_('Repository scope works only with tokens with VCS type.')}
83 83 </p>
84 84 </div>
85 85 <div class="buttons">
86 86 ${h.submit('save',_('Add'),class_="btn")}
87 87 ${h.reset('reset',_('Reset'),class_="btn")}
88 88 </div>
89 89 </div>
90 90 </div>
91 91 ${h.end_form()}
92 92 </div>
93 93 </div>
94 94 </div>
95 95 <script>
96 96 $(document).ready(function(){
97 97
98 98 var select2Options = {
99 99 'containerCssClass': "drop-menu",
100 100 'dropdownCssClass': "drop-menu-dropdown",
101 101 'dropdownAutoWidth': true
102 102 };
103 103 $("#role").select2(select2Options);
104 104
105 105 var preloadData = {
106 106 results: [
107 107 % for entry in c.lifetime_values:
108 108 {id:${entry[0]}, text:"${entry[1]}"}${'' if loop.last else ','}
109 109 % endfor
110 110 ]
111 111 };
112 112
113 113 $("#lifetime").select2({
114 114 containerCssClass: "drop-menu",
115 115 dropdownCssClass: "drop-menu-dropdown",
116 116 dropdownAutoWidth: true,
117 117 data: preloadData,
118 118 placeholder: "${_('Select or enter expiration date')}",
119 119 query: function(query) {
120 120 feedLifetimeOptions(query, preloadData);
121 121 }
122 122 });
123 123
124 124
125 125 var repoFilter = function(data) {
126 126 var results = [];
127 127
128 128 if (!data.results[0]) {
129 129 return data
130 130 }
131 131
132 132 $.each(data.results[0].children, function() {
133 133 // replace name to ID for submision
134 this.id = this.obj.repo_id;
134 this.id = this.repo_id;
135 135 results.push(this);
136 136 });
137 137
138 138 data.results[0].children = results;
139 139 return data;
140 140 };
141 141
142 142 $("#scope_repo_id_disabled").select2(select2Options);
143 143
144 144 var selectVcsScope = function() {
145 145 // select vcs scope and disable input
146 146 $("#role").select2("val", "${c.role_vcs}").trigger('change');
147 147 $("#role").select2("readonly", true)
148 148 };
149 149
150 150 $("#scope_repo_id").select2({
151 151 cachedDataSource: {},
152 152 minimumInputLength: 2,
153 153 placeholder: "${_('repository scope')}",
154 154 dropdownAutoWidth: true,
155 155 containerCssClass: "drop-menu",
156 156 dropdownCssClass: "drop-menu-dropdown",
157 formatResult: formatResult,
157 formatResult: formatRepoResult,
158 158 query: $.debounce(250, function(query){
159 159 self = this;
160 160 var cacheKey = query.term;
161 161 var cachedData = self.cachedDataSource[cacheKey];
162 162
163 163 if (cachedData) {
164 164 query.callback({results: cachedData.results});
165 165 } else {
166 166 $.ajax({
167 167 url: pyroutes.url('repo_list_data'),
168 168 data: {'query': query.term},
169 169 dataType: 'json',
170 170 type: 'GET',
171 171 success: function(data) {
172 172 data = repoFilter(data);
173 173 self.cachedDataSource[cacheKey] = data;
174 174 query.callback({results: data.results});
175 175 },
176 176 error: function(data, textStatus, errorThrown) {
177 177 alert("Error while fetching entries.\nError code {0} ({1}).".format(data.status, data.statusText));
178 178 }
179 179 })
180 180 }
181 181 })
182 182 });
183 183 $("#scope_repo_id").on('select2-selecting', function(e){
184 184 selectVcsScope()
185 185 });
186 186
187 187 });
188 188 </script>
@@ -1,211 +1,211 b''
1 1 <%namespace name="base" file="/base/base.mako"/>
2 2
3 3 <%
4 4 elems = [
5 5 (_('Owner'), lambda:base.gravatar_with_user(c.rhodecode_db_repo.user.email), '', ''),
6 6 (_('Created on'), h.format_date(c.rhodecode_db_repo.created_on), '', ''),
7 7 (_('Updated on'), h.format_date(c.rhodecode_db_repo.updated_on), '', ''),
8 8 (_('Cached Commit id'), lambda: h.link_to(c.rhodecode_db_repo.changeset_cache.get('short_id'), h.route_path('repo_commit',repo_name=c.repo_name,commit_id=c.rhodecode_db_repo.changeset_cache.get('raw_id'))), '', ''),
9 9 (_('Attached scoped tokens'), len(c.rhodecode_db_repo.scoped_tokens), '', [x.user for x in c.rhodecode_db_repo.scoped_tokens]),
10 10 ]
11 11 %>
12 12
13 13 <div class="panel panel-default">
14 14 <div class="panel-heading" id="advanced-info" >
15 15 <h3 class="panel-title">${_('Repository: %s') % c.rhodecode_db_repo.repo_name} <a class="permalink" href="#advanced-info"> ΒΆ</a></h3>
16 16 </div>
17 17 <div class="panel-body">
18 18 ${base.dt_info_panel(elems)}
19 19 </div>
20 20 </div>
21 21
22 22
23 23 <div class="panel panel-default">
24 24 <div class="panel-heading" id="advanced-fork">
25 25 <h3 class="panel-title">${_('Fork Reference')} <a class="permalink" href="#advanced-fork"> ΒΆ</a></h3>
26 26 </div>
27 27 <div class="panel-body">
28 28 ${h.secure_form(h.route_path('edit_repo_advanced_fork', repo_name=c.rhodecode_db_repo.repo_name), request=request)}
29 29
30 30 % if c.rhodecode_db_repo.fork:
31 31 <div class="panel-body-title-text">${h.literal(_('This repository is a fork of %(repo_link)s') % {'repo_link': h.link_to_if(c.has_origin_repo_read_perm,c.rhodecode_db_repo.fork.repo_name, h.route_path('repo_summary', repo_name=c.rhodecode_db_repo.fork.repo_name))})}
32 32 | <button class="btn btn-link btn-danger" type="submit">Remove fork reference</button></div>
33 33 % endif
34 34
35 35 <div class="field">
36 36 ${h.hidden('id_fork_of')}
37 37 ${h.submit('set_as_fork_%s' % c.rhodecode_db_repo.repo_name,_('Set'),class_="btn btn-small",)}
38 38 </div>
39 39 <div class="field">
40 40 <span class="help-block">${_('Manually set this repository as a fork of another from the list')}</span>
41 41 </div>
42 42 ${h.end_form()}
43 43 </div>
44 44 </div>
45 45
46 46
47 47 <div class="panel panel-default">
48 48 <div class="panel-heading" id="advanced-journal">
49 49 <h3 class="panel-title">${_('Public Journal Visibility')} <a class="permalink" href="#advanced-journal"> ΒΆ</a></h3>
50 50 </div>
51 51 <div class="panel-body">
52 52 ${h.secure_form(h.route_path('edit_repo_advanced_journal', repo_name=c.rhodecode_db_repo.repo_name), request=request)}
53 53 <div class="field">
54 54 %if c.in_public_journal:
55 55 <button class="btn btn-small" type="submit">
56 56 ${_('Remove from Public Journal')}
57 57 </button>
58 58 %else:
59 59 <button class="btn btn-small" type="submit">
60 60 ${_('Add to Public Journal')}
61 61 </button>
62 62 %endif
63 63 </div>
64 64 <div class="field" >
65 65 <span class="help-block">${_('All actions made on this repository will be visible to everyone following the public journal.')}</span>
66 66 </div>
67 67 ${h.end_form()}
68 68 </div>
69 69 </div>
70 70
71 71
72 72 <div class="panel panel-default">
73 73 <div class="panel-heading" id="advanced-locking">
74 74 <h3 class="panel-title">${_('Locking state')} <a class="permalink" href="#advanced-locking"> ΒΆ</a></h3>
75 75 </div>
76 76 <div class="panel-body">
77 77 ${h.secure_form(h.route_path('edit_repo_advanced_locking', repo_name=c.rhodecode_db_repo.repo_name), request=request)}
78 78
79 79 %if c.rhodecode_db_repo.locked[0]:
80 80 <div class="panel-body-title-text">${'Locked by %s on %s. Lock reason: %s' % (h.person_by_id(c.rhodecode_db_repo.locked[0]),
81 81 h.format_date(h. time_to_datetime(c.rhodecode_db_repo.locked[1])), c.rhodecode_db_repo.locked[2])}</div>
82 82 %else:
83 83 <div class="panel-body-title-text">${_('This Repository is not currently locked.')}</div>
84 84 %endif
85 85
86 86 <div class="field" >
87 87 %if c.rhodecode_db_repo.locked[0]:
88 88 ${h.hidden('set_unlock', '1')}
89 89 <button class="btn btn-small" type="submit"
90 90 onclick="return confirm('${_('Confirm to unlock repository.')}');">
91 91 <i class="icon-unlock"></i>
92 92 ${_('Unlock repository')}
93 93 </button>
94 94 %else:
95 95 ${h.hidden('set_lock', '1')}
96 96 <button class="btn btn-small" type="submit"
97 97 onclick="return confirm('${_('Confirm to lock repository.')}');">
98 98 <i class="icon-lock"></i>
99 99 ${_('Lock Repository')}
100 100 </button>
101 101 %endif
102 102 </div>
103 103 <div class="field" >
104 104 <span class="help-block">
105 105 ${_('Force repository locking. This only works when anonymous access is disabled. Pulling from the repository locks the repository to that user until the same user pushes to that repository again.')}
106 106 </span>
107 107 </div>
108 108 ${h.end_form()}
109 109 </div>
110 110 </div>
111 111
112 112 <div class="panel panel-danger">
113 113 <div class="panel-heading" id="advanced-delete">
114 114 <h3 class="panel-title">${_('Delete repository')} <a class="permalink" href="#advanced-delete"> ΒΆ</a></h3>
115 115 </div>
116 116 <div class="panel-body">
117 117 ${h.secure_form(h.route_path('edit_repo_advanced_delete', repo_name=c.repo_name), request=request)}
118 118 <table class="display">
119 119 <tr>
120 120 <td>
121 121 ${_ungettext('This repository has %s fork.', 'This repository has %s forks.', c.rhodecode_db_repo.forks.count()) % c.rhodecode_db_repo.forks.count()}
122 122 </td>
123 123 <td>
124 124 %if c.rhodecode_db_repo.forks.count():
125 125 <input type="radio" name="forks" value="detach_forks" checked="checked"/> <label for="forks">${_('Detach forks')}</label>
126 126 %endif
127 127 </td>
128 128 <td>
129 129 %if c.rhodecode_db_repo.forks.count():
130 130 <input type="radio" name="forks" value="delete_forks"/> <label for="forks">${_('Delete forks')}</label>
131 131 %endif
132 132 </td>
133 133 </tr>
134 134 </table>
135 135 <div style="margin: 0 0 20px 0" class="fake-space"></div>
136 136
137 137 <div class="field">
138 138 <button class="btn btn-small btn-danger" type="submit"
139 139 onclick="return confirm('${_('Confirm to delete this repository: %s') % c.repo_name}');">
140 140 <i class="icon-remove-sign"></i>
141 141 ${_('Delete This Repository')}
142 142 </button>
143 143 </div>
144 144 <div class="field">
145 145 <span class="help-block">
146 146 ${_('This repository will be renamed in a special way in order to make it inaccessible to RhodeCode Enterprise and its VCS systems. If you need to fully delete it from the file system, please do it manually, or with rhodecode-cleanup-repos command available in rhodecode-tools.')}
147 147 </span>
148 148 </div>
149 149
150 150 ${h.end_form()}
151 151 </div>
152 152 </div>
153 153
154 154
155 155 <script>
156 156
157 157 var currentRepoId = ${c.rhodecode_db_repo.repo_id};
158 158
159 159 var repoTypeFilter = function(data) {
160 160 var results = [];
161 161
162 162 if (!data.results[0]) {
163 163 return data
164 164 }
165 165
166 166 $.each(data.results[0].children, function() {
167 167 // filter out the SAME repo, it cannot be used as fork of itself
168 if (this.obj.repo_id != currentRepoId) {
169 this.id = this.obj.repo_id;
168 if (this.repo_id != currentRepoId) {
169 this.id = this.repo_id;
170 170 results.push(this)
171 171 }
172 172 });
173 173 data.results[0].children = results;
174 174 return data;
175 175 };
176 176
177 177 $("#id_fork_of").select2({
178 178 cachedDataSource: {},
179 179 minimumInputLength: 2,
180 180 placeholder: "${_('Change repository') if c.rhodecode_db_repo.fork else _('Pick repository')}",
181 181 dropdownAutoWidth: true,
182 182 containerCssClass: "drop-menu",
183 183 dropdownCssClass: "drop-menu-dropdown",
184 formatResult: formatResult,
184 formatResult: formatRepoResult,
185 185 query: $.debounce(250, function(query){
186 186 self = this;
187 187 var cacheKey = query.term;
188 188 var cachedData = self.cachedDataSource[cacheKey];
189 189
190 190 if (cachedData) {
191 191 query.callback({results: cachedData.results});
192 192 } else {
193 193 $.ajax({
194 194 url: pyroutes.url('repo_list_data'),
195 195 data: {'query': query.term, repo_type: '${c.rhodecode_db_repo.repo_type}'},
196 196 dataType: 'json',
197 197 type: 'GET',
198 198 success: function(data) {
199 199 data = repoTypeFilter(data);
200 200 self.cachedDataSource[cacheKey] = data;
201 201 query.callback({results: data.results});
202 202 },
203 203 error: function(data, textStatus, errorThrown) {
204 204 alert("Error while fetching entries.\nError code {0} ({1}).".format(data.status, data.statusText));
205 205 }
206 206 })
207 207 }
208 208 })
209 209 });
210 210 </script>
211 211
@@ -1,186 +1,186 b''
1 1 <div class="panel panel-default">
2 2 <div class="panel-heading">
3 3 <h3 class="panel-title">${_('Authentication Tokens')}</h3>
4 4 </div>
5 5 <div class="panel-body">
6 6 <div class="apikeys_wrap">
7 7 <p>
8 8 ${_('Each token can have a role. Token with a role can be used only in given context, '
9 9 'e.g. VCS tokens can be used together with the authtoken auth plugin for git/hg/svn operations only.')}
10 10 </p>
11 11 <table class="rctable auth_tokens">
12 12 <tr>
13 13 <th>${_('Token')}</th>
14 14 <th>${_('Scope')}</th>
15 15 <th>${_('Description')}</th>
16 16 <th>${_('Role')}</th>
17 17 <th>${_('Expiration')}</th>
18 18 <th>${_('Action')}</th>
19 19 </tr>
20 20 %if c.user_auth_tokens:
21 21 %for auth_token in c.user_auth_tokens:
22 22 <tr class="${'expired' if auth_token.expired else ''}">
23 23 <td class="truncate-wrap td-authtoken"><div class="user_auth_tokens truncate autoexpand"><code>${auth_token.api_key}</code></div></td>
24 24 <td class="td">${auth_token.scope_humanized}</td>
25 25 <td class="td-wrap">${auth_token.description}</td>
26 26 <td class="td-tags">
27 27 <span class="tag disabled">${auth_token.role_humanized}</span>
28 28 </td>
29 29 <td class="td-exp">
30 30 %if auth_token.expires == -1:
31 31 ${_('never')}
32 32 %else:
33 33 %if auth_token.expired:
34 34 <span style="text-decoration: line-through">${h.age_component(h.time_to_utcdatetime(auth_token.expires))}</span>
35 35 %else:
36 36 ${h.age_component(h.time_to_utcdatetime(auth_token.expires))}
37 37 %endif
38 38 %endif
39 39 </td>
40 40 <td class="td-action">
41 41 ${h.secure_form(h.route_path('edit_user_auth_tokens_delete', user_id=c.user.user_id), request=request)}
42 42 ${h.hidden('del_auth_token', auth_token.user_api_key_id)}
43 43 <button class="btn btn-link btn-danger" type="submit"
44 44 onclick="return confirm('${_('Confirm to remove this auth token: %s') % auth_token.token_obfuscated}');">
45 45 ${_('Delete')}
46 46 </button>
47 47 ${h.end_form()}
48 48 </td>
49 49 </tr>
50 50 %endfor
51 51 %else:
52 52 <tr><td><div class="ip">${_('No additional auth tokens specified')}</div></td></tr>
53 53 %endif
54 54 </table>
55 55 </div>
56 56
57 57 <div class="user_auth_tokens">
58 58 ${h.secure_form(h.route_path('edit_user_auth_tokens_add', user_id=c.user.user_id), request=request)}
59 59 <div class="form form-vertical">
60 60 <!-- fields -->
61 61 <div class="fields">
62 62 <div class="field">
63 63 <div class="label">
64 64 <label for="new_email">${_('New authentication token')}:</label>
65 65 </div>
66 66 <div class="input">
67 67 ${h.text('description', class_='medium', placeholder=_('Description'))}
68 68 ${h.hidden('lifetime')}
69 69 ${h.select('role', '', c.role_options)}
70 70
71 71 % if c.allow_scoped_tokens:
72 72 ${h.hidden('scope_repo_id')}
73 73 % else:
74 74 ${h.select('scope_repo_id_disabled', '', ['Scopes available in EE edition'], disabled='disabled')}
75 75 % endif
76 76 </div>
77 77 <p class="help-block">
78 78 ${_('Repository scope works only with tokens with VCS type.')}
79 79 </p>
80 80 </div>
81 81 <div class="buttons">
82 82 ${h.submit('save',_('Add'),class_="btn")}
83 83 ${h.reset('reset',_('Reset'),class_="btn")}
84 84 </div>
85 85 </div>
86 86 </div>
87 87 ${h.end_form()}
88 88 </div>
89 89 </div>
90 90 </div>
91 91
92 92 <script>
93 93
94 94 $(document).ready(function(){
95 95
96 96 var select2Options = {
97 97 'containerCssClass': "drop-menu",
98 98 'dropdownCssClass': "drop-menu-dropdown",
99 99 'dropdownAutoWidth': true
100 100 };
101 101 $("#role").select2(select2Options);
102 102
103 103 var preloadData = {
104 104 results: [
105 105 % for entry in c.lifetime_values:
106 106 {id:${entry[0]}, text:"${entry[1]}"}${'' if loop.last else ','}
107 107 % endfor
108 108 ]
109 109 };
110 110
111 111 $("#lifetime").select2({
112 112 containerCssClass: "drop-menu",
113 113 dropdownCssClass: "drop-menu-dropdown",
114 114 dropdownAutoWidth: true,
115 115 data: preloadData,
116 116 placeholder: "${_('Select or enter expiration date')}",
117 117 query: function(query) {
118 118 feedLifetimeOptions(query, preloadData);
119 119 }
120 120 });
121 121
122 122
123 123 var repoFilter = function(data) {
124 124 var results = [];
125 125
126 126 if (!data.results[0]) {
127 127 return data
128 128 }
129 129
130 130 $.each(data.results[0].children, function() {
131 131 // replace name to ID for submision
132 this.id = this.obj.repo_id;
132 this.id = this.repo_id;
133 133 results.push(this);
134 134 });
135 135
136 136 data.results[0].children = results;
137 137 return data;
138 138 };
139 139
140 140 $("#scope_repo_id_disabled").select2(select2Options);
141 141
142 142 var selectVcsScope = function() {
143 143 // select vcs scope and disable input
144 144 $("#role").select2("val", "${c.role_vcs}").trigger('change');
145 145 $("#role").select2("readonly", true)
146 146 };
147 147
148 148 $("#scope_repo_id").select2({
149 149 cachedDataSource: {},
150 150 minimumInputLength: 2,
151 151 placeholder: "${_('repository scope')}",
152 152 dropdownAutoWidth: true,
153 153 containerCssClass: "drop-menu",
154 154 dropdownCssClass: "drop-menu-dropdown",
155 formatResult: formatResult,
155 formatResult: formatRepoResult,
156 156 query: $.debounce(250, function(query){
157 157 self = this;
158 158 var cacheKey = query.term;
159 159 var cachedData = self.cachedDataSource[cacheKey];
160 160
161 161 if (cachedData) {
162 162 query.callback({results: cachedData.results});
163 163 } else {
164 164 $.ajax({
165 165 url: pyroutes.url('repo_list_data'),
166 166 data: {'query': query.term},
167 167 dataType: 'json',
168 168 type: 'GET',
169 169 success: function(data) {
170 170 data = repoFilter(data);
171 171 self.cachedDataSource[cacheKey] = data;
172 172 query.callback({results: data.results});
173 173 },
174 174 error: function(data, textStatus, errorThrown) {
175 175 alert("Error while fetching entries.\nError code {0} ({1}).".format(data.status, data.statusText));
176 176 }
177 177 })
178 178 }
179 179 })
180 180 });
181 181 $("#scope_repo_id").on('select2-selecting', function(e){
182 182 selectVcsScope()
183 183 });
184 184
185 185 });
186 186 </script>
@@ -1,613 +1,662 b''
1 1 ## -*- coding: utf-8 -*-
2 2 <%inherit file="root.mako"/>
3 3
4 4 <%include file="/ejs_templates/templates.html"/>
5 5
6 6 <div class="outerwrapper">
7 7 <!-- HEADER -->
8 8 <div class="header">
9 9 <div id="header-inner" class="wrapper">
10 10 <div id="logo">
11 11 <div class="logo-wrapper">
12 12 <a href="${h.route_path('home')}"><img src="${h.asset('images/rhodecode-logo-white-216x60.png')}" alt="RhodeCode"/></a>
13 13 </div>
14 14 %if c.rhodecode_name:
15 15 <div class="branding">- ${h.branding(c.rhodecode_name)}</div>
16 16 %endif
17 17 </div>
18 18 <!-- MENU BAR NAV -->
19 19 ${self.menu_bar_nav()}
20 20 <!-- END MENU BAR NAV -->
21 21 </div>
22 22 </div>
23 23 ${self.menu_bar_subnav()}
24 24 <!-- END HEADER -->
25 25
26 26 <!-- CONTENT -->
27 27 <div id="content" class="wrapper">
28 28
29 29 <rhodecode-toast id="notifications"></rhodecode-toast>
30 30
31 31 <div class="main">
32 32 ${next.main()}
33 33 </div>
34 34 </div>
35 35 <!-- END CONTENT -->
36 36
37 37 </div>
38 38 <!-- FOOTER -->
39 39 <div id="footer">
40 40 <div id="footer-inner" class="title wrapper">
41 41 <div>
42 42 <p class="footer-link-right">
43 43 % if c.visual.show_version:
44 44 RhodeCode Enterprise ${c.rhodecode_version} ${c.rhodecode_edition}
45 45 % endif
46 46 &copy; 2010-${h.datetime.today().year}, <a href="${h.route_url('rhodecode_official')}" target="_blank">RhodeCode GmbH</a>. All rights reserved.
47 47 % if c.visual.rhodecode_support_url:
48 48 <a href="${c.visual.rhodecode_support_url}" target="_blank">${_('Support')}</a>
49 49 % endif
50 50 </p>
51 51 <% sid = 'block' if request.GET.get('showrcid') else 'none' %>
52 52 <p class="server-instance" style="display:${sid}">
53 53 ## display hidden instance ID if specially defined
54 54 % if c.rhodecode_instanceid:
55 55 ${_('RhodeCode instance id: %s') % c.rhodecode_instanceid}
56 56 % endif
57 57 </p>
58 58 </div>
59 59 </div>
60 60 </div>
61 61
62 62 <!-- END FOOTER -->
63 63
64 64 ### MAKO DEFS ###
65 65
66 66 <%def name="menu_bar_subnav()">
67 67 </%def>
68 68
69 69 <%def name="breadcrumbs(class_='breadcrumbs')">
70 70 <div class="${class_}">
71 71 ${self.breadcrumbs_links()}
72 72 </div>
73 73 </%def>
74 74
75 75 <%def name="admin_menu()">
76 76 <ul class="admin_menu submenu">
77 77 <li><a href="${h.route_path('admin_audit_logs')}">${_('Admin audit logs')}</a></li>
78 78 <li><a href="${h.route_path('repos')}">${_('Repositories')}</a></li>
79 79 <li><a href="${h.route_path('repo_groups')}">${_('Repository groups')}</a></li>
80 80 <li><a href="${h.route_path('users')}">${_('Users')}</a></li>
81 81 <li><a href="${h.route_path('user_groups')}">${_('User groups')}</a></li>
82 82 <li><a href="${h.route_path('admin_permissions_application')}">${_('Permissions')}</a></li>
83 83 <li><a href="${h.route_path('auth_home', traverse='')}">${_('Authentication')}</a></li>
84 84 <li><a href="${h.route_path('global_integrations_home')}">${_('Integrations')}</a></li>
85 85 <li><a href="${h.route_path('admin_defaults_repositories')}">${_('Defaults')}</a></li>
86 86 <li class="last"><a href="${h.route_path('admin_settings')}">${_('Settings')}</a></li>
87 87 </ul>
88 88 </%def>
89 89
90 90
91 91 <%def name="dt_info_panel(elements)">
92 92 <dl class="dl-horizontal">
93 93 %for dt, dd, title, show_items in elements:
94 94 <dt>${dt}:</dt>
95 95 <dd title="${h.tooltip(title)}">
96 96 %if callable(dd):
97 97 ## allow lazy evaluation of elements
98 98 ${dd()}
99 99 %else:
100 100 ${dd}
101 101 %endif
102 102 %if show_items:
103 103 <span class="btn-collapse" data-toggle="item-${h.md5_safe(dt)[:6]}-details">${_('Show More')} </span>
104 104 %endif
105 105 </dd>
106 106
107 107 %if show_items:
108 108 <div class="collapsable-content" data-toggle="item-${h.md5_safe(dt)[:6]}-details" style="display: none">
109 109 %for item in show_items:
110 110 <dt></dt>
111 111 <dd>${item}</dd>
112 112 %endfor
113 113 </div>
114 114 %endif
115 115
116 116 %endfor
117 117 </dl>
118 118 </%def>
119 119
120 120
121 121 <%def name="gravatar(email, size=16)">
122 122 <%
123 123 if (size > 16):
124 124 gravatar_class = 'gravatar gravatar-large'
125 125 else:
126 126 gravatar_class = 'gravatar'
127 127 %>
128 128 <%doc>
129 129 TODO: johbo: For now we serve double size images to make it smooth
130 130 for retina. This is how it worked until now. Should be replaced
131 131 with a better solution at some point.
132 132 </%doc>
133 133 <img class="${gravatar_class}" src="${h.gravatar_url(email, size * 2)}" height="${size}" width="${size}">
134 134 </%def>
135 135
136 136
137 137 <%def name="gravatar_with_user(contact, size=16, show_disabled=False)">
138 138 <% email = h.email_or_none(contact) %>
139 139 <div class="rc-user tooltip" title="${h.tooltip(h.author_string(email))}">
140 140 ${self.gravatar(email, size)}
141 141 <span class="${'user user-disabled' if show_disabled else 'user'}"> ${h.link_to_user(contact)}</span>
142 142 </div>
143 143 </%def>
144 144
145 145
146 146 ## admin menu used for people that have some admin resources
147 147 <%def name="admin_menu_simple(repositories=None, repository_groups=None, user_groups=None)">
148 148 <ul class="submenu">
149 149 %if repositories:
150 150 <li class="local-admin-repos"><a href="${h.route_path('repos')}">${_('Repositories')}</a></li>
151 151 %endif
152 152 %if repository_groups:
153 153 <li class="local-admin-repo-groups"><a href="${h.route_path('repo_groups')}">${_('Repository groups')}</a></li>
154 154 %endif
155 155 %if user_groups:
156 156 <li class="local-admin-user-groups"><a href="${h.route_path('user_groups')}">${_('User groups')}</a></li>
157 157 %endif
158 158 </ul>
159 159 </%def>
160 160
161 161 <%def name="repo_page_title(repo_instance)">
162 162 <div class="title-content">
163 163 <div class="title-main">
164 164 ## SVN/HG/GIT icons
165 165 %if h.is_hg(repo_instance):
166 166 <i class="icon-hg"></i>
167 167 %endif
168 168 %if h.is_git(repo_instance):
169 169 <i class="icon-git"></i>
170 170 %endif
171 171 %if h.is_svn(repo_instance):
172 172 <i class="icon-svn"></i>
173 173 %endif
174 174
175 175 ## public/private
176 176 %if repo_instance.private:
177 177 <i class="icon-repo-private"></i>
178 178 %else:
179 179 <i class="icon-repo-public"></i>
180 180 %endif
181 181
182 182 ## repo name with group name
183 183 ${h.breadcrumb_repo_link(c.rhodecode_db_repo)}
184 184
185 185 </div>
186 186
187 187 ## FORKED
188 188 %if repo_instance.fork:
189 189 <p>
190 190 <i class="icon-code-fork"></i> ${_('Fork of')}
191 191 <a href="${h.route_path('repo_summary',repo_name=repo_instance.fork.repo_name)}">${repo_instance.fork.repo_name}</a>
192 192 </p>
193 193 %endif
194 194
195 195 ## IMPORTED FROM REMOTE
196 196 %if repo_instance.clone_uri:
197 197 <p>
198 198 <i class="icon-code-fork"></i> ${_('Clone from')}
199 199 <a href="${h.safe_str(h.hide_credentials(repo_instance.clone_uri))}">${h.hide_credentials(repo_instance.clone_uri)}</a>
200 200 </p>
201 201 %endif
202 202
203 203 ## LOCKING STATUS
204 204 %if repo_instance.locked[0]:
205 205 <p class="locking_locked">
206 206 <i class="icon-repo-lock"></i>
207 207 ${_('Repository locked by %(user)s') % {'user': h.person_by_id(repo_instance.locked[0])}}
208 208 </p>
209 209 %elif repo_instance.enable_locking:
210 210 <p class="locking_unlocked">
211 211 <i class="icon-repo-unlock"></i>
212 212 ${_('Repository not locked. Pull repository to lock it.')}
213 213 </p>
214 214 %endif
215 215
216 216 </div>
217 217 </%def>
218 218
219 219 <%def name="repo_menu(active=None)">
220 220 <%
221 221 def is_active(selected):
222 222 if selected == active:
223 223 return "active"
224 224 %>
225 225
226 226 <!--- CONTEXT BAR -->
227 227 <div id="context-bar">
228 228 <div class="wrapper">
229 <ul id="context-pages" class="horizontal-list navigation">
229 <ul id="context-pages" class="navigation horizontal-list">
230 230 <li class="${is_active('summary')}"><a class="menulink" href="${h.route_path('repo_summary', repo_name=c.repo_name)}"><div class="menulabel">${_('Summary')}</div></a></li>
231 231 <li class="${is_active('changelog')}"><a class="menulink" href="${h.route_path('repo_changelog', repo_name=c.repo_name)}"><div class="menulabel">${_('Changelog')}</div></a></li>
232 232 <li class="${is_active('files')}"><a class="menulink" href="${h.route_path('repo_files', repo_name=c.repo_name, commit_id=c.rhodecode_db_repo.landing_rev[1], f_path='')}"><div class="menulabel">${_('Files')}</div></a></li>
233 233 <li class="${is_active('compare')}"><a class="menulink" href="${h.route_path('repo_compare_select',repo_name=c.repo_name)}"><div class="menulabel">${_('Compare')}</div></a></li>
234 234 ## TODO: anderson: ideally it would have a function on the scm_instance "enable_pullrequest() and enable_fork()"
235 235 %if c.rhodecode_db_repo.repo_type in ['git','hg']:
236 236 <li class="${is_active('showpullrequest')}">
237 237 <a class="menulink" href="${h.route_path('pullrequest_show_all', repo_name=c.repo_name)}" title="${h.tooltip(_('Show Pull Requests for %s') % c.repo_name)}">
238 238 %if c.repository_pull_requests:
239 239 <span class="pr_notifications">${c.repository_pull_requests}</span>
240 240 %endif
241 241 <div class="menulabel">${_('Pull Requests')}</div>
242 242 </a>
243 243 </li>
244 244 %endif
245 245 <li class="${is_active('options')}">
246 246 <a class="menulink dropdown">
247 247 <div class="menulabel">${_('Options')} <div class="show_more"></div></div>
248 248 </a>
249 249 <ul class="submenu">
250 250 %if h.HasRepoPermissionAll('repository.admin')(c.repo_name):
251 251 <li><a href="${h.route_path('edit_repo',repo_name=c.repo_name)}">${_('Settings')}</a></li>
252 252 %endif
253 253 %if c.rhodecode_db_repo.fork:
254 254 <li>
255 255 <a title="${h.tooltip(_('Compare fork with %s' % c.rhodecode_db_repo.fork.repo_name))}"
256 256 href="${h.route_path('repo_compare',
257 257 repo_name=c.rhodecode_db_repo.fork.repo_name,
258 258 source_ref_type=c.rhodecode_db_repo.landing_rev[0],
259 259 source_ref=c.rhodecode_db_repo.landing_rev[1],
260 260 target_repo=c.repo_name,target_ref_type='branch' if request.GET.get('branch') else c.rhodecode_db_repo.landing_rev[0],
261 261 target_ref=request.GET.get('branch') or c.rhodecode_db_repo.landing_rev[1],
262 262 _query=dict(merge=1))}"
263 263 >
264 264 ${_('Compare fork')}
265 265 </a>
266 266 </li>
267 267 %endif
268 268
269 269 <li><a href="${h.route_path('search_repo',repo_name=c.repo_name)}">${_('Search')}</a></li>
270 270
271 271 %if h.HasRepoPermissionAny('repository.write','repository.admin')(c.repo_name) and c.rhodecode_db_repo.enable_locking:
272 272 %if c.rhodecode_db_repo.locked[0]:
273 273 <li><a class="locking_del" href="${h.route_path('repo_edit_toggle_locking',repo_name=c.repo_name)}">${_('Unlock')}</a></li>
274 274 %else:
275 275 <li><a class="locking_add" href="${h.route_path('repo_edit_toggle_locking',repo_name=c.repo_name)}">${_('Lock')}</a></li>
276 276 %endif
277 277 %endif
278 278 %if c.rhodecode_user.username != h.DEFAULT_USER:
279 279 %if c.rhodecode_db_repo.repo_type in ['git','hg']:
280 280 <li><a href="${h.route_path('repo_fork_new',repo_name=c.repo_name)}">${_('Fork')}</a></li>
281 281 <li><a href="${h.route_path('pullrequest_new',repo_name=c.repo_name)}">${_('Create Pull Request')}</a></li>
282 282 %endif
283 283 %endif
284 284 </ul>
285 285 </li>
286 286 </ul>
287 287 </div>
288 288 <div class="clear"></div>
289 289 </div>
290 290 <!--- END CONTEXT BAR -->
291 291
292 292 </%def>
293 293
294 294 <%def name="usermenu(active=False)">
295 295 ## USER MENU
296 296 <li id="quick_login_li" class="${'active' if active else ''}">
297 297 <a id="quick_login_link" class="menulink childs">
298 298 ${gravatar(c.rhodecode_user.email, 20)}
299 299 <span class="user">
300 300 %if c.rhodecode_user.username != h.DEFAULT_USER:
301 301 <span class="menu_link_user">${c.rhodecode_user.username}</span><div class="show_more"></div>
302 302 %else:
303 303 <span>${_('Sign in')}</span>
304 304 %endif
305 305 </span>
306 306 </a>
307 307
308 308 <div class="user-menu submenu">
309 309 <div id="quick_login">
310 310 %if c.rhodecode_user.username == h.DEFAULT_USER:
311 311 <h4>${_('Sign in to your account')}</h4>
312 312 ${h.form(h.route_path('login', _query={'came_from': h.current_route_path(request)}), needs_csrf_token=False)}
313 313 <div class="form form-vertical">
314 314 <div class="fields">
315 315 <div class="field">
316 316 <div class="label">
317 317 <label for="username">${_('Username')}:</label>
318 318 </div>
319 319 <div class="input">
320 320 ${h.text('username',class_='focus',tabindex=1)}
321 321 </div>
322 322
323 323 </div>
324 324 <div class="field">
325 325 <div class="label">
326 326 <label for="password">${_('Password')}:</label>
327 327 %if h.HasPermissionAny('hg.password_reset.enabled')():
328 328 <span class="forgot_password">${h.link_to(_('(Forgot password?)'),h.route_path('reset_password'), class_='pwd_reset')}</span>
329 329 %endif
330 330 </div>
331 331 <div class="input">
332 332 ${h.password('password',class_='focus',tabindex=2)}
333 333 </div>
334 334 </div>
335 335 <div class="buttons">
336 336 <div class="register">
337 337 %if h.HasPermissionAny('hg.admin', 'hg.register.auto_activate', 'hg.register.manual_activate')():
338 338 ${h.link_to(_("Don't have an account?"),h.route_path('register'))} <br/>
339 339 %endif
340 340 ${h.link_to(_("Using external auth? Sign In here."),h.route_path('login'))}
341 341 </div>
342 342 <div class="submit">
343 343 ${h.submit('sign_in',_('Sign In'),class_="btn btn-small",tabindex=3)}
344 344 </div>
345 345 </div>
346 346 </div>
347 347 </div>
348 348 ${h.end_form()}
349 349 %else:
350 350 <div class="">
351 351 <div class="big_gravatar">${gravatar(c.rhodecode_user.email, 48)}</div>
352 352 <div class="full_name">${c.rhodecode_user.full_name_or_username}</div>
353 353 <div class="email">${c.rhodecode_user.email}</div>
354 354 </div>
355 355 <div class="">
356 356 <ol class="links">
357 357 <li>${h.link_to(_(u'My account'),h.route_path('my_account_profile'))}</li>
358 358 % if c.rhodecode_user.personal_repo_group:
359 359 <li>${h.link_to(_(u'My personal group'), h.route_path('repo_group_home', repo_group_name=c.rhodecode_user.personal_repo_group.group_name))}</li>
360 360 % endif
361 361 <li>${h.link_to(_(u'Pull Requests'), h.route_path('my_account_pullrequests'))}</li>
362 362
363 363 <li class="logout">
364 364 ${h.secure_form(h.route_path('logout'), request=request)}
365 365 ${h.submit('log_out', _(u'Sign Out'),class_="btn btn-primary")}
366 366 ${h.end_form()}
367 367 </li>
368 368 </ol>
369 369 </div>
370 370 %endif
371 371 </div>
372 372 </div>
373 373 %if c.rhodecode_user.username != h.DEFAULT_USER:
374 374 <div class="pill_container">
375 375 <a class="menu_link_notifications ${'empty' if c.unread_notifications == 0 else ''}" href="${h.route_path('notifications_show_all')}">${c.unread_notifications}</a>
376 376 </div>
377 377 % endif
378 378 </li>
379 379 </%def>
380 380
381 381 <%def name="menu_items(active=None)">
382 382 <%
383 383 def is_active(selected):
384 384 if selected == active:
385 385 return "active"
386 386 return ""
387 387 %>
388 388 <ul id="quick" class="main_nav navigation horizontal-list">
389 <!-- repo switcher -->
390 <li class="${is_active('repositories')} repo_switcher_li has_select2">
391 <input id="repo_switcher" name="repo_switcher" type="hidden">
389
390 ## Main filter
391 <li>
392 <div class="menulabel main_filter_box">
393 <div class="main_filter_input_box">
394 <input class="main_filter_input" id="main_filter" size="15" type="text" name="main_filter" placeholder="${_('search / go to...')}" value=""/>
395 </div>
396 <div class="main_filter_help_box">
397 <a href="#showFilterHelp" onclick="showMainFilterBox(); return false">?</a>
398 </div>
399 </div>
400
401 <div id="main_filter_help" style="display: none">
402 Use '/' key to quickly access this field.
403 Enter name of repository, or repository group for quick search.
404
405 Prefix query to allow special search:
406
407 For usernames, e.g user:admin
408
409 For commit hash/id, e.g commit:efced4
410
411 </div>
392 412 </li>
393 413
394 414 ## ROOT MENU
395 415 %if c.rhodecode_user.username != h.DEFAULT_USER:
396 416 <li class="${is_active('journal')}">
397 417 <a class="menulink" title="${_('Show activity journal')}" href="${h.route_path('journal')}">
398 418 <div class="menulabel">${_('Journal')}</div>
399 419 </a>
400 420 </li>
401 421 %else:
402 422 <li class="${is_active('journal')}">
403 423 <a class="menulink" title="${_('Show Public activity journal')}" href="${h.route_path('journal_public')}">
404 424 <div class="menulabel">${_('Public journal')}</div>
405 425 </a>
406 426 </li>
407 427 %endif
408 428 <li class="${is_active('gists')}">
409 429 <a class="menulink childs" title="${_('Show Gists')}" href="${h.route_path('gists_show')}">
410 430 <div class="menulabel">${_('Gists')}</div>
411 431 </a>
412 432 </li>
413 433 <li class="${is_active('search')}">
414 434 <a class="menulink" title="${_('Search in repositories you have access to')}" href="${h.route_path('search')}">
415 435 <div class="menulabel">${_('Search')}</div>
416 436 </a>
417 437 </li>
418 438 % if h.HasPermissionAll('hg.admin')('access admin main page'):
419 439 <li class="${is_active('admin')}">
420 440 <a class="menulink childs" title="${_('Admin settings')}" href="#" onclick="return false;">
421 441 <div class="menulabel">${_('Admin')} <div class="show_more"></div></div>
422 442 </a>
423 443 ${admin_menu()}
424 444 </li>
425 445 % elif c.rhodecode_user.repositories_admin or c.rhodecode_user.repository_groups_admin or c.rhodecode_user.user_groups_admin:
426 446 <li class="${is_active('admin')}">
427 447 <a class="menulink childs" title="${_('Delegated Admin settings')}">
428 448 <div class="menulabel">${_('Admin')} <div class="show_more"></div></div>
429 449 </a>
430 450 ${admin_menu_simple(c.rhodecode_user.repositories_admin,
431 451 c.rhodecode_user.repository_groups_admin,
432 452 c.rhodecode_user.user_groups_admin or h.HasPermissionAny('hg.usergroup.create.true')())}
433 453 </li>
434 454 % endif
455 ## render extra user menu
456 ${usermenu(active=(active=='my_account'))}
457
435 458 % if c.debug_style:
436 459 <li class="${is_active('debug_style')}">
437 460 <a class="menulink" title="${_('Style')}" href="${h.route_path('debug_style_home')}">
438 461 <div class="menulabel">${_('Style')}</div>
439 462 </a>
440 463 </li>
441 464 % endif
442 ## render extra user menu
443 ${usermenu(active=(active=='my_account'))}
444 465 </ul>
445 466
446 467 <script type="text/javascript">
447 var visual_show_public_icon = "${c.visual.show_public_icon}" == "True";
468 var visualShowPublicIcon = "${c.visual.show_public_icon}" == "True";
448 469
449 /*format the look of items in the list*/
450 var format = function(state, escapeMarkup){
451 if (!state.id){
452 return state.text; // optgroup
470 var formatRepoResult = function(result, container, query, escapeMarkup) {
471 return function(data, escapeMarkup) {
472 if (!data.repo_id){
473 return data.text; // optgroup text Repositories
453 474 }
454 var obj_dict = state.obj;
455 var tmpl = '';
456 475
457 if(obj_dict && state.type == 'repo'){
458 if(obj_dict['repo_type'] === 'hg'){
476 var tmpl = '';
477 var repoType = data['repo_type'];
478 var repoName = data['text'];
479
480 if(data && data.type == 'repo'){
481 if(repoType === 'hg'){
459 482 tmpl += '<i class="icon-hg"></i> ';
460 483 }
461 else if(obj_dict['repo_type'] === 'git'){
484 else if(repoType === 'git'){
462 485 tmpl += '<i class="icon-git"></i> ';
463 486 }
464 else if(obj_dict['repo_type'] === 'svn'){
487 else if(repoType === 'svn'){
465 488 tmpl += '<i class="icon-svn"></i> ';
466 489 }
467 if(obj_dict['private']){
490 if(data['private']){
468 491 tmpl += '<i class="icon-lock" ></i> ';
469 492 }
470 else if(visual_show_public_icon){
493 else if(visualShowPublicIcon){
471 494 tmpl += '<i class="icon-unlock-alt"></i> ';
472 495 }
473 496 }
474 if(obj_dict && state.type == 'commit') {
475 tmpl += '<i class="icon-tag"></i>';
476 }
477 if(obj_dict && state.type == 'group'){
478 tmpl += '<i class="icon-folder-close"></i> ';
479 }
480 tmpl += escapeMarkup(state.text);
497 tmpl += escapeMarkup(repoName);
481 498 return tmpl;
482 };
483 499
484 var formatResult = function(result, container, query, escapeMarkup) {
485 return format(result, escapeMarkup);
486 };
487
488 var formatSelection = function(data, container, escapeMarkup) {
489 return format(data, escapeMarkup);
500 }(result, escapeMarkup);
490 501 };
491 502
492 $("#repo_switcher").select2({
493 cachedDataSource: {},
494 minimumInputLength: 2,
495 placeholder: '<div class="menulabel">${_('Go to')} <div class="show_more"></div></div>',
496 dropdownAutoWidth: true,
497 formatResult: formatResult,
498 formatSelection: formatSelection,
499 containerCssClass: "repo-switcher",
500 dropdownCssClass: "repo-switcher-dropdown",
501 escapeMarkup: function(m){
502 // don't escape our custom placeholder
503 if(m.substr(0,23) == '<div class="menulabel">'){
504 return m;
503
504 var autocompleteMainFilterFormatResult = function (data, value, org_formatter) {
505
506 if (value.split(':').length === 2) {
507 value = value.split(':')[1]
505 508 }
506 509
507 return Select2.util.escapeMarkup(m);
508 },
509 query: $.debounce(250, function(query){
510 self = this;
511 var cacheKey = query.term;
512 var cachedData = self.cachedDataSource[cacheKey];
510 var searchType = data['type'];
511 var valueDisplay = data['value_display'];
512
513 var escapeRegExChars = function (value) {
514 return value.replace(/[\-\[\]\/\{\}\(\)\*\+\?\.\\\^\$\|]/g, "\\$&");
515 };
516 var pattern = '(' + escapeRegExChars(value) + ')';
517
518 // highlight match
519 valueDisplay = Select2.util.escapeMarkup(valueDisplay);
520 valueDisplay = valueDisplay.replace(new RegExp(pattern, 'gi'), '<strong>$1<\/strong>');
521
522 var icon = '';
513 523
514 if (cachedData) {
515 query.callback({results: cachedData.results});
516 } else {
517 $.ajax({
518 url: pyroutes.url('goto_switcher_data'),
519 data: {'query': query.term},
520 dataType: 'json',
521 type: 'GET',
522 success: function(data) {
523 self.cachedDataSource[cacheKey] = data;
524 query.callback({results: data.results});
525 },
526 error: function(data, textStatus, errorThrown) {
527 alert("Error while fetching entries.\nError code {0} ({1}).".format(data.status, data.statusText));
524 if (searchType === 'search') {
525 icon += '<i class="icon-more"></i> ';
526 }
527 else if (searchType === 'repo') {
528 if (data['repo_type'] === 'hg') {
529 icon += '<i class="icon-hg"></i> ';
530 }
531 else if (data['repo_type'] === 'git') {
532 icon += '<i class="icon-git"></i> ';
533 }
534 else if (data['repo_type'] === 'svn') {
535 icon += '<i class="icon-svn"></i> ';
536 }
537 if (data['private']) {
538 icon += '<i class="icon-lock" ></i> ';
539 }
540 else if (visualShowPublicIcon) {
541 icon += '<i class="icon-unlock-alt"></i> ';
542 }
543 }
544 else if (searchType === 'repo_group') {
545 icon += '<i class="icon-folder-close"></i> ';
546 }
547 else if (searchType === 'user') {
548 icon += '<img class="gravatar" src="{0}"/>'.format(data['icon_link']);
549 }
550 else if (searchType === 'commit') {
551 icon += '<i class="icon-tag"></i>';
528 552 }
529 })
553
554 var tmpl = '<div class="ac-container-wrap">{0}{1}</div>';
555 return tmpl.format(icon, valueDisplay);
556 };
557
558 var handleSelect = function(element, suggestion) {
559 window.location = suggestion['url'];
560 };
561 var autocompleteMainFilterResult = function (suggestion, originalQuery, queryLowerCase) {
562 if (queryLowerCase.split(':').length === 2) {
563 queryLowerCase = queryLowerCase.split(':')[1]
530 564 }
531 })
565 return suggestion.value_display.toLowerCase().indexOf(queryLowerCase) !== -1;
566 };
567
568 $('#main_filter').autocomplete({
569 serviceUrl: pyroutes.url('goto_switcher_data'),
570 minChars:2,
571 maxHeight:400,
572 deferRequestBy: 300, //miliseconds
573 tabDisabled: true,
574 autoSelectFirst: true,
575 formatResult: autocompleteMainFilterFormatResult,
576 lookupFilter: autocompleteMainFilterResult,
577 onSelect: function(element, suggestion){
578 handleSelect(element, suggestion);
579 return false;
580 }
532 581 });
533 582
534 $("#repo_switcher").on('select2-selecting', function(e){
535 e.preventDefault();
536 window.location = e.choice.url;
537 });
583 showMainFilterBox = function () {
584 $('#main_filter_help').toggle();
585 }
538 586
539 587 </script>
540 588 <script src="${h.asset('js/rhodecode/base/keyboard-bindings.js', ver=c.rhodecode_version_hash)}"></script>
541 589 </%def>
542 590
543 591 <div class="modal" id="help_kb" tabindex="-1" role="dialog" aria-labelledby="myModalLabel" aria-hidden="true">
544 592 <div class="modal-dialog">
545 593 <div class="modal-content">
546 594 <div class="modal-header">
547 595 <button type="button" class="close" data-dismiss="modal" aria-hidden="true">&times;</button>
548 596 <h4 class="modal-title" id="myModalLabel">${_('Keyboard shortcuts')}</h4>
549 597 </div>
550 598 <div class="modal-body">
551 599 <div class="block-left">
552 600 <table class="keyboard-mappings">
553 601 <tbody>
554 602 <tr>
555 603 <th></th>
556 604 <th>${_('Site-wide shortcuts')}</th>
557 605 </tr>
558 606 <%
559 607 elems = [
560 ('/', 'Open quick search box'),
608 ('/', 'Use quick search box'),
561 609 ('g h', 'Goto home page'),
562 610 ('g g', 'Goto my private gists page'),
563 611 ('g G', 'Goto my public gists page'),
564 612 ('n r', 'New repository page'),
565 613 ('n g', 'New gist page'),
566 614 ]
567 615 %>
568 616 %for key, desc in elems:
569 617 <tr>
570 618 <td class="keys">
571 619 <span class="key tag">${key}</span>
572 620 </td>
573 621 <td>${desc}</td>
574 622 </tr>
575 623 %endfor
576 624 </tbody>
577 625 </table>
578 626 </div>
579 627 <div class="block-left">
580 628 <table class="keyboard-mappings">
581 629 <tbody>
582 630 <tr>
583 631 <th></th>
584 632 <th>${_('Repositories')}</th>
585 633 </tr>
586 634 <%
587 635 elems = [
588 636 ('g s', 'Goto summary page'),
589 637 ('g c', 'Goto changelog page'),
590 638 ('g f', 'Goto files page'),
591 639 ('g F', 'Goto files page with file search activated'),
592 640 ('g p', 'Goto pull requests page'),
593 641 ('g o', 'Goto repository settings'),
594 642 ('g O', 'Goto repository permissions settings'),
595 643 ]
596 644 %>
597 645 %for key, desc in elems:
598 646 <tr>
599 647 <td class="keys">
600 648 <span class="key tag">${key}</span>
601 649 </td>
602 650 <td>${desc}</td>
603 651 </tr>
604 652 %endfor
605 653 </tbody>
606 654 </table>
607 655 </div>
608 656 </div>
609 657 <div class="modal-footer">
610 658 </div>
611 659 </div><!-- /.modal-content -->
612 660 </div><!-- /.modal-dialog -->
613 661 </div><!-- /.modal -->
662
@@ -1,201 +1,141 b''
1 1 <%inherit file="/base/base.mako"/>
2 2
3 3 <%def name="main()">
4 4 <div class="box">
5 5 <!-- box / title -->
6 6 <div class="title">
7 7 <div class="block-left breadcrumbs">
8 <input class="q_filter_box" id="q_filter" size="15" type="text" name="filter" placeholder="${_('quick filter...')}" value=""/>
9 8 ${self.breadcrumbs()}
10 <span id="match_container" style="display:none">&raquo; <span id="match_count">0</span> ${_('matches')}</span>
9 <span id="match_container" style="display:none"><span id="match_count">0</span> ${_('matches')}</span>
11 10 </div>
12 11 %if c.rhodecode_user.username != h.DEFAULT_USER:
13 12 <div class="block-right">
14 13 <%
15 14 is_admin = h.HasPermissionAny('hg.admin')('can create repos index page')
16 15 create_repo = h.HasPermissionAny('hg.create.repository')('can create repository index page')
17 16 create_repo_group = h.HasPermissionAny('hg.repogroup.create.true')('can create repository groups index page')
18 17 create_user_group = h.HasPermissionAny('hg.usergroup.create.true')('can create user groups index page')
19 18
20 19 gr_name = c.repo_group.group_name if c.repo_group else None
21 20 # create repositories with write permission on group is set to true
22 21 create_on_write = h.HasPermissionAny('hg.create.write_on_repogroup.true')()
23 22 group_admin = h.HasRepoGroupPermissionAny('group.admin')(gr_name, 'group admin index page')
24 23 group_write = h.HasRepoGroupPermissionAny('group.write')(gr_name, 'can write into group index page')
25 24 %>
26 25
27 26 %if not c.repo_group:
28 27 ## no repository group context here
29 28 %if is_admin or create_repo:
30 29 <a href="${h.route_path('repo_new')}" class="btn btn-small btn-success btn-primary">${_('Add Repository')}</a>
31 30 %endif
32 31
33 32 %if is_admin or create_repo_group:
34 33 <a href="${h.route_path('repo_group_new')}" class="btn btn-small btn-default">${_(u'Add Repository Group')}</a>
35 34 %endif
36 35 %else:
37 36 ##we're inside other repository group other terms apply
38 37 %if is_admin or group_admin or (group_write and create_on_write):
39 38 <a href="${h.route_path('repo_new',_query=dict(parent_group=c.repo_group.group_id))}" class="btn btn-small btn-success btn-primary">${_('Add Repository')}</a>
40 39 %endif
41 40 %if is_admin or group_admin:
42 41 <a href="${h.route_path('repo_group_new',_query=dict(parent_group=c.repo_group.group_id))}" class="btn btn-small btn-default">${_(u'Add Repository Group')}</a>
43 42 %endif
44 43 %if is_admin or group_admin:
45 44 <a href="${h.route_path('edit_repo_group',repo_group_name=c.repo_group.group_name)}" title="${_('You have admin right to this group, and can edit it')}" class="btn btn-small btn-primary">${_('Edit Repository Group')}</a>
46 45 %endif
47 46 %endif
48 47 </div>
49 48 %endif
50 49 </div>
51 50 <!-- end box / title -->
52 51 <div class="table">
53 52 <div id="groups_list_wrap">
54 53 <table id="group_list_table" class="display"></table>
55 54 </div>
56 55 </div>
57 56
58 57 <div class="table">
59 58 <div id="repos_list_wrap">
60 59 <table id="repo_list_table" class="display"></table>
61 60 </div>
62 61 </div>
63 62
64 63 ## no repository groups and repos present, show something to the users
65 64 % if c.repo_groups_data == '[]' and c.repos_data == '[]':
66 65 <div class="table">
67 66 <h2 class="no-object-border">
68 67 ${_('No repositories or repositories groups exists here.')}
69 68 </h2>
70 69 </div>
71 70 % endif
72 71
73 72 </div>
74 73 <script>
75 74 $(document).ready(function() {
76 75
77 76 // repo group list
78 77 % if c.repo_groups_data != '[]':
79 78 $('#group_list_table').DataTable({
80 79 data: ${c.repo_groups_data|n},
81 80 dom: 'rtp',
82 81 pageLength: ${c.visual.dashboard_items},
83 82 order: [[ 0, "asc" ]],
84 83 columns: [
85 84 { data: {"_": "name",
86 85 "sort": "name_raw"}, title: "${_('Name')}", className: "td-componentname" },
87 86 { data: 'menu', "bSortable": false, className: "quick_repo_menu" },
88 87 { data: {"_": "desc",
89 88 "sort": "desc"}, title: "${_('Description')}", className: "td-description" },
90 89 { data: {"_": "last_change",
91 90 "sort": "last_change_raw",
92 91 "type": Number}, title: "${_('Last Change')}", className: "td-time" },
93 92 { data: {"_": "owner",
94 93 "sort": "owner"}, title: "${_('Owner')}", className: "td-user" }
95 94 ],
96 95 language: {
97 96 paginate: DEFAULT_GRID_PAGINATION,
98 97 emptyTable: _gettext("No repository groups available yet.")
99 98 },
100 99 "drawCallback": function( settings, json ) {
101 100 timeagoActivate();
102 101 quick_repo_menu();
103 102 }
104 103 });
105 104 % endif
106 105
107 106 // repo list
108 107 % if c.repos_data != '[]':
109 108 $('#repo_list_table').DataTable({
110 109 data: ${c.repos_data|n},
111 110 dom: 'rtp',
112 111 order: [[ 0, "asc" ]],
113 112 pageLength: ${c.visual.dashboard_items},
114 113 columns: [
115 114 { data: {"_": "name",
116 115 "sort": "name_raw"}, title: "${_('Name')}", className: "truncate-wrap td-componentname" },
117 116 { data: 'menu', "bSortable": false, className: "quick_repo_menu" },
118 117 { data: {"_": "desc",
119 118 "sort": "desc"}, title: "${_('Description')}", className: "td-description" },
120 119 { data: {"_": "last_change",
121 120 "sort": "last_change_raw",
122 121 "type": Number}, title: "${_('Last Change')}", className: "td-time" },
123 122 { data: {"_": "last_changeset",
124 123 "sort": "last_changeset_raw",
125 124 "type": Number}, title: "${_('Commit')}", className: "td-hash" },
126 125 { data: {"_": "owner",
127 126 "sort": "owner"}, title: "${_('Owner')}", className: "td-user" }
128 127 ],
129 128 language: {
130 129 paginate: DEFAULT_GRID_PAGINATION,
131 130 emptyTable: _gettext("No repositories available yet.")
132 131 },
133 132 "drawCallback": function( settings, json ) {
134 133 timeagoActivate();
135 134 quick_repo_menu();
136 135 }
137 136 });
138 137 % endif
139 138
140 var getDatatableCount = function() {
141 var reposCount = 0;
142 var reposCountTotal = 0;
143
144 % if c.repos_data != '[]':
145 var pageInfo = $('#repo_list_table').dataTable().api().page.info();
146 var reposCount = pageInfo.recordsDisplay;
147 var reposCountTotal = pageInfo.recordsTotal;
148 % endif
149
150 var repoGroupsCount = 0;
151 var repoGroupsCountTotal = 0;
152
153 % if c.repo_groups_data != '[]':
154 var pageInfo = $('#group_list_table').dataTable().api().page.info();
155 var repoGroupsCount = pageInfo.recordsDisplay;
156 var repoGroupsCountTotal = pageInfo.recordsTotal;
157 % endif
158
159 if (repoGroupsCount !== repoGroupsCountTotal) {
160 $('#match_count').text(reposCount + repoGroupsCount);
161 }
162 if (reposCount !== reposCountTotal) {
163 $('#match_container').show();
164 }
165 if ($('#q_filter').val() === '') {
166 $('#match_container').hide();
167 }
168 };
169
170 // update the counter when doing search
171 $('#repo_list_table, #group_list_table').on( 'search.dt', function (e,settings) {
172 getDatatableCount();
173 });
174
175 // filter, filter both grids
176 $('#q_filter').on( 'keyup', function () {
177
178 % if c.repo_groups_data != '[]':
179 var repo_group_api = $('#group_list_table').dataTable().api();
180 repo_group_api
181 .columns( 0 )
182 .search( this.value )
183 .draw();
184 % endif
185
186 % if c.repos_data != '[]':
187 var repo_api = $('#repo_list_table').dataTable().api();
188 repo_api
189 .columns( 0 )
190 .search( this.value )
191 .draw();
192 % endif
193
194 });
195
196 // refilter table if page load via back button
197 $("#q_filter").trigger('keyup');
198
199 139 });
200 140 </script>
201 141 </%def>
@@ -1,537 +1,537 b''
1 1 <%inherit file="/base/base.mako"/>
2 2
3 3 <%def name="title()">
4 4 ${c.repo_name} ${_('New pull request')}
5 5 </%def>
6 6
7 7 <%def name="breadcrumbs_links()">
8 8 ${_('New pull request')}
9 9 </%def>
10 10
11 11 <%def name="menu_bar_nav()">
12 12 ${self.menu_items(active='repositories')}
13 13 </%def>
14 14
15 15 <%def name="menu_bar_subnav()">
16 16 ${self.repo_menu(active='showpullrequest')}
17 17 </%def>
18 18
19 19 <%def name="main()">
20 20 <div class="box">
21 21 <div class="title">
22 22 ${self.repo_page_title(c.rhodecode_db_repo)}
23 23 </div>
24 24
25 25 ${h.secure_form(h.route_path('pullrequest_create', repo_name=c.repo_name, _query=request.GET.mixed()), id='pull_request_form', request=request)}
26 26
27 27 ${self.breadcrumbs()}
28 28
29 29 <div class="box pr-summary">
30 30
31 31 <div class="summary-details block-left">
32 32
33 33
34 34 <div class="pr-details-title">
35 35 ${_('Pull request summary')}
36 36 </div>
37 37
38 38 <div class="form" style="padding-top: 10px">
39 39 <!-- fields -->
40 40
41 41 <div class="fields" >
42 42
43 43 <div class="field">
44 44 <div class="label">
45 45 <label for="pullrequest_title">${_('Title')}:</label>
46 46 </div>
47 47 <div class="input">
48 48 ${h.text('pullrequest_title', c.default_title, class_="medium autogenerated-title")}
49 49 </div>
50 50 </div>
51 51
52 52 <div class="field">
53 53 <div class="label label-textarea">
54 54 <label for="pullrequest_desc">${_('Description')}:</label>
55 55 </div>
56 56 <div class="textarea text-area editor">
57 57 ${h.textarea('pullrequest_desc',size=30, )}
58 58 <span class="help-block">${_('Write a short description on this pull request')}</span>
59 59 </div>
60 60 </div>
61 61
62 62 <div class="field">
63 63 <div class="label label-textarea">
64 64 <label for="pullrequest_desc">${_('Commit flow')}:</label>
65 65 </div>
66 66
67 67 ## TODO: johbo: Abusing the "content" class here to get the
68 68 ## desired effect. Should be replaced by a proper solution.
69 69
70 70 ##ORG
71 71 <div class="content">
72 72 <strong>${_('Source repository')}:</strong>
73 73 ${c.rhodecode_db_repo.description}
74 74 </div>
75 75 <div class="content">
76 76 ${h.hidden('source_repo')}
77 77 ${h.hidden('source_ref')}
78 78 </div>
79 79
80 80 ##OTHER, most Probably the PARENT OF THIS FORK
81 81 <div class="content">
82 82 ## filled with JS
83 83 <div id="target_repo_desc"></div>
84 84 </div>
85 85
86 86 <div class="content">
87 87 ${h.hidden('target_repo')}
88 88 ${h.hidden('target_ref')}
89 89 <span id="target_ref_loading" style="display: none">
90 90 ${_('Loading refs...')}
91 91 </span>
92 92 </div>
93 93 </div>
94 94
95 95 <div class="field">
96 96 <div class="label label-textarea">
97 97 <label for="pullrequest_submit"></label>
98 98 </div>
99 99 <div class="input">
100 100 <div class="pr-submit-button">
101 101 ${h.submit('save',_('Submit Pull Request'),class_="btn")}
102 102 </div>
103 103 <div id="pr_open_message"></div>
104 104 </div>
105 105 </div>
106 106
107 107 <div class="pr-spacing-container"></div>
108 108 </div>
109 109 </div>
110 110 </div>
111 111 <div>
112 112 ## AUTHOR
113 113 <div class="reviewers-title block-right">
114 114 <div class="pr-details-title">
115 115 ${_('Author of this pull request')}
116 116 </div>
117 117 </div>
118 118 <div class="block-right pr-details-content reviewers">
119 119 <ul class="group_members">
120 120 <li>
121 121 ${self.gravatar_with_user(c.rhodecode_user.email, 16)}
122 122 </li>
123 123 </ul>
124 124 </div>
125 125
126 126 ## REVIEW RULES
127 127 <div id="review_rules" style="display: none" class="reviewers-title block-right">
128 128 <div class="pr-details-title">
129 129 ${_('Reviewer rules')}
130 130 </div>
131 131 <div class="pr-reviewer-rules">
132 132 ## review rules will be appended here, by default reviewers logic
133 133 </div>
134 134 </div>
135 135
136 136 ## REVIEWERS
137 137 <div class="reviewers-title block-right">
138 138 <div class="pr-details-title">
139 139 ${_('Pull request reviewers')}
140 140 <span class="calculate-reviewers"> - ${_('loading...')}</span>
141 141 </div>
142 142 </div>
143 143 <div id="reviewers" class="block-right pr-details-content reviewers">
144 144 ## members goes here, filled via JS based on initial selection !
145 145 <input type="hidden" name="__start__" value="review_members:sequence">
146 146 <ul id="review_members" class="group_members"></ul>
147 147 <input type="hidden" name="__end__" value="review_members:sequence">
148 148 <div id="add_reviewer_input" class='ac'>
149 149 <div class="reviewer_ac">
150 150 ${h.text('user', class_='ac-input', placeholder=_('Add reviewer or reviewer group'))}
151 151 <div id="reviewers_container"></div>
152 152 </div>
153 153 </div>
154 154 </div>
155 155 </div>
156 156 </div>
157 157 <div class="box">
158 158 <div>
159 159 ## overview pulled by ajax
160 160 <div id="pull_request_overview"></div>
161 161 </div>
162 162 </div>
163 163 ${h.end_form()}
164 164 </div>
165 165
166 166 <script type="text/javascript">
167 167 $(function(){
168 168 var defaultSourceRepo = '${c.default_repo_data['source_repo_name']}';
169 169 var defaultSourceRepoData = ${c.default_repo_data['source_refs_json']|n};
170 170 var defaultTargetRepo = '${c.default_repo_data['target_repo_name']}';
171 171 var defaultTargetRepoData = ${c.default_repo_data['target_refs_json']|n};
172 172
173 173 var $pullRequestForm = $('#pull_request_form');
174 174 var $sourceRepo = $('#source_repo', $pullRequestForm);
175 175 var $targetRepo = $('#target_repo', $pullRequestForm);
176 176 var $sourceRef = $('#source_ref', $pullRequestForm);
177 177 var $targetRef = $('#target_ref', $pullRequestForm);
178 178
179 179 var sourceRepo = function() { return $sourceRepo.eq(0).val() };
180 180 var sourceRef = function() { return $sourceRef.eq(0).val().split(':') };
181 181
182 182 var targetRepo = function() { return $targetRepo.eq(0).val() };
183 183 var targetRef = function() { return $targetRef.eq(0).val().split(':') };
184 184
185 185 var calculateContainerWidth = function() {
186 186 var maxWidth = 0;
187 187 var repoSelect2Containers = ['#source_repo', '#target_repo'];
188 188 $.each(repoSelect2Containers, function(idx, value) {
189 189 $(value).select2('container').width('auto');
190 190 var curWidth = $(value).select2('container').width();
191 191 if (maxWidth <= curWidth) {
192 192 maxWidth = curWidth;
193 193 }
194 194 $.each(repoSelect2Containers, function(idx, value) {
195 195 $(value).select2('container').width(maxWidth + 10);
196 196 });
197 197 });
198 198 };
199 199
200 200 var initRefSelection = function(selectedRef) {
201 201 return function(element, callback) {
202 202 // translate our select2 id into a text, it's a mapping to show
203 203 // simple label when selecting by internal ID.
204 204 var id, refData;
205 205 if (selectedRef === undefined || selectedRef === null) {
206 206 id = element.val();
207 207 refData = element.val().split(':');
208 208
209 209 if (refData.length !== 3){
210 210 refData = ["", "", ""]
211 211 }
212 212 } else {
213 213 id = selectedRef;
214 214 refData = selectedRef.split(':');
215 215 }
216 216
217 217 var text = refData[1];
218 218 if (refData[0] === 'rev') {
219 219 text = text.substring(0, 12);
220 220 }
221 221
222 222 var data = {id: id, text: text};
223 223 callback(data);
224 224 };
225 225 };
226 226
227 227 var formatRefSelection = function(item) {
228 228 var prefix = '';
229 229 var refData = item.id.split(':');
230 230 if (refData[0] === 'branch') {
231 231 prefix = '<i class="icon-branch"></i>';
232 232 }
233 233 else if (refData[0] === 'book') {
234 234 prefix = '<i class="icon-bookmark"></i>';
235 235 }
236 236 else if (refData[0] === 'tag') {
237 237 prefix = '<i class="icon-tag"></i>';
238 238 }
239 239
240 240 var originalOption = item.element;
241 241 return prefix + item.text;
242 242 };
243 243
244 244 // custom code mirror
245 245 var codeMirrorInstance = initPullRequestsCodeMirror('#pullrequest_desc');
246 246
247 247 reviewersController = new ReviewersController();
248 248
249 249 var queryTargetRepo = function(self, query) {
250 250 // cache ALL results if query is empty
251 251 var cacheKey = query.term || '__';
252 252 var cachedData = self.cachedDataSource[cacheKey];
253 253
254 254 if (cachedData) {
255 255 query.callback({results: cachedData.results});
256 256 } else {
257 257 $.ajax({
258 258 url: pyroutes.url('pullrequest_repo_destinations', {'repo_name': templateContext.repo_name}),
259 259 data: {query: query.term},
260 260 dataType: 'json',
261 261 type: 'GET',
262 262 success: function(data) {
263 263 self.cachedDataSource[cacheKey] = data;
264 264 query.callback({results: data.results});
265 265 },
266 266 error: function(data, textStatus, errorThrown) {
267 267 alert(
268 268 "Error while fetching entries.\nError code {0} ({1}).".format(data.status, data.statusText));
269 269 }
270 270 });
271 271 }
272 272 };
273 273
274 274 var queryTargetRefs = function(initialData, query) {
275 275 var data = {results: []};
276 276 // filter initialData
277 277 $.each(initialData, function() {
278 278 var section = this.text;
279 279 var children = [];
280 280 $.each(this.children, function() {
281 281 if (query.term.length === 0 ||
282 282 this.text.toUpperCase().indexOf(query.term.toUpperCase()) >= 0 ) {
283 283 children.push({'id': this.id, 'text': this.text})
284 284 }
285 285 });
286 286 data.results.push({'text': section, 'children': children})
287 287 });
288 288 query.callback({results: data.results});
289 289 };
290 290
291 291 var loadRepoRefDiffPreview = function() {
292 292
293 293 var url_data = {
294 294 'repo_name': targetRepo(),
295 295 'target_repo': sourceRepo(),
296 296 'source_ref': targetRef()[2],
297 297 'source_ref_type': 'rev',
298 298 'target_ref': sourceRef()[2],
299 299 'target_ref_type': 'rev',
300 300 'merge': true,
301 301 '_': Date.now() // bypass browser caching
302 302 }; // gather the source/target ref and repo here
303 303
304 304 if (sourceRef().length !== 3 || targetRef().length !== 3) {
305 305 prButtonLock(true, "${_('Please select source and target')}");
306 306 return;
307 307 }
308 308 var url = pyroutes.url('repo_compare', url_data);
309 309
310 310 // lock PR button, so we cannot send PR before it's calculated
311 311 prButtonLock(true, "${_('Loading compare ...')}", 'compare');
312 312
313 313 if (loadRepoRefDiffPreview._currentRequest) {
314 314 loadRepoRefDiffPreview._currentRequest.abort();
315 315 }
316 316
317 317 loadRepoRefDiffPreview._currentRequest = $.get(url)
318 318 .error(function(data, textStatus, errorThrown) {
319 319 if (textStatus !== 'abort') {
320 320 alert(
321 321 "Error while processing request.\nError code {0} ({1}).".format(
322 322 data.status, data.statusText));
323 323 }
324 324
325 325 })
326 326 .done(function(data) {
327 327 loadRepoRefDiffPreview._currentRequest = null;
328 328 $('#pull_request_overview').html(data);
329 329
330 330 var commitElements = $(data).find('tr[commit_id]');
331 331
332 332 var prTitleAndDesc = getTitleAndDescription(
333 333 sourceRef()[1], commitElements, 5);
334 334
335 335 var title = prTitleAndDesc[0];
336 336 var proposedDescription = prTitleAndDesc[1];
337 337
338 338 var useGeneratedTitle = (
339 339 $('#pullrequest_title').hasClass('autogenerated-title') ||
340 340 $('#pullrequest_title').val() === "");
341 341
342 342 if (title && useGeneratedTitle) {
343 343 // use generated title if we haven't specified our own
344 344 $('#pullrequest_title').val(title);
345 345 $('#pullrequest_title').addClass('autogenerated-title');
346 346
347 347 }
348 348
349 349 var useGeneratedDescription = (
350 350 !codeMirrorInstance._userDefinedDesc ||
351 351 codeMirrorInstance.getValue() === "");
352 352
353 353 if (proposedDescription && useGeneratedDescription) {
354 354 // set proposed content, if we haven't defined our own,
355 355 // or we don't have description written
356 356 codeMirrorInstance._userDefinedDesc = false; // reset state
357 357 codeMirrorInstance.setValue(proposedDescription);
358 358 }
359 359
360 360 var msg = '';
361 361 if (commitElements.length === 1) {
362 362 msg = "${_ungettext('This pull request will consist of __COMMITS__ commit.', 'This pull request will consist of __COMMITS__ commits.', 1)}";
363 363 } else {
364 364 msg = "${_ungettext('This pull request will consist of __COMMITS__ commit.', 'This pull request will consist of __COMMITS__ commits.', 2)}";
365 365 }
366 366
367 367 msg += ' <a id="pull_request_overview_url" href="{0}" target="_blank">${_('Show detailed compare.')}</a>'.format(url);
368 368
369 369 if (commitElements.length) {
370 370 var commitsLink = '<a href="#pull_request_overview"><strong>{0}</strong></a>'.format(commitElements.length);
371 371 prButtonLock(false, msg.replace('__COMMITS__', commitsLink), 'compare');
372 372 }
373 373 else {
374 374 prButtonLock(true, "${_('There are no commits to merge.')}", 'compare');
375 375 }
376 376
377 377
378 378 });
379 379 };
380 380
381 381 var Select2Box = function(element, overrides) {
382 382 var globalDefaults = {
383 383 dropdownAutoWidth: true,
384 384 containerCssClass: "drop-menu",
385 385 dropdownCssClass: "drop-menu-dropdown"
386 386 };
387 387
388 388 var initSelect2 = function(defaultOptions) {
389 389 var options = jQuery.extend(globalDefaults, defaultOptions, overrides);
390 390 element.select2(options);
391 391 };
392 392
393 393 return {
394 394 initRef: function() {
395 395 var defaultOptions = {
396 396 minimumResultsForSearch: 5,
397 397 formatSelection: formatRefSelection
398 398 };
399 399
400 400 initSelect2(defaultOptions);
401 401 },
402 402
403 403 initRepo: function(defaultValue, readOnly) {
404 404 var defaultOptions = {
405 405 initSelection : function (element, callback) {
406 406 var data = {id: defaultValue, text: defaultValue};
407 407 callback(data);
408 408 }
409 409 };
410 410
411 411 initSelect2(defaultOptions);
412 412
413 413 element.select2('val', defaultSourceRepo);
414 414 if (readOnly === true) {
415 415 element.select2('readonly', true);
416 416 }
417 417 }
418 418 };
419 419 };
420 420
421 421 var initTargetRefs = function(refsData, selectedRef) {
422 422
423 423 Select2Box($targetRef, {
424 424 placeholder: "${_('Select commit reference')}",
425 425 query: function(query) {
426 426 queryTargetRefs(refsData, query);
427 427 },
428 428 initSelection : initRefSelection(selectedRef)
429 429 }).initRef();
430 430
431 431 if (!(selectedRef === undefined)) {
432 432 $targetRef.select2('val', selectedRef);
433 433 }
434 434 };
435 435
436 436 var targetRepoChanged = function(repoData) {
437 437 // generate new DESC of target repo displayed next to select
438 438 var prLink = pyroutes.url('pullrequest_new', {'repo_name': repoData['name']});
439 439 $('#target_repo_desc').html(
440 440 "<strong>${_('Target repository')}</strong>: {0}. <a href=\"{1}\">Switch base, and use as source.</a>".format(repoData['description'], prLink)
441 441 );
442 442
443 443 // generate dynamic select2 for refs.
444 444 initTargetRefs(repoData['refs']['select2_refs'],
445 445 repoData['refs']['selected_ref']);
446 446
447 447 };
448 448
449 449 var sourceRefSelect2 = Select2Box($sourceRef, {
450 450 placeholder: "${_('Select commit reference')}",
451 451 query: function(query) {
452 452 var initialData = defaultSourceRepoData['refs']['select2_refs'];
453 453 queryTargetRefs(initialData, query)
454 454 },
455 455 initSelection: initRefSelection()
456 456 }
457 457 );
458 458
459 459 var sourceRepoSelect2 = Select2Box($sourceRepo, {
460 460 query: function(query) {}
461 461 });
462 462
463 463 var targetRepoSelect2 = Select2Box($targetRepo, {
464 464 cachedDataSource: {},
465 465 query: $.debounce(250, function(query) {
466 466 queryTargetRepo(this, query);
467 467 }),
468 formatResult: formatResult
468 formatResult: formatRepoResult
469 469 });
470 470
471 471 sourceRefSelect2.initRef();
472 472
473 473 sourceRepoSelect2.initRepo(defaultSourceRepo, true);
474 474
475 475 targetRepoSelect2.initRepo(defaultTargetRepo, false);
476 476
477 477 $sourceRef.on('change', function(e){
478 478 loadRepoRefDiffPreview();
479 479 reviewersController.loadDefaultReviewers(
480 480 sourceRepo(), sourceRef(), targetRepo(), targetRef());
481 481 });
482 482
483 483 $targetRef.on('change', function(e){
484 484 loadRepoRefDiffPreview();
485 485 reviewersController.loadDefaultReviewers(
486 486 sourceRepo(), sourceRef(), targetRepo(), targetRef());
487 487 });
488 488
489 489 $targetRepo.on('change', function(e){
490 490 var repoName = $(this).val();
491 491 calculateContainerWidth();
492 492 $targetRef.select2('destroy');
493 493 $('#target_ref_loading').show();
494 494
495 495 $.ajax({
496 496 url: pyroutes.url('pullrequest_repo_refs',
497 497 {'repo_name': templateContext.repo_name, 'target_repo_name':repoName}),
498 498 data: {},
499 499 dataType: 'json',
500 500 type: 'GET',
501 501 success: function(data) {
502 502 $('#target_ref_loading').hide();
503 503 targetRepoChanged(data);
504 504 loadRepoRefDiffPreview();
505 505 },
506 506 error: function(data, textStatus, errorThrown) {
507 507 alert("Error while fetching entries.\nError code {0} ({1}).".format(data.status, data.statusText));
508 508 }
509 509 })
510 510
511 511 });
512 512
513 513 prButtonLock(true, "${_('Please select source and target')}", 'all');
514 514
515 515 // auto-load on init, the target refs select2
516 516 calculateContainerWidth();
517 517 targetRepoChanged(defaultTargetRepoData);
518 518
519 519 $('#pullrequest_title').on('keyup', function(e){
520 520 $(this).removeClass('autogenerated-title');
521 521 });
522 522
523 523 % if c.default_source_ref:
524 524 // in case we have a pre-selected value, use it now
525 525 $sourceRef.select2('val', '${c.default_source_ref}');
526 526 // diff preview load
527 527 loadRepoRefDiffPreview();
528 528 // default reviewers
529 529 reviewersController.loadDefaultReviewers(
530 530 sourceRepo(), sourceRef(), targetRepo(), targetRef());
531 531 % endif
532 532
533 533 ReviewerAutoComplete('#user');
534 534 });
535 535 </script>
536 536
537 537 </%def>
General Comments 0
You need to be logged in to leave comments. Login now