##// END OF EJS Templates
pull-requests: moved the listing of pull requests for repo into pyramid....
marcink -
r1766:d8048d6f default
parent child Browse files
Show More
@@ -0,0 +1,184 b''
1 # -*- coding: utf-8 -*-
2
3 # Copyright (C) 2011-2017 RhodeCode GmbH
4 #
5 # This program is free software: you can redistribute it and/or modify
6 # it under the terms of the GNU Affero General Public License, version 3
7 # (only), as published by the Free Software Foundation.
8 #
9 # This program is distributed in the hope that it will be useful,
10 # but WITHOUT ANY WARRANTY; without even the implied warranty of
11 # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
12 # GNU General Public License for more details.
13 #
14 # You should have received a copy of the GNU Affero General Public License
15 # along with this program. If not, see <http://www.gnu.org/licenses/>.
16 #
17 # This program is dual-licensed. If you wish to learn more about the
18 # RhodeCode Enterprise Edition, including its added features, Support services,
19 # and proprietary license terms, please see https://rhodecode.com/licenses/
20
21 import logging
22
23 from pyramid.view import view_config
24
25 from rhodecode.apps._base import RepoAppView, DataGridAppView
26 from rhodecode.lib import helpers as h
27 from rhodecode.lib import audit_logger
28 from rhodecode.lib.auth import (
29 LoginRequired, HasRepoPermissionAnyDecorator)
30 from rhodecode.lib.utils import PartialRenderer
31 from rhodecode.lib.utils2 import str2bool
32 from rhodecode.model.comment import CommentsModel
33 from rhodecode.model.db import PullRequest
34 from rhodecode.model.pull_request import PullRequestModel
35
36 log = logging.getLogger(__name__)
37
38
39 class RepoPullRequestsView(RepoAppView, DataGridAppView):
40
41 def load_default_context(self):
42 c = self._get_local_tmpl_context()
43
44 # TODO(marcink): remove repo_info and use c.rhodecode_db_repo instead
45 c.repo_info = self.db_repo
46
47 self._register_global_c(c)
48 return c
49
50 def _get_pull_requests_list(
51 self, repo_name, source, filter_type, opened_by, statuses):
52
53 draw, start, limit = self._extract_chunk(self.request)
54 search_q, order_by, order_dir = self._extract_ordering(self.request)
55 _render = PartialRenderer('data_table/_dt_elements.mako')
56
57 # pagination
58
59 if filter_type == 'awaiting_review':
60 pull_requests = PullRequestModel().get_awaiting_review(
61 repo_name, source=source, opened_by=opened_by,
62 statuses=statuses, offset=start, length=limit,
63 order_by=order_by, order_dir=order_dir)
64 pull_requests_total_count = PullRequestModel().count_awaiting_review(
65 repo_name, source=source, statuses=statuses,
66 opened_by=opened_by)
67 elif filter_type == 'awaiting_my_review':
68 pull_requests = PullRequestModel().get_awaiting_my_review(
69 repo_name, source=source, opened_by=opened_by,
70 user_id=self._rhodecode_user.user_id, statuses=statuses,
71 offset=start, length=limit, order_by=order_by,
72 order_dir=order_dir)
73 pull_requests_total_count = PullRequestModel().count_awaiting_my_review(
74 repo_name, source=source, user_id=self._rhodecode_user.user_id,
75 statuses=statuses, opened_by=opened_by)
76 else:
77 pull_requests = PullRequestModel().get_all(
78 repo_name, source=source, opened_by=opened_by,
79 statuses=statuses, offset=start, length=limit,
80 order_by=order_by, order_dir=order_dir)
81 pull_requests_total_count = PullRequestModel().count_all(
82 repo_name, source=source, statuses=statuses,
83 opened_by=opened_by)
84
85 data = []
86 comments_model = CommentsModel()
87 for pr in pull_requests:
88 comments = comments_model.get_all_comments(
89 self.db_repo.repo_id, pull_request=pr)
90
91 data.append({
92 'name': _render('pullrequest_name',
93 pr.pull_request_id, pr.target_repo.repo_name),
94 'name_raw': pr.pull_request_id,
95 'status': _render('pullrequest_status',
96 pr.calculated_review_status()),
97 'title': _render(
98 'pullrequest_title', pr.title, pr.description),
99 'description': h.escape(pr.description),
100 'updated_on': _render('pullrequest_updated_on',
101 h.datetime_to_time(pr.updated_on)),
102 'updated_on_raw': h.datetime_to_time(pr.updated_on),
103 'created_on': _render('pullrequest_updated_on',
104 h.datetime_to_time(pr.created_on)),
105 'created_on_raw': h.datetime_to_time(pr.created_on),
106 'author': _render('pullrequest_author',
107 pr.author.full_contact, ),
108 'author_raw': pr.author.full_name,
109 'comments': _render('pullrequest_comments', len(comments)),
110 'comments_raw': len(comments),
111 'closed': pr.is_closed(),
112 })
113
114 data = ({
115 'draw': draw,
116 'data': data,
117 'recordsTotal': pull_requests_total_count,
118 'recordsFiltered': pull_requests_total_count,
119 })
120 return data
121
122 @LoginRequired()
123 @HasRepoPermissionAnyDecorator(
124 'repository.read', 'repository.write', 'repository.admin')
125 @view_config(
126 route_name='pullrequest_show_all', request_method='GET',
127 renderer='rhodecode:templates/pullrequests/pullrequests.mako')
128 def pull_request_list(self):
129 c = self.load_default_context()
130
131 req_get = self.request.GET
132 c.source = str2bool(req_get.get('source'))
133 c.closed = str2bool(req_get.get('closed'))
134 c.my = str2bool(req_get.get('my'))
135 c.awaiting_review = str2bool(req_get.get('awaiting_review'))
136 c.awaiting_my_review = str2bool(req_get.get('awaiting_my_review'))
137
138 c.active = 'open'
139 if c.my:
140 c.active = 'my'
141 if c.closed:
142 c.active = 'closed'
143 if c.awaiting_review and not c.source:
144 c.active = 'awaiting'
145 if c.source and not c.awaiting_review:
146 c.active = 'source'
147 if c.awaiting_my_review:
148 c.active = 'awaiting_my'
149
150 return self._get_template_context(c)
151
152 @LoginRequired()
153 @HasRepoPermissionAnyDecorator(
154 'repository.read', 'repository.write', 'repository.admin')
155 @view_config(
156 route_name='pullrequest_show_all_data', request_method='GET',
157 renderer='json_ext', xhr=True)
158 def pull_request_list_data(self):
159
160 # additional filters
161 req_get = self.request.GET
162 source = str2bool(req_get.get('source'))
163 closed = str2bool(req_get.get('closed'))
164 my = str2bool(req_get.get('my'))
165 awaiting_review = str2bool(req_get.get('awaiting_review'))
166 awaiting_my_review = str2bool(req_get.get('awaiting_my_review'))
167
168 filter_type = 'awaiting_review' if awaiting_review \
169 else 'awaiting_my_review' if awaiting_my_review \
170 else None
171
172 opened_by = None
173 if my:
174 opened_by = [self._rhodecode_user.user_id]
175
176 statuses = [PullRequest.STATUS_NEW, PullRequest.STATUS_OPEN]
177 if closed:
178 statuses = [PullRequest.STATUS_CLOSED]
179
180 data = self._get_pull_requests_list(
181 repo_name=self.db_repo_name, source=source,
182 filter_type=filter_type, opened_by=opened_by, statuses=statuses)
183
184 return data
@@ -1,273 +1,301 b''
1 1 # -*- coding: utf-8 -*-
2 2
3 3 # Copyright (C) 2016-2017 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 time
22 22 import logging
23 23 from pylons import tmpl_context as c
24 24 from pyramid.httpexceptions import HTTPFound
25 25
26 26 from rhodecode.lib import helpers as h
27 27 from rhodecode.lib.utils import PartialRenderer
28 28 from rhodecode.lib.utils2 import StrictAttributeDict, safe_int, datetime_to_time
29 29 from rhodecode.lib.vcs.exceptions import RepositoryRequirementError
30 30 from rhodecode.lib.ext_json import json
31 31 from rhodecode.model import repo
32 32 from rhodecode.model.db import User
33 33 from rhodecode.model.scm import ScmModel
34 34
35 35 log = logging.getLogger(__name__)
36 36
37 37
38 38 ADMIN_PREFIX = '/_admin'
39 39 STATIC_FILE_PREFIX = '/_static'
40 40
41 41
42 42 def get_format_ref_id(repo):
43 43 """Returns a `repo` specific reference formatter function"""
44 44 if h.is_svn(repo):
45 45 return _format_ref_id_svn
46 46 else:
47 47 return _format_ref_id
48 48
49 49
50 50 def _format_ref_id(name, raw_id):
51 51 """Default formatting of a given reference `name`"""
52 52 return name
53 53
54 54
55 55 def _format_ref_id_svn(name, raw_id):
56 56 """Special way of formatting a reference for Subversion including path"""
57 57 return '%s@%s' % (name, raw_id)
58 58
59 59
60 60 class TemplateArgs(StrictAttributeDict):
61 61 pass
62 62
63 63
64 64 class BaseAppView(object):
65 65
66 66 def __init__(self, context, request):
67 67 self.request = request
68 68 self.context = context
69 69 self.session = request.session
70 70 self._rhodecode_user = request.user # auth user
71 71 self._rhodecode_db_user = self._rhodecode_user.get_instance()
72 72 self._maybe_needs_password_change(
73 73 request.matched_route.name, self._rhodecode_db_user)
74 74
75 75 def _maybe_needs_password_change(self, view_name, user_obj):
76 76 log.debug('Checking if user %s needs password change on view %s',
77 77 user_obj, view_name)
78 78 skip_user_views = [
79 79 'logout', 'login',
80 80 'my_account_password', 'my_account_password_update'
81 81 ]
82 82
83 83 if not user_obj:
84 84 return
85 85
86 86 if user_obj.username == User.DEFAULT_USER:
87 87 return
88 88
89 89 now = time.time()
90 90 should_change = user_obj.user_data.get('force_password_change')
91 91 change_after = safe_int(should_change) or 0
92 92 if should_change and now > change_after:
93 93 log.debug('User %s requires password change', user_obj)
94 94 h.flash('You are required to change your password', 'warning',
95 95 ignore_duplicate=True)
96 96
97 97 if view_name not in skip_user_views:
98 98 raise HTTPFound(
99 99 self.request.route_path('my_account_password'))
100 100
101 101 def _get_local_tmpl_context(self):
102 102 c = TemplateArgs()
103 103 c.auth_user = self.request.user
104 104 return c
105 105
106 106 def _register_global_c(self, tmpl_args):
107 107 """
108 108 Registers attributes to pylons global `c`
109 109 """
110 110 # TODO(marcink): remove once pyramid migration is finished
111 111 for k, v in tmpl_args.items():
112 112 setattr(c, k, v)
113 113
114 114 def _get_template_context(self, tmpl_args):
115 115 self._register_global_c(tmpl_args)
116 116
117 117 local_tmpl_args = {
118 118 'defaults': {},
119 119 'errors': {},
120 120 }
121 121 local_tmpl_args.update(tmpl_args)
122 122 return local_tmpl_args
123 123
124 124 def load_default_context(self):
125 125 """
126 126 example:
127 127
128 128 def load_default_context(self):
129 129 c = self._get_local_tmpl_context()
130 130 c.custom_var = 'foobar'
131 131 self._register_global_c(c)
132 132 return c
133 133 """
134 134 raise NotImplementedError('Needs implementation in view class')
135 135
136 136
137 137 class RepoAppView(BaseAppView):
138 138
139 139 def __init__(self, context, request):
140 140 super(RepoAppView, self).__init__(context, request)
141 141 self.db_repo = request.db_repo
142 142 self.db_repo_name = self.db_repo.repo_name
143 143 self.db_repo_pull_requests = ScmModel().get_pull_requests(self.db_repo)
144 144
145 145 def _handle_missing_requirements(self, error):
146 146 log.error(
147 147 'Requirements are missing for repository %s: %s',
148 148 self.db_repo_name, error.message)
149 149
150 150 def _get_local_tmpl_context(self):
151 151 c = super(RepoAppView, self)._get_local_tmpl_context()
152 152 # register common vars for this type of view
153 153 c.rhodecode_db_repo = self.db_repo
154 154 c.repo_name = self.db_repo_name
155 155 c.repository_pull_requests = self.db_repo_pull_requests
156 156
157 157 c.repository_requirements_missing = False
158 158 try:
159 159 self.rhodecode_vcs_repo = self.db_repo.scm_instance()
160 160 except RepositoryRequirementError as e:
161 161 c.repository_requirements_missing = True
162 162 self._handle_missing_requirements(e)
163 163
164 164 return c
165 165
166 166
167 167 class DataGridAppView(object):
168 168 """
169 169 Common class to have re-usable grid rendering components
170 170 """
171 171
172 172 def _extract_ordering(self, request, column_map=None):
173 173 column_map = column_map or {}
174 174 column_index = safe_int(request.GET.get('order[0][column]'))
175 175 order_dir = request.GET.get(
176 176 'order[0][dir]', 'desc')
177 177 order_by = request.GET.get(
178 178 'columns[%s][data][sort]' % column_index, 'name_raw')
179 179
180 180 # translate datatable to DB columns
181 181 order_by = column_map.get(order_by) or order_by
182 182
183 183 search_q = request.GET.get('search[value]')
184 184 return search_q, order_by, order_dir
185 185
186 186 def _extract_chunk(self, request):
187 187 start = safe_int(request.GET.get('start'), 0)
188 188 length = safe_int(request.GET.get('length'), 25)
189 189 draw = safe_int(request.GET.get('draw'))
190 190 return draw, start, length
191 191
192 192
193 193 class BaseReferencesView(RepoAppView):
194 194 """
195 195 Base for reference view for branches, tags and bookmarks.
196 196 """
197 197 def load_default_context(self):
198 198 c = self._get_local_tmpl_context()
199 199
200 200 # TODO(marcink): remove repo_info and use c.rhodecode_db_repo instead
201 201 c.repo_info = self.db_repo
202 202
203 203 self._register_global_c(c)
204 204 return c
205 205
206 206 def load_refs_context(self, ref_items, partials_template):
207 207 _render = PartialRenderer(partials_template)
208 208 _data = []
209 209 pre_load = ["author", "date", "message"]
210 210
211 211 is_svn = h.is_svn(self.rhodecode_vcs_repo)
212 212 format_ref_id = get_format_ref_id(self.rhodecode_vcs_repo)
213 213
214 214 for ref_name, commit_id in ref_items:
215 215 commit = self.rhodecode_vcs_repo.get_commit(
216 216 commit_id=commit_id, pre_load=pre_load)
217 217
218 218 # TODO: johbo: Unify generation of reference links
219 219 use_commit_id = '/' in ref_name or is_svn
220 220 files_url = h.url(
221 221 'files_home',
222 222 repo_name=c.repo_name,
223 223 f_path=ref_name if is_svn else '',
224 224 revision=commit_id if use_commit_id else ref_name,
225 225 at=ref_name)
226 226
227 227 _data.append({
228 228 "name": _render('name', ref_name, files_url),
229 229 "name_raw": ref_name,
230 230 "date": _render('date', commit.date),
231 231 "date_raw": datetime_to_time(commit.date),
232 232 "author": _render('author', commit.author),
233 233 "commit": _render(
234 234 'commit', commit.message, commit.raw_id, commit.idx),
235 235 "commit_raw": commit.idx,
236 236 "compare": _render(
237 237 'compare', format_ref_id(ref_name, commit.raw_id)),
238 238 })
239 239 c.has_references = bool(_data)
240 240 c.data = json.dumps(_data)
241 241
242 242
243 243 class RepoRoutePredicate(object):
244 244 def __init__(self, val, config):
245 245 self.val = val
246 246
247 247 def text(self):
248 248 return 'repo_route = %s' % self.val
249 249
250 250 phash = text
251 251
252 252 def __call__(self, info, request):
253 253 repo_name = info['match']['repo_name']
254 254 repo_model = repo.RepoModel()
255 255 by_name_match = repo_model.get_by_repo_name(repo_name, cache=True)
256 256 # if we match quickly from database, short circuit the operation,
257 257 # and validate repo based on the type.
258 258 if by_name_match:
259 259 # register this as request object we can re-use later
260 260 request.db_repo = by_name_match
261 261 return True
262 262
263 263 by_id_match = repo_model.get_repo_by_id(repo_name)
264 264 if by_id_match:
265 265 request.db_repo = by_id_match
266 266 return True
267 267
268 268 return False
269 269
270 270
271 class RepoTypeRoutePredicate(object):
272 def __init__(self, val, config):
273 self.val = val or ['hg', 'git', 'svn']
274
275 def text(self):
276 return 'repo_accepted_type = %s' % self.val
277
278 phash = text
279
280 def __call__(self, info, request):
281
282 rhodecode_db_repo = request.db_repo
283
284 log.debug(
285 '%s checking repo type for %s in %s',
286 self.__class__.__name__, rhodecode_db_repo.repo_type, self.val)
287
288 if rhodecode_db_repo.repo_type in self.val:
289 return True
290 else:
291 log.warning('Current view is not supported for repo type:%s',
292 rhodecode_db_repo.repo_type)
293 return False
294
295
296
271 297 def includeme(config):
272 298 config.add_route_predicate(
273 299 'repo_route', RepoRoutePredicate)
300 config.add_route_predicate(
301 'repo_accepted_types', RepoTypeRoutePredicate) No newline at end of file
@@ -1,115 +1,125 b''
1 1 # -*- coding: utf-8 -*-
2 2
3 3 # Copyright (C) 2016-2017 RhodeCode GmbH
4 4 #
5 5 # This program is free software: you can redistribute it and/or modify
6 6 # it under the terms of the GNU Affero General Public License, version 3
7 7 # (only), as published by the Free Software Foundation.
8 8 #
9 9 # This program is distributed in the hope that it will be useful,
10 10 # but WITHOUT ANY WARRANTY; without even the implied warranty of
11 11 # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
12 12 # GNU General Public License for more details.
13 13 #
14 14 # You should have received a copy of the GNU Affero General Public License
15 15 # along with this program. If not, see <http://www.gnu.org/licenses/>.
16 16 #
17 17 # This program is dual-licensed. If you wish to learn more about the
18 18 # RhodeCode Enterprise Edition, including its added features, Support services,
19 19 # and proprietary license terms, please see https://rhodecode.com/licenses/
20 20
21 21
22 22 def includeme(config):
23 23
24 24 # Summary
25 25 config.add_route(
26 26 name='repo_summary_explicit',
27 27 pattern='/{repo_name:.*?[^/]}/summary', repo_route=True)
28 28
29 29 # Tags
30 30 config.add_route(
31 31 name='tags_home',
32 32 pattern='/{repo_name:.*?[^/]}/tags', repo_route=True)
33 33
34 34 # Branches
35 35 config.add_route(
36 36 name='branches_home',
37 37 pattern='/{repo_name:.*?[^/]}/branches', repo_route=True)
38 38
39 39 # Bookmarks
40 40 config.add_route(
41 41 name='bookmarks_home',
42 42 pattern='/{repo_name:.*?[^/]}/bookmarks', repo_route=True)
43 43
44 44 # Pull Requests
45 45 config.add_route(
46 46 name='pullrequest_show',
47 47 pattern='/{repo_name:.*?[^/]}/pull-request/{pull_request_id}',
48 48 repo_route=True)
49 49
50 config.add_route(
51 name='pullrequest_show_all',
52 pattern='/{repo_name:.*?[^/]}/pull-request',
53 repo_route=True, repo_accepted_types=['hg', 'git'])
54
55 config.add_route(
56 name='pullrequest_show_all_data',
57 pattern='/{repo_name:.*?[^/]}/pull-request-data',
58 repo_route=True, repo_accepted_types=['hg', 'git'])
59
50 60 # Settings
51 61 config.add_route(
52 62 name='edit_repo',
53 63 pattern='/{repo_name:.*?[^/]}/settings', repo_route=True)
54 64
55 65 # Settings advanced
56 66 config.add_route(
57 67 name='edit_repo_advanced',
58 68 pattern='/{repo_name:.*?[^/]}/settings/advanced', repo_route=True)
59 69 config.add_route(
60 70 name='edit_repo_advanced_delete',
61 71 pattern='/{repo_name:.*?[^/]}/settings/advanced/delete', repo_route=True)
62 72 config.add_route(
63 73 name='edit_repo_advanced_locking',
64 74 pattern='/{repo_name:.*?[^/]}/settings/advanced/locking', repo_route=True)
65 75 config.add_route(
66 76 name='edit_repo_advanced_journal',
67 77 pattern='/{repo_name:.*?[^/]}/settings/advanced/journal', repo_route=True)
68 78 config.add_route(
69 79 name='edit_repo_advanced_fork',
70 80 pattern='/{repo_name:.*?[^/]}/settings/advanced/fork', repo_route=True)
71 81
72 82 # Caches
73 83 config.add_route(
74 84 name='edit_repo_caches',
75 85 pattern='/{repo_name:.*?[^/]}/settings/caches', repo_route=True)
76 86
77 87 # Permissions
78 88 config.add_route(
79 89 name='edit_repo_perms',
80 90 pattern='/{repo_name:.*?[^/]}/settings/permissions', repo_route=True)
81 91
82 92 # Repo Review Rules
83 93 config.add_route(
84 94 name='repo_reviewers',
85 95 pattern='/{repo_name:.*?[^/]}/settings/review/rules', repo_route=True)
86 96
87 97 # Maintenance
88 98 config.add_route(
89 99 name='repo_maintenance',
90 100 pattern='/{repo_name:.*?[^/]}/settings/maintenance', repo_route=True)
91 101
92 102 config.add_route(
93 103 name='repo_maintenance_execute',
94 104 pattern='/{repo_name:.*?[^/]}/settings/maintenance/execute', repo_route=True)
95 105
96 106 # Strip
97 107 config.add_route(
98 108 name='strip',
99 109 pattern='/{repo_name:.*?[^/]}/settings/strip', repo_route=True)
100 110
101 111 config.add_route(
102 112 name='strip_check',
103 113 pattern='/{repo_name:.*?[^/]}/settings/strip_check', repo_route=True)
104 114
105 115 config.add_route(
106 116 name='strip_execute',
107 117 pattern='/{repo_name:.*?[^/]}/settings/strip_execute', repo_route=True)
108 118
109 119 # NOTE(marcink): needs to be at the end for catch-all
110 120 # config.add_route(
111 121 # name='repo_summary',
112 122 # pattern='/{repo_name:.*?[^/]}', repo_route=True)
113 123
114 124 # Scan module for configuration decorators.
115 125 config.scan()
@@ -1,1037 +1,1030 b''
1 1 # -*- coding: utf-8 -*-
2 2
3 3 # Copyright (C) 2010-2017 RhodeCode GmbH
4 4 #
5 5 # This program is free software: you can redistribute it and/or modify
6 6 # it under the terms of the GNU Affero General Public License, version 3
7 7 # (only), as published by the Free Software Foundation.
8 8 #
9 9 # This program is distributed in the hope that it will be useful,
10 10 # but WITHOUT ANY WARRANTY; without even the implied warranty of
11 11 # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
12 12 # GNU General Public License for more details.
13 13 #
14 14 # You should have received a copy of the GNU Affero General Public License
15 15 # along with this program. If not, see <http://www.gnu.org/licenses/>.
16 16 #
17 17 # This program is dual-licensed. If you wish to learn more about the
18 18 # RhodeCode Enterprise Edition, including its added features, Support services,
19 19 # and proprietary license terms, please see https://rhodecode.com/licenses/
20 20
21 21 """
22 22 Routes configuration
23 23
24 24 The more specific and detailed routes should be defined first so they
25 25 may take precedent over the more generic routes. For more information
26 26 refer to the routes manual at http://routes.groovie.org/docs/
27 27
28 28 IMPORTANT: if you change any routing here, make sure to take a look at lib/base.py
29 29 and _route_name variable which uses some of stored naming here to do redirects.
30 30 """
31 31 import os
32 32 import re
33 33 from routes import Mapper
34 34
35 35 # prefix for non repository related links needs to be prefixed with `/`
36 36 ADMIN_PREFIX = '/_admin'
37 37 STATIC_FILE_PREFIX = '/_static'
38 38
39 39 # Default requirements for URL parts
40 40 URL_NAME_REQUIREMENTS = {
41 41 # group name can have a slash in them, but they must not end with a slash
42 42 'group_name': r'.*?[^/]',
43 43 'repo_group_name': r'.*?[^/]',
44 44 # repo names can have a slash in them, but they must not end with a slash
45 45 'repo_name': r'.*?[^/]',
46 46 # file path eats up everything at the end
47 47 'f_path': r'.*',
48 48 # reference types
49 49 'source_ref_type': '(branch|book|tag|rev|\%\(source_ref_type\)s)',
50 50 'target_ref_type': '(branch|book|tag|rev|\%\(target_ref_type\)s)',
51 51 }
52 52
53 53
54 54 def add_route_requirements(route_path, requirements):
55 55 """
56 56 Adds regex requirements to pyramid routes using a mapping dict
57 57
58 58 >>> add_route_requirements('/{action}/{id}', {'id': r'\d+'})
59 59 '/{action}/{id:\d+}'
60 60
61 61 """
62 62 for key, regex in requirements.items():
63 63 route_path = route_path.replace('{%s}' % key, '{%s:%s}' % (key, regex))
64 64 return route_path
65 65
66 66
67 67 class JSRoutesMapper(Mapper):
68 68 """
69 69 Wrapper for routes.Mapper to make pyroutes compatible url definitions
70 70 """
71 71 _named_route_regex = re.compile(r'^[a-z-_0-9A-Z]+$')
72 72 _argument_prog = re.compile('\{(.*?)\}|:\((.*)\)')
73 73 def __init__(self, *args, **kw):
74 74 super(JSRoutesMapper, self).__init__(*args, **kw)
75 75 self._jsroutes = []
76 76
77 77 def connect(self, *args, **kw):
78 78 """
79 79 Wrapper for connect to take an extra argument jsroute=True
80 80
81 81 :param jsroute: boolean, if True will add the route to the pyroutes list
82 82 """
83 83 if kw.pop('jsroute', False):
84 84 if not self._named_route_regex.match(args[0]):
85 85 raise Exception('only named routes can be added to pyroutes')
86 86 self._jsroutes.append(args[0])
87 87
88 88 super(JSRoutesMapper, self).connect(*args, **kw)
89 89
90 90 def _extract_route_information(self, route):
91 91 """
92 92 Convert a route into tuple(name, path, args), eg:
93 93 ('show_user', '/profile/%(username)s', ['username'])
94 94 """
95 95 routepath = route.routepath
96 96 def replace(matchobj):
97 97 if matchobj.group(1):
98 98 return "%%(%s)s" % matchobj.group(1).split(':')[0]
99 99 else:
100 100 return "%%(%s)s" % matchobj.group(2)
101 101
102 102 routepath = self._argument_prog.sub(replace, routepath)
103 103 return (
104 104 route.name,
105 105 routepath,
106 106 [(arg[0].split(':')[0] if arg[0] != '' else arg[1])
107 107 for arg in self._argument_prog.findall(route.routepath)]
108 108 )
109 109
110 110 def jsroutes(self):
111 111 """
112 112 Return a list of pyroutes.js compatible routes
113 113 """
114 114 for route_name in self._jsroutes:
115 115 yield self._extract_route_information(self._routenames[route_name])
116 116
117 117
118 118 def make_map(config):
119 119 """Create, configure and return the routes Mapper"""
120 120 rmap = JSRoutesMapper(directory=config['pylons.paths']['controllers'],
121 121 always_scan=config['debug'])
122 122 rmap.minimization = False
123 123 rmap.explicit = False
124 124
125 125 from rhodecode.lib.utils2 import str2bool
126 126 from rhodecode.model import repo, repo_group
127 127
128 128 def check_repo(environ, match_dict):
129 129 """
130 130 check for valid repository for proper 404 handling
131 131
132 132 :param environ:
133 133 :param match_dict:
134 134 """
135 135 repo_name = match_dict.get('repo_name')
136 136
137 137 if match_dict.get('f_path'):
138 138 # fix for multiple initial slashes that causes errors
139 139 match_dict['f_path'] = match_dict['f_path'].lstrip('/')
140 140 repo_model = repo.RepoModel()
141 141 by_name_match = repo_model.get_by_repo_name(repo_name)
142 142 # if we match quickly from database, short circuit the operation,
143 143 # and validate repo based on the type.
144 144 if by_name_match:
145 145 return True
146 146
147 147 by_id_match = repo_model.get_repo_by_id(repo_name)
148 148 if by_id_match:
149 149 repo_name = by_id_match.repo_name
150 150 match_dict['repo_name'] = repo_name
151 151 return True
152 152
153 153 return False
154 154
155 155 def check_group(environ, match_dict):
156 156 """
157 157 check for valid repository group path for proper 404 handling
158 158
159 159 :param environ:
160 160 :param match_dict:
161 161 """
162 162 repo_group_name = match_dict.get('group_name')
163 163 repo_group_model = repo_group.RepoGroupModel()
164 164 by_name_match = repo_group_model.get_by_group_name(repo_group_name)
165 165 if by_name_match:
166 166 return True
167 167
168 168 return False
169 169
170 170 def check_user_group(environ, match_dict):
171 171 """
172 172 check for valid user group for proper 404 handling
173 173
174 174 :param environ:
175 175 :param match_dict:
176 176 """
177 177 return True
178 178
179 179 def check_int(environ, match_dict):
180 180 return match_dict.get('id').isdigit()
181 181
182 182
183 183 #==========================================================================
184 184 # CUSTOM ROUTES HERE
185 185 #==========================================================================
186 186
187 187 # MAIN PAGE
188 188 rmap.connect('home', '/', controller='home', action='index')
189 189
190 190 # ping and pylons error test
191 191 rmap.connect('ping', '%s/ping' % (ADMIN_PREFIX,), controller='home', action='ping')
192 192 rmap.connect('error_test', '%s/error_test' % (ADMIN_PREFIX,), controller='home', action='error_test')
193 193
194 194 # ADMIN REPOSITORY ROUTES
195 195 with rmap.submapper(path_prefix=ADMIN_PREFIX,
196 196 controller='admin/repos') as m:
197 197 m.connect('repos', '/repos',
198 198 action='create', conditions={'method': ['POST']})
199 199 m.connect('repos', '/repos',
200 200 action='index', conditions={'method': ['GET']})
201 201 m.connect('new_repo', '/create_repository', jsroute=True,
202 202 action='create_repository', conditions={'method': ['GET']})
203 203 m.connect('delete_repo', '/repos/{repo_name}',
204 204 action='delete', conditions={'method': ['DELETE']},
205 205 requirements=URL_NAME_REQUIREMENTS)
206 206 m.connect('repo', '/repos/{repo_name}',
207 207 action='show', conditions={'method': ['GET'],
208 208 'function': check_repo},
209 209 requirements=URL_NAME_REQUIREMENTS)
210 210
211 211 # ADMIN REPOSITORY GROUPS ROUTES
212 212 with rmap.submapper(path_prefix=ADMIN_PREFIX,
213 213 controller='admin/repo_groups') as m:
214 214 m.connect('repo_groups', '/repo_groups',
215 215 action='create', conditions={'method': ['POST']})
216 216 m.connect('repo_groups', '/repo_groups',
217 217 action='index', conditions={'method': ['GET']})
218 218 m.connect('new_repo_group', '/repo_groups/new',
219 219 action='new', conditions={'method': ['GET']})
220 220 m.connect('update_repo_group', '/repo_groups/{group_name}',
221 221 action='update', conditions={'method': ['PUT'],
222 222 'function': check_group},
223 223 requirements=URL_NAME_REQUIREMENTS)
224 224
225 225 # EXTRAS REPO GROUP ROUTES
226 226 m.connect('edit_repo_group', '/repo_groups/{group_name}/edit',
227 227 action='edit',
228 228 conditions={'method': ['GET'], 'function': check_group},
229 229 requirements=URL_NAME_REQUIREMENTS)
230 230 m.connect('edit_repo_group', '/repo_groups/{group_name}/edit',
231 231 action='edit',
232 232 conditions={'method': ['PUT'], 'function': check_group},
233 233 requirements=URL_NAME_REQUIREMENTS)
234 234
235 235 m.connect('edit_repo_group_advanced', '/repo_groups/{group_name}/edit/advanced',
236 236 action='edit_repo_group_advanced',
237 237 conditions={'method': ['GET'], 'function': check_group},
238 238 requirements=URL_NAME_REQUIREMENTS)
239 239 m.connect('edit_repo_group_advanced', '/repo_groups/{group_name}/edit/advanced',
240 240 action='edit_repo_group_advanced',
241 241 conditions={'method': ['PUT'], 'function': check_group},
242 242 requirements=URL_NAME_REQUIREMENTS)
243 243
244 244 m.connect('edit_repo_group_perms', '/repo_groups/{group_name}/edit/permissions',
245 245 action='edit_repo_group_perms',
246 246 conditions={'method': ['GET'], 'function': check_group},
247 247 requirements=URL_NAME_REQUIREMENTS)
248 248 m.connect('edit_repo_group_perms', '/repo_groups/{group_name}/edit/permissions',
249 249 action='update_perms',
250 250 conditions={'method': ['PUT'], 'function': check_group},
251 251 requirements=URL_NAME_REQUIREMENTS)
252 252
253 253 m.connect('delete_repo_group', '/repo_groups/{group_name}',
254 254 action='delete', conditions={'method': ['DELETE'],
255 255 'function': check_group},
256 256 requirements=URL_NAME_REQUIREMENTS)
257 257
258 258 # ADMIN USER ROUTES
259 259 with rmap.submapper(path_prefix=ADMIN_PREFIX,
260 260 controller='admin/users') as m:
261 261 m.connect('users', '/users',
262 262 action='create', conditions={'method': ['POST']})
263 263 m.connect('new_user', '/users/new',
264 264 action='new', conditions={'method': ['GET']})
265 265 m.connect('update_user', '/users/{user_id}',
266 266 action='update', conditions={'method': ['PUT']})
267 267 m.connect('delete_user', '/users/{user_id}',
268 268 action='delete', conditions={'method': ['DELETE']})
269 269 m.connect('edit_user', '/users/{user_id}/edit',
270 270 action='edit', conditions={'method': ['GET']}, jsroute=True)
271 271 m.connect('user', '/users/{user_id}',
272 272 action='show', conditions={'method': ['GET']})
273 273 m.connect('force_password_reset_user', '/users/{user_id}/password_reset',
274 274 action='reset_password', conditions={'method': ['POST']})
275 275 m.connect('create_personal_repo_group', '/users/{user_id}/create_repo_group',
276 276 action='create_personal_repo_group', conditions={'method': ['POST']})
277 277
278 278 # EXTRAS USER ROUTES
279 279 m.connect('edit_user_advanced', '/users/{user_id}/edit/advanced',
280 280 action='edit_advanced', conditions={'method': ['GET']})
281 281 m.connect('edit_user_advanced', '/users/{user_id}/edit/advanced',
282 282 action='update_advanced', conditions={'method': ['PUT']})
283 283
284 284 m.connect('edit_user_global_perms', '/users/{user_id}/edit/global_permissions',
285 285 action='edit_global_perms', conditions={'method': ['GET']})
286 286 m.connect('edit_user_global_perms', '/users/{user_id}/edit/global_permissions',
287 287 action='update_global_perms', conditions={'method': ['PUT']})
288 288
289 289 m.connect('edit_user_perms_summary', '/users/{user_id}/edit/permissions_summary',
290 290 action='edit_perms_summary', conditions={'method': ['GET']})
291 291
292 292 m.connect('edit_user_emails', '/users/{user_id}/edit/emails',
293 293 action='edit_emails', conditions={'method': ['GET']})
294 294 m.connect('edit_user_emails', '/users/{user_id}/edit/emails',
295 295 action='add_email', conditions={'method': ['PUT']})
296 296 m.connect('edit_user_emails', '/users/{user_id}/edit/emails',
297 297 action='delete_email', conditions={'method': ['DELETE']})
298 298
299 299 m.connect('edit_user_ips', '/users/{user_id}/edit/ips',
300 300 action='edit_ips', conditions={'method': ['GET']})
301 301 m.connect('edit_user_ips', '/users/{user_id}/edit/ips',
302 302 action='add_ip', conditions={'method': ['PUT']})
303 303 m.connect('edit_user_ips', '/users/{user_id}/edit/ips',
304 304 action='delete_ip', conditions={'method': ['DELETE']})
305 305
306 306 # ADMIN USER GROUPS REST ROUTES
307 307 with rmap.submapper(path_prefix=ADMIN_PREFIX,
308 308 controller='admin/user_groups') as m:
309 309 m.connect('users_groups', '/user_groups',
310 310 action='create', conditions={'method': ['POST']})
311 311 m.connect('users_groups', '/user_groups',
312 312 action='index', conditions={'method': ['GET']})
313 313 m.connect('new_users_group', '/user_groups/new',
314 314 action='new', conditions={'method': ['GET']})
315 315 m.connect('update_users_group', '/user_groups/{user_group_id}',
316 316 action='update', conditions={'method': ['PUT']})
317 317 m.connect('delete_users_group', '/user_groups/{user_group_id}',
318 318 action='delete', conditions={'method': ['DELETE']})
319 319 m.connect('edit_users_group', '/user_groups/{user_group_id}/edit',
320 320 action='edit', conditions={'method': ['GET']},
321 321 function=check_user_group)
322 322
323 323 # EXTRAS USER GROUP ROUTES
324 324 m.connect('edit_user_group_global_perms',
325 325 '/user_groups/{user_group_id}/edit/global_permissions',
326 326 action='edit_global_perms', conditions={'method': ['GET']})
327 327 m.connect('edit_user_group_global_perms',
328 328 '/user_groups/{user_group_id}/edit/global_permissions',
329 329 action='update_global_perms', conditions={'method': ['PUT']})
330 330 m.connect('edit_user_group_perms_summary',
331 331 '/user_groups/{user_group_id}/edit/permissions_summary',
332 332 action='edit_perms_summary', conditions={'method': ['GET']})
333 333
334 334 m.connect('edit_user_group_perms',
335 335 '/user_groups/{user_group_id}/edit/permissions',
336 336 action='edit_perms', conditions={'method': ['GET']})
337 337 m.connect('edit_user_group_perms',
338 338 '/user_groups/{user_group_id}/edit/permissions',
339 339 action='update_perms', conditions={'method': ['PUT']})
340 340
341 341 m.connect('edit_user_group_advanced',
342 342 '/user_groups/{user_group_id}/edit/advanced',
343 343 action='edit_advanced', conditions={'method': ['GET']})
344 344
345 345 m.connect('edit_user_group_advanced_sync',
346 346 '/user_groups/{user_group_id}/edit/advanced/sync',
347 347 action='edit_advanced_set_synchronization', conditions={'method': ['POST']})
348 348
349 349 m.connect('edit_user_group_members',
350 350 '/user_groups/{user_group_id}/edit/members', jsroute=True,
351 351 action='user_group_members', conditions={'method': ['GET']})
352 352
353 353 # ADMIN PERMISSIONS ROUTES
354 354 with rmap.submapper(path_prefix=ADMIN_PREFIX,
355 355 controller='admin/permissions') as m:
356 356 m.connect('admin_permissions_application', '/permissions/application',
357 357 action='permission_application_update', conditions={'method': ['POST']})
358 358 m.connect('admin_permissions_application', '/permissions/application',
359 359 action='permission_application', conditions={'method': ['GET']})
360 360
361 361 m.connect('admin_permissions_global', '/permissions/global',
362 362 action='permission_global_update', conditions={'method': ['POST']})
363 363 m.connect('admin_permissions_global', '/permissions/global',
364 364 action='permission_global', conditions={'method': ['GET']})
365 365
366 366 m.connect('admin_permissions_object', '/permissions/object',
367 367 action='permission_objects_update', conditions={'method': ['POST']})
368 368 m.connect('admin_permissions_object', '/permissions/object',
369 369 action='permission_objects', conditions={'method': ['GET']})
370 370
371 371 m.connect('admin_permissions_ips', '/permissions/ips',
372 372 action='permission_ips', conditions={'method': ['POST']})
373 373 m.connect('admin_permissions_ips', '/permissions/ips',
374 374 action='permission_ips', conditions={'method': ['GET']})
375 375
376 376 m.connect('admin_permissions_overview', '/permissions/overview',
377 377 action='permission_perms', conditions={'method': ['GET']})
378 378
379 379 # ADMIN DEFAULTS REST ROUTES
380 380 with rmap.submapper(path_prefix=ADMIN_PREFIX,
381 381 controller='admin/defaults') as m:
382 382 m.connect('admin_defaults_repositories', '/defaults/repositories',
383 383 action='update_repository_defaults', conditions={'method': ['POST']})
384 384 m.connect('admin_defaults_repositories', '/defaults/repositories',
385 385 action='index', conditions={'method': ['GET']})
386 386
387 387 # ADMIN DEBUG STYLE ROUTES
388 388 if str2bool(config.get('debug_style')):
389 389 with rmap.submapper(path_prefix=ADMIN_PREFIX + '/debug_style',
390 390 controller='debug_style') as m:
391 391 m.connect('debug_style_home', '',
392 392 action='index', conditions={'method': ['GET']})
393 393 m.connect('debug_style_template', '/t/{t_path}',
394 394 action='template', conditions={'method': ['GET']})
395 395
396 396 # ADMIN SETTINGS ROUTES
397 397 with rmap.submapper(path_prefix=ADMIN_PREFIX,
398 398 controller='admin/settings') as m:
399 399
400 400 # default
401 401 m.connect('admin_settings', '/settings',
402 402 action='settings_global_update',
403 403 conditions={'method': ['POST']})
404 404 m.connect('admin_settings', '/settings',
405 405 action='settings_global', conditions={'method': ['GET']})
406 406
407 407 m.connect('admin_settings_vcs', '/settings/vcs',
408 408 action='settings_vcs_update',
409 409 conditions={'method': ['POST']})
410 410 m.connect('admin_settings_vcs', '/settings/vcs',
411 411 action='settings_vcs',
412 412 conditions={'method': ['GET']})
413 413 m.connect('admin_settings_vcs', '/settings/vcs',
414 414 action='delete_svn_pattern',
415 415 conditions={'method': ['DELETE']})
416 416
417 417 m.connect('admin_settings_mapping', '/settings/mapping',
418 418 action='settings_mapping_update',
419 419 conditions={'method': ['POST']})
420 420 m.connect('admin_settings_mapping', '/settings/mapping',
421 421 action='settings_mapping', conditions={'method': ['GET']})
422 422
423 423 m.connect('admin_settings_global', '/settings/global',
424 424 action='settings_global_update',
425 425 conditions={'method': ['POST']})
426 426 m.connect('admin_settings_global', '/settings/global',
427 427 action='settings_global', conditions={'method': ['GET']})
428 428
429 429 m.connect('admin_settings_visual', '/settings/visual',
430 430 action='settings_visual_update',
431 431 conditions={'method': ['POST']})
432 432 m.connect('admin_settings_visual', '/settings/visual',
433 433 action='settings_visual', conditions={'method': ['GET']})
434 434
435 435 m.connect('admin_settings_issuetracker',
436 436 '/settings/issue-tracker', action='settings_issuetracker',
437 437 conditions={'method': ['GET']})
438 438 m.connect('admin_settings_issuetracker_save',
439 439 '/settings/issue-tracker/save',
440 440 action='settings_issuetracker_save',
441 441 conditions={'method': ['POST']})
442 442 m.connect('admin_issuetracker_test', '/settings/issue-tracker/test',
443 443 action='settings_issuetracker_test',
444 444 conditions={'method': ['POST']})
445 445 m.connect('admin_issuetracker_delete',
446 446 '/settings/issue-tracker/delete',
447 447 action='settings_issuetracker_delete',
448 448 conditions={'method': ['DELETE']})
449 449
450 450 m.connect('admin_settings_email', '/settings/email',
451 451 action='settings_email_update',
452 452 conditions={'method': ['POST']})
453 453 m.connect('admin_settings_email', '/settings/email',
454 454 action='settings_email', conditions={'method': ['GET']})
455 455
456 456 m.connect('admin_settings_hooks', '/settings/hooks',
457 457 action='settings_hooks_update',
458 458 conditions={'method': ['POST', 'DELETE']})
459 459 m.connect('admin_settings_hooks', '/settings/hooks',
460 460 action='settings_hooks', conditions={'method': ['GET']})
461 461
462 462 m.connect('admin_settings_search', '/settings/search',
463 463 action='settings_search', conditions={'method': ['GET']})
464 464
465 465 m.connect('admin_settings_supervisor', '/settings/supervisor',
466 466 action='settings_supervisor', conditions={'method': ['GET']})
467 467 m.connect('admin_settings_supervisor_log', '/settings/supervisor/{procid}/log',
468 468 action='settings_supervisor_log', conditions={'method': ['GET']})
469 469
470 470 m.connect('admin_settings_labs', '/settings/labs',
471 471 action='settings_labs_update',
472 472 conditions={'method': ['POST']})
473 473 m.connect('admin_settings_labs', '/settings/labs',
474 474 action='settings_labs', conditions={'method': ['GET']})
475 475
476 476 # ADMIN MY ACCOUNT
477 477 with rmap.submapper(path_prefix=ADMIN_PREFIX,
478 478 controller='admin/my_account') as m:
479 479
480 480 m.connect('my_account_edit', '/my_account/edit',
481 481 action='my_account_edit', conditions={'method': ['GET']})
482 482 m.connect('my_account', '/my_account/update',
483 483 action='my_account_update', conditions={'method': ['POST']})
484 484
485 485 # NOTE(marcink): this needs to be kept for password force flag to be
486 486 # handler, remove after migration to pyramid
487 487 m.connect('my_account_password', '/my_account/password',
488 488 action='my_account_password', conditions={'method': ['GET']})
489 489
490 490 m.connect('my_account_repos', '/my_account/repos',
491 491 action='my_account_repos', conditions={'method': ['GET']})
492 492
493 493 m.connect('my_account_watched', '/my_account/watched',
494 494 action='my_account_watched', conditions={'method': ['GET']})
495 495
496 496 m.connect('my_account_pullrequests', '/my_account/pull_requests',
497 497 action='my_account_pullrequests', conditions={'method': ['GET']})
498 498
499 499 m.connect('my_account_perms', '/my_account/perms',
500 500 action='my_account_perms', conditions={'method': ['GET']})
501 501
502 502 m.connect('my_account_emails', '/my_account/emails',
503 503 action='my_account_emails', conditions={'method': ['GET']})
504 504 m.connect('my_account_emails', '/my_account/emails',
505 505 action='my_account_emails_add', conditions={'method': ['POST']})
506 506 m.connect('my_account_emails', '/my_account/emails',
507 507 action='my_account_emails_delete', conditions={'method': ['DELETE']})
508 508
509 509 m.connect('my_account_notifications', '/my_account/notifications',
510 510 action='my_notifications',
511 511 conditions={'method': ['GET']})
512 512 m.connect('my_account_notifications_toggle_visibility',
513 513 '/my_account/toggle_visibility',
514 514 action='my_notifications_toggle_visibility',
515 515 conditions={'method': ['POST']})
516 516
517 517 # NOTIFICATION REST ROUTES
518 518 with rmap.submapper(path_prefix=ADMIN_PREFIX,
519 519 controller='admin/notifications') as m:
520 520 m.connect('notifications', '/notifications',
521 521 action='index', conditions={'method': ['GET']})
522 522 m.connect('notifications_mark_all_read', '/notifications/mark_all_read',
523 523 action='mark_all_read', conditions={'method': ['POST']})
524 524 m.connect('/notifications/{notification_id}',
525 525 action='update', conditions={'method': ['PUT']})
526 526 m.connect('/notifications/{notification_id}',
527 527 action='delete', conditions={'method': ['DELETE']})
528 528 m.connect('notification', '/notifications/{notification_id}',
529 529 action='show', conditions={'method': ['GET']})
530 530
531 531 # ADMIN GIST
532 532 with rmap.submapper(path_prefix=ADMIN_PREFIX,
533 533 controller='admin/gists') as m:
534 534 m.connect('gists', '/gists',
535 535 action='create', conditions={'method': ['POST']})
536 536 m.connect('gists', '/gists', jsroute=True,
537 537 action='index', conditions={'method': ['GET']})
538 538 m.connect('new_gist', '/gists/new', jsroute=True,
539 539 action='new', conditions={'method': ['GET']})
540 540
541 541 m.connect('/gists/{gist_id}',
542 542 action='delete', conditions={'method': ['DELETE']})
543 543 m.connect('edit_gist', '/gists/{gist_id}/edit',
544 544 action='edit_form', conditions={'method': ['GET']})
545 545 m.connect('edit_gist', '/gists/{gist_id}/edit',
546 546 action='edit', conditions={'method': ['POST']})
547 547 m.connect(
548 548 'edit_gist_check_revision', '/gists/{gist_id}/edit/check_revision',
549 549 action='check_revision', conditions={'method': ['GET']})
550 550
551 551 m.connect('gist', '/gists/{gist_id}',
552 552 action='show', conditions={'method': ['GET']})
553 553 m.connect('gist_rev', '/gists/{gist_id}/{revision}',
554 554 revision='tip',
555 555 action='show', conditions={'method': ['GET']})
556 556 m.connect('formatted_gist', '/gists/{gist_id}/{revision}/{format}',
557 557 revision='tip',
558 558 action='show', conditions={'method': ['GET']})
559 559 m.connect('formatted_gist_file', '/gists/{gist_id}/{revision}/{format}/{f_path}',
560 560 revision='tip',
561 561 action='show', conditions={'method': ['GET']},
562 562 requirements=URL_NAME_REQUIREMENTS)
563 563
564 564 # USER JOURNAL
565 565 rmap.connect('journal', '%s/journal' % (ADMIN_PREFIX,),
566 566 controller='journal', action='index')
567 567 rmap.connect('journal_rss', '%s/journal/rss' % (ADMIN_PREFIX,),
568 568 controller='journal', action='journal_rss')
569 569 rmap.connect('journal_atom', '%s/journal/atom' % (ADMIN_PREFIX,),
570 570 controller='journal', action='journal_atom')
571 571
572 572 rmap.connect('public_journal', '%s/public_journal' % (ADMIN_PREFIX,),
573 573 controller='journal', action='public_journal')
574 574
575 575 rmap.connect('public_journal_rss', '%s/public_journal/rss' % (ADMIN_PREFIX,),
576 576 controller='journal', action='public_journal_rss')
577 577
578 578 rmap.connect('public_journal_rss_old', '%s/public_journal_rss' % (ADMIN_PREFIX,),
579 579 controller='journal', action='public_journal_rss')
580 580
581 581 rmap.connect('public_journal_atom',
582 582 '%s/public_journal/atom' % (ADMIN_PREFIX,), controller='journal',
583 583 action='public_journal_atom')
584 584
585 585 rmap.connect('public_journal_atom_old',
586 586 '%s/public_journal_atom' % (ADMIN_PREFIX,), controller='journal',
587 587 action='public_journal_atom')
588 588
589 589 rmap.connect('toggle_following', '%s/toggle_following' % (ADMIN_PREFIX,),
590 590 controller='journal', action='toggle_following', jsroute=True,
591 591 conditions={'method': ['POST']})
592 592
593 593 # FEEDS
594 594 rmap.connect('rss_feed_home', '/{repo_name}/feed/rss',
595 595 controller='feed', action='rss',
596 596 conditions={'function': check_repo},
597 597 requirements=URL_NAME_REQUIREMENTS)
598 598
599 599 rmap.connect('atom_feed_home', '/{repo_name}/feed/atom',
600 600 controller='feed', action='atom',
601 601 conditions={'function': check_repo},
602 602 requirements=URL_NAME_REQUIREMENTS)
603 603
604 604 #==========================================================================
605 605 # REPOSITORY ROUTES
606 606 #==========================================================================
607 607
608 608 rmap.connect('repo_creating_home', '/{repo_name}/repo_creating',
609 609 controller='admin/repos', action='repo_creating',
610 610 requirements=URL_NAME_REQUIREMENTS)
611 611 rmap.connect('repo_check_home', '/{repo_name}/crepo_check',
612 612 controller='admin/repos', action='repo_check',
613 613 requirements=URL_NAME_REQUIREMENTS)
614 614
615 615 rmap.connect('repo_stats', '/{repo_name}/repo_stats/{commit_id}',
616 616 controller='summary', action='repo_stats',
617 617 conditions={'function': check_repo},
618 618 requirements=URL_NAME_REQUIREMENTS, jsroute=True)
619 619
620 620 rmap.connect('repo_refs_data', '/{repo_name}/refs-data',
621 621 controller='summary', action='repo_refs_data',
622 622 requirements=URL_NAME_REQUIREMENTS, jsroute=True)
623 623 rmap.connect('repo_refs_changelog_data', '/{repo_name}/refs-data-changelog',
624 624 controller='summary', action='repo_refs_changelog_data',
625 625 requirements=URL_NAME_REQUIREMENTS, jsroute=True)
626 626 rmap.connect('repo_default_reviewers_data', '/{repo_name}/default-reviewers',
627 627 controller='summary', action='repo_default_reviewers_data',
628 628 jsroute=True, requirements=URL_NAME_REQUIREMENTS)
629 629
630 630 rmap.connect('changeset_home', '/{repo_name}/changeset/{revision}',
631 631 controller='changeset', revision='tip',
632 632 conditions={'function': check_repo},
633 633 requirements=URL_NAME_REQUIREMENTS, jsroute=True)
634 634 rmap.connect('changeset_children', '/{repo_name}/changeset_children/{revision}',
635 635 controller='changeset', revision='tip', action='changeset_children',
636 636 conditions={'function': check_repo},
637 637 requirements=URL_NAME_REQUIREMENTS)
638 638 rmap.connect('changeset_parents', '/{repo_name}/changeset_parents/{revision}',
639 639 controller='changeset', revision='tip', action='changeset_parents',
640 640 conditions={'function': check_repo},
641 641 requirements=URL_NAME_REQUIREMENTS)
642 642
643 643 # repo edit options
644 644 rmap.connect('edit_repo_fields', '/{repo_name}/settings/fields',
645 645 controller='admin/repos', action='edit_fields',
646 646 conditions={'method': ['GET'], 'function': check_repo},
647 647 requirements=URL_NAME_REQUIREMENTS)
648 648 rmap.connect('create_repo_fields', '/{repo_name}/settings/fields/new',
649 649 controller='admin/repos', action='create_repo_field',
650 650 conditions={'method': ['PUT'], 'function': check_repo},
651 651 requirements=URL_NAME_REQUIREMENTS)
652 652 rmap.connect('delete_repo_fields', '/{repo_name}/settings/fields/{field_id}',
653 653 controller='admin/repos', action='delete_repo_field',
654 654 conditions={'method': ['DELETE'], 'function': check_repo},
655 655 requirements=URL_NAME_REQUIREMENTS)
656 656
657 657 rmap.connect('toggle_locking', '/{repo_name}/settings/advanced/locking_toggle',
658 658 controller='admin/repos', action='toggle_locking',
659 659 conditions={'method': ['GET'], 'function': check_repo},
660 660 requirements=URL_NAME_REQUIREMENTS)
661 661
662 662 rmap.connect('edit_repo_remote', '/{repo_name}/settings/remote',
663 663 controller='admin/repos', action='edit_remote_form',
664 664 conditions={'method': ['GET'], 'function': check_repo},
665 665 requirements=URL_NAME_REQUIREMENTS)
666 666 rmap.connect('edit_repo_remote', '/{repo_name}/settings/remote',
667 667 controller='admin/repos', action='edit_remote',
668 668 conditions={'method': ['PUT'], 'function': check_repo},
669 669 requirements=URL_NAME_REQUIREMENTS)
670 670
671 671 rmap.connect('edit_repo_statistics', '/{repo_name}/settings/statistics',
672 672 controller='admin/repos', action='edit_statistics_form',
673 673 conditions={'method': ['GET'], 'function': check_repo},
674 674 requirements=URL_NAME_REQUIREMENTS)
675 675 rmap.connect('edit_repo_statistics', '/{repo_name}/settings/statistics',
676 676 controller='admin/repos', action='edit_statistics',
677 677 conditions={'method': ['PUT'], 'function': check_repo},
678 678 requirements=URL_NAME_REQUIREMENTS)
679 679 rmap.connect('repo_settings_issuetracker',
680 680 '/{repo_name}/settings/issue-tracker',
681 681 controller='admin/repos', action='repo_issuetracker',
682 682 conditions={'method': ['GET'], 'function': check_repo},
683 683 requirements=URL_NAME_REQUIREMENTS)
684 684 rmap.connect('repo_issuetracker_test',
685 685 '/{repo_name}/settings/issue-tracker/test',
686 686 controller='admin/repos', action='repo_issuetracker_test',
687 687 conditions={'method': ['POST'], 'function': check_repo},
688 688 requirements=URL_NAME_REQUIREMENTS)
689 689 rmap.connect('repo_issuetracker_delete',
690 690 '/{repo_name}/settings/issue-tracker/delete',
691 691 controller='admin/repos', action='repo_issuetracker_delete',
692 692 conditions={'method': ['DELETE'], 'function': check_repo},
693 693 requirements=URL_NAME_REQUIREMENTS)
694 694 rmap.connect('repo_issuetracker_save',
695 695 '/{repo_name}/settings/issue-tracker/save',
696 696 controller='admin/repos', action='repo_issuetracker_save',
697 697 conditions={'method': ['POST'], 'function': check_repo},
698 698 requirements=URL_NAME_REQUIREMENTS)
699 699 rmap.connect('repo_vcs_settings', '/{repo_name}/settings/vcs',
700 700 controller='admin/repos', action='repo_settings_vcs_update',
701 701 conditions={'method': ['POST'], 'function': check_repo},
702 702 requirements=URL_NAME_REQUIREMENTS)
703 703 rmap.connect('repo_vcs_settings', '/{repo_name}/settings/vcs',
704 704 controller='admin/repos', action='repo_settings_vcs',
705 705 conditions={'method': ['GET'], 'function': check_repo},
706 706 requirements=URL_NAME_REQUIREMENTS)
707 707 rmap.connect('repo_vcs_settings', '/{repo_name}/settings/vcs',
708 708 controller='admin/repos', action='repo_delete_svn_pattern',
709 709 conditions={'method': ['DELETE'], 'function': check_repo},
710 710 requirements=URL_NAME_REQUIREMENTS)
711 711 rmap.connect('repo_pullrequest_settings', '/{repo_name}/settings/pullrequest',
712 712 controller='admin/repos', action='repo_settings_pullrequest',
713 713 conditions={'method': ['GET', 'POST'], 'function': check_repo},
714 714 requirements=URL_NAME_REQUIREMENTS)
715 715
716 716 # still working url for backward compat.
717 717 rmap.connect('raw_changeset_home_depraced',
718 718 '/{repo_name}/raw-changeset/{revision}',
719 719 controller='changeset', action='changeset_raw',
720 720 revision='tip', conditions={'function': check_repo},
721 721 requirements=URL_NAME_REQUIREMENTS)
722 722
723 723 # new URLs
724 724 rmap.connect('changeset_raw_home',
725 725 '/{repo_name}/changeset-diff/{revision}',
726 726 controller='changeset', action='changeset_raw',
727 727 revision='tip', conditions={'function': check_repo},
728 728 requirements=URL_NAME_REQUIREMENTS)
729 729
730 730 rmap.connect('changeset_patch_home',
731 731 '/{repo_name}/changeset-patch/{revision}',
732 732 controller='changeset', action='changeset_patch',
733 733 revision='tip', conditions={'function': check_repo},
734 734 requirements=URL_NAME_REQUIREMENTS)
735 735
736 736 rmap.connect('changeset_download_home',
737 737 '/{repo_name}/changeset-download/{revision}',
738 738 controller='changeset', action='changeset_download',
739 739 revision='tip', conditions={'function': check_repo},
740 740 requirements=URL_NAME_REQUIREMENTS)
741 741
742 742 rmap.connect('changeset_comment',
743 743 '/{repo_name}/changeset/{revision}/comment', jsroute=True,
744 744 controller='changeset', revision='tip', action='comment',
745 745 conditions={'function': check_repo},
746 746 requirements=URL_NAME_REQUIREMENTS)
747 747
748 748 rmap.connect('changeset_comment_preview',
749 749 '/{repo_name}/changeset/comment/preview', jsroute=True,
750 750 controller='changeset', action='preview_comment',
751 751 conditions={'function': check_repo, 'method': ['POST']},
752 752 requirements=URL_NAME_REQUIREMENTS)
753 753
754 754 rmap.connect('changeset_comment_delete',
755 755 '/{repo_name}/changeset/comment/{comment_id}/delete',
756 756 controller='changeset', action='delete_comment',
757 757 conditions={'function': check_repo, 'method': ['DELETE']},
758 758 requirements=URL_NAME_REQUIREMENTS, jsroute=True)
759 759
760 760 rmap.connect('changeset_info', '/{repo_name}/changeset_info/{revision}',
761 761 controller='changeset', action='changeset_info',
762 762 requirements=URL_NAME_REQUIREMENTS, jsroute=True)
763 763
764 764 rmap.connect('compare_home',
765 765 '/{repo_name}/compare',
766 766 controller='compare', action='index',
767 767 conditions={'function': check_repo},
768 768 requirements=URL_NAME_REQUIREMENTS)
769 769
770 770 rmap.connect('compare_url',
771 771 '/{repo_name}/compare/{source_ref_type}@{source_ref:.*?}...{target_ref_type}@{target_ref:.*?}',
772 772 controller='compare', action='compare',
773 773 conditions={'function': check_repo},
774 774 requirements=URL_NAME_REQUIREMENTS, jsroute=True)
775 775
776 776 rmap.connect('pullrequest_home',
777 777 '/{repo_name}/pull-request/new', controller='pullrequests',
778 778 action='index', conditions={'function': check_repo,
779 779 'method': ['GET']},
780 780 requirements=URL_NAME_REQUIREMENTS, jsroute=True)
781 781
782 782 rmap.connect('pullrequest',
783 783 '/{repo_name}/pull-request/new', controller='pullrequests',
784 784 action='create', conditions={'function': check_repo,
785 785 'method': ['POST']},
786 786 requirements=URL_NAME_REQUIREMENTS, jsroute=True)
787 787
788 788 rmap.connect('pullrequest_repo_refs',
789 789 '/{repo_name}/pull-request/refs/{target_repo_name:.*?[^/]}',
790 790 controller='pullrequests',
791 791 action='get_repo_refs',
792 792 conditions={'function': check_repo, 'method': ['GET']},
793 793 requirements=URL_NAME_REQUIREMENTS, jsroute=True)
794 794
795 795 rmap.connect('pullrequest_repo_destinations',
796 796 '/{repo_name}/pull-request/repo-destinations',
797 797 controller='pullrequests',
798 798 action='get_repo_destinations',
799 799 conditions={'function': check_repo, 'method': ['GET']},
800 800 requirements=URL_NAME_REQUIREMENTS, jsroute=True)
801 801
802 802 rmap.connect('pullrequest_show',
803 803 '/{repo_name}/pull-request/{pull_request_id}',
804 804 controller='pullrequests',
805 805 action='show', conditions={'function': check_repo,
806 806 'method': ['GET']},
807 807 requirements=URL_NAME_REQUIREMENTS, jsroute=True)
808 808
809 809 rmap.connect('pullrequest_update',
810 810 '/{repo_name}/pull-request/{pull_request_id}',
811 811 controller='pullrequests',
812 812 action='update', conditions={'function': check_repo,
813 813 'method': ['PUT']},
814 814 requirements=URL_NAME_REQUIREMENTS, jsroute=True)
815 815
816 816 rmap.connect('pullrequest_merge',
817 817 '/{repo_name}/pull-request/{pull_request_id}',
818 818 controller='pullrequests',
819 819 action='merge', conditions={'function': check_repo,
820 820 'method': ['POST']},
821 821 requirements=URL_NAME_REQUIREMENTS)
822 822
823 823 rmap.connect('pullrequest_delete',
824 824 '/{repo_name}/pull-request/{pull_request_id}',
825 825 controller='pullrequests',
826 826 action='delete', conditions={'function': check_repo,
827 827 'method': ['DELETE']},
828 828 requirements=URL_NAME_REQUIREMENTS)
829 829
830 rmap.connect('pullrequest_show_all',
831 '/{repo_name}/pull-request',
832 controller='pullrequests',
833 action='show_all', conditions={'function': check_repo,
834 'method': ['GET']},
835 requirements=URL_NAME_REQUIREMENTS, jsroute=True)
836
837 830 rmap.connect('pullrequest_comment',
838 831 '/{repo_name}/pull-request-comment/{pull_request_id}',
839 832 controller='pullrequests',
840 833 action='comment', conditions={'function': check_repo,
841 834 'method': ['POST']},
842 835 requirements=URL_NAME_REQUIREMENTS, jsroute=True)
843 836
844 837 rmap.connect('pullrequest_comment_delete',
845 838 '/{repo_name}/pull-request-comment/{comment_id}/delete',
846 839 controller='pullrequests', action='delete_comment',
847 840 conditions={'function': check_repo, 'method': ['DELETE']},
848 841 requirements=URL_NAME_REQUIREMENTS, jsroute=True)
849 842
850 843 rmap.connect('summary_home_explicit', '/{repo_name}/summary',
851 844 controller='summary', conditions={'function': check_repo},
852 845 requirements=URL_NAME_REQUIREMENTS)
853 846
854 847 rmap.connect('changelog_home', '/{repo_name}/changelog', jsroute=True,
855 848 controller='changelog', conditions={'function': check_repo},
856 849 requirements=URL_NAME_REQUIREMENTS)
857 850
858 851 rmap.connect('changelog_summary_home', '/{repo_name}/changelog_summary',
859 852 controller='changelog', action='changelog_summary',
860 853 conditions={'function': check_repo},
861 854 requirements=URL_NAME_REQUIREMENTS)
862 855
863 856 rmap.connect('changelog_file_home',
864 857 '/{repo_name}/changelog/{revision}/{f_path}',
865 858 controller='changelog', f_path=None,
866 859 conditions={'function': check_repo},
867 860 requirements=URL_NAME_REQUIREMENTS, jsroute=True)
868 861
869 862 rmap.connect('changelog_elements', '/{repo_name}/changelog_details',
870 863 controller='changelog', action='changelog_elements',
871 864 conditions={'function': check_repo},
872 865 requirements=URL_NAME_REQUIREMENTS, jsroute=True)
873 866
874 867 rmap.connect('files_home', '/{repo_name}/files/{revision}/{f_path}',
875 868 controller='files', revision='tip', f_path='',
876 869 conditions={'function': check_repo},
877 870 requirements=URL_NAME_REQUIREMENTS, jsroute=True)
878 871
879 872 rmap.connect('files_home_simple_catchrev',
880 873 '/{repo_name}/files/{revision}',
881 874 controller='files', revision='tip', f_path='',
882 875 conditions={'function': check_repo},
883 876 requirements=URL_NAME_REQUIREMENTS)
884 877
885 878 rmap.connect('files_home_simple_catchall',
886 879 '/{repo_name}/files',
887 880 controller='files', revision='tip', f_path='',
888 881 conditions={'function': check_repo},
889 882 requirements=URL_NAME_REQUIREMENTS)
890 883
891 884 rmap.connect('files_history_home',
892 885 '/{repo_name}/history/{revision}/{f_path}',
893 886 controller='files', action='history', revision='tip', f_path='',
894 887 conditions={'function': check_repo},
895 888 requirements=URL_NAME_REQUIREMENTS, jsroute=True)
896 889
897 890 rmap.connect('files_authors_home',
898 891 '/{repo_name}/authors/{revision}/{f_path}',
899 892 controller='files', action='authors', revision='tip', f_path='',
900 893 conditions={'function': check_repo},
901 894 requirements=URL_NAME_REQUIREMENTS, jsroute=True)
902 895
903 896 rmap.connect('files_diff_home', '/{repo_name}/diff/{f_path}',
904 897 controller='files', action='diff', f_path='',
905 898 conditions={'function': check_repo},
906 899 requirements=URL_NAME_REQUIREMENTS)
907 900
908 901 rmap.connect('files_diff_2way_home',
909 902 '/{repo_name}/diff-2way/{f_path}',
910 903 controller='files', action='diff_2way', f_path='',
911 904 conditions={'function': check_repo},
912 905 requirements=URL_NAME_REQUIREMENTS)
913 906
914 907 rmap.connect('files_rawfile_home',
915 908 '/{repo_name}/rawfile/{revision}/{f_path}',
916 909 controller='files', action='rawfile', revision='tip',
917 910 f_path='', conditions={'function': check_repo},
918 911 requirements=URL_NAME_REQUIREMENTS)
919 912
920 913 rmap.connect('files_raw_home',
921 914 '/{repo_name}/raw/{revision}/{f_path}',
922 915 controller='files', action='raw', revision='tip', f_path='',
923 916 conditions={'function': check_repo},
924 917 requirements=URL_NAME_REQUIREMENTS)
925 918
926 919 rmap.connect('files_render_home',
927 920 '/{repo_name}/render/{revision}/{f_path}',
928 921 controller='files', action='index', revision='tip', f_path='',
929 922 rendered=True, conditions={'function': check_repo},
930 923 requirements=URL_NAME_REQUIREMENTS)
931 924
932 925 rmap.connect('files_annotate_home',
933 926 '/{repo_name}/annotate/{revision}/{f_path}',
934 927 controller='files', action='index', revision='tip',
935 928 f_path='', annotate=True, conditions={'function': check_repo},
936 929 requirements=URL_NAME_REQUIREMENTS, jsroute=True)
937 930
938 931 rmap.connect('files_annotate_previous',
939 932 '/{repo_name}/annotate-previous/{revision}/{f_path}',
940 933 controller='files', action='annotate_previous', revision='tip',
941 934 f_path='', annotate=True, conditions={'function': check_repo},
942 935 requirements=URL_NAME_REQUIREMENTS, jsroute=True)
943 936
944 937 rmap.connect('files_edit',
945 938 '/{repo_name}/edit/{revision}/{f_path}',
946 939 controller='files', action='edit', revision='tip',
947 940 f_path='',
948 941 conditions={'function': check_repo, 'method': ['POST']},
949 942 requirements=URL_NAME_REQUIREMENTS)
950 943
951 944 rmap.connect('files_edit_home',
952 945 '/{repo_name}/edit/{revision}/{f_path}',
953 946 controller='files', action='edit_home', revision='tip',
954 947 f_path='', conditions={'function': check_repo},
955 948 requirements=URL_NAME_REQUIREMENTS)
956 949
957 950 rmap.connect('files_add',
958 951 '/{repo_name}/add/{revision}/{f_path}',
959 952 controller='files', action='add', revision='tip',
960 953 f_path='',
961 954 conditions={'function': check_repo, 'method': ['POST']},
962 955 requirements=URL_NAME_REQUIREMENTS)
963 956
964 957 rmap.connect('files_add_home',
965 958 '/{repo_name}/add/{revision}/{f_path}',
966 959 controller='files', action='add_home', revision='tip',
967 960 f_path='', conditions={'function': check_repo},
968 961 requirements=URL_NAME_REQUIREMENTS)
969 962
970 963 rmap.connect('files_delete',
971 964 '/{repo_name}/delete/{revision}/{f_path}',
972 965 controller='files', action='delete', revision='tip',
973 966 f_path='',
974 967 conditions={'function': check_repo, 'method': ['POST']},
975 968 requirements=URL_NAME_REQUIREMENTS)
976 969
977 970 rmap.connect('files_delete_home',
978 971 '/{repo_name}/delete/{revision}/{f_path}',
979 972 controller='files', action='delete_home', revision='tip',
980 973 f_path='', conditions={'function': check_repo},
981 974 requirements=URL_NAME_REQUIREMENTS)
982 975
983 976 rmap.connect('files_archive_home', '/{repo_name}/archive/{fname}',
984 977 controller='files', action='archivefile',
985 978 conditions={'function': check_repo},
986 979 requirements=URL_NAME_REQUIREMENTS, jsroute=True)
987 980
988 981 rmap.connect('files_nodelist_home',
989 982 '/{repo_name}/nodelist/{revision}/{f_path}',
990 983 controller='files', action='nodelist',
991 984 conditions={'function': check_repo},
992 985 requirements=URL_NAME_REQUIREMENTS, jsroute=True)
993 986
994 987 rmap.connect('files_nodetree_full',
995 988 '/{repo_name}/nodetree_full/{commit_id}/{f_path}',
996 989 controller='files', action='nodetree_full',
997 990 conditions={'function': check_repo},
998 991 requirements=URL_NAME_REQUIREMENTS, jsroute=True)
999 992
1000 993 rmap.connect('repo_fork_create_home', '/{repo_name}/fork',
1001 994 controller='forks', action='fork_create',
1002 995 conditions={'function': check_repo, 'method': ['POST']},
1003 996 requirements=URL_NAME_REQUIREMENTS)
1004 997
1005 998 rmap.connect('repo_fork_home', '/{repo_name}/fork',
1006 999 controller='forks', action='fork',
1007 1000 conditions={'function': check_repo},
1008 1001 requirements=URL_NAME_REQUIREMENTS)
1009 1002
1010 1003 rmap.connect('repo_forks_home', '/{repo_name}/forks',
1011 1004 controller='forks', action='forks',
1012 1005 conditions={'function': check_repo},
1013 1006 requirements=URL_NAME_REQUIREMENTS)
1014 1007
1015 1008 # must be here for proper group/repo catching pattern
1016 1009 _connect_with_slash(
1017 1010 rmap, 'repo_group_home', '/{group_name}',
1018 1011 controller='home', action='index_repo_group',
1019 1012 conditions={'function': check_group},
1020 1013 requirements=URL_NAME_REQUIREMENTS)
1021 1014
1022 1015 # catch all, at the end
1023 1016 _connect_with_slash(
1024 1017 rmap, 'summary_home', '/{repo_name}', jsroute=True,
1025 1018 controller='summary', action='index',
1026 1019 conditions={'function': check_repo},
1027 1020 requirements=URL_NAME_REQUIREMENTS)
1028 1021
1029 1022 return rmap
1030 1023
1031 1024
1032 1025 def _connect_with_slash(mapper, name, path, *args, **kwargs):
1033 1026 """
1034 1027 Connect a route with an optional trailing slash in `path`.
1035 1028 """
1036 1029 mapper.connect(name + '_slash', path + '/', *args, **kwargs)
1037 1030 mapper.connect(name, path, *args, **kwargs)
@@ -1,1096 +1,978 b''
1 1 # -*- coding: utf-8 -*-
2 2
3 3 # Copyright (C) 2012-2017 RhodeCode GmbH
4 4 #
5 5 # This program is free software: you can redistribute it and/or modify
6 6 # it under the terms of the GNU Affero General Public License, version 3
7 7 # (only), as published by the Free Software Foundation.
8 8 #
9 9 # This program is distributed in the hope that it will be useful,
10 10 # but WITHOUT ANY WARRANTY; without even the implied warranty of
11 11 # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
12 12 # GNU General Public License for more details.
13 13 #
14 14 # You should have received a copy of the GNU Affero General Public License
15 15 # along with this program. If not, see <http://www.gnu.org/licenses/>.
16 16 #
17 17 # This program is dual-licensed. If you wish to learn more about the
18 18 # RhodeCode Enterprise Edition, including its added features, Support services,
19 19 # and proprietary license terms, please see https://rhodecode.com/licenses/
20 20
21 21 """
22 22 pull requests controller for rhodecode for initializing pull requests
23 23 """
24 24 import types
25 25
26 26 import peppercorn
27 27 import formencode
28 28 import logging
29 29 import collections
30 30
31 31 from webob.exc import HTTPNotFound, HTTPForbidden, HTTPBadRequest
32 32 from pylons import request, tmpl_context as c, url
33 33 from pylons.controllers.util import redirect
34 34 from pylons.i18n.translation import _
35 35 from pyramid.threadlocal import get_current_registry
36 36 from sqlalchemy.sql import func
37 37 from sqlalchemy.sql.expression import or_
38 38
39 39 from rhodecode import events
40 40 from rhodecode.lib import auth, diffs, helpers as h, codeblocks
41 41 from rhodecode.lib.ext_json import json
42 42 from rhodecode.lib.base import (
43 43 BaseRepoController, render, vcs_operation_context)
44 44 from rhodecode.lib.auth import (
45 45 LoginRequired, HasRepoPermissionAnyDecorator, NotAnonymous,
46 46 HasAcceptedRepoType, XHRRequired)
47 47 from rhodecode.lib.channelstream import channelstream_request
48 48 from rhodecode.lib.utils import jsonify
49 49 from rhodecode.lib.utils2 import (
50 50 safe_int, safe_str, str2bool, safe_unicode)
51 51 from rhodecode.lib.vcs.backends.base import (
52 52 EmptyCommit, UpdateFailureReason, EmptyRepository)
53 53 from rhodecode.lib.vcs.exceptions import (
54 54 EmptyRepositoryError, CommitDoesNotExistError, RepositoryRequirementError,
55 55 NodeDoesNotExistError)
56 56
57 57 from rhodecode.model.changeset_status import ChangesetStatusModel
58 58 from rhodecode.model.comment import CommentsModel
59 59 from rhodecode.model.db import (PullRequest, ChangesetStatus, ChangesetComment,
60 60 Repository, PullRequestVersion)
61 61 from rhodecode.model.forms import PullRequestForm
62 62 from rhodecode.model.meta import Session
63 63 from rhodecode.model.pull_request import PullRequestModel, MergeCheck
64 64
65 65 log = logging.getLogger(__name__)
66 66
67 67
68 68 class PullrequestsController(BaseRepoController):
69 69
70 70 def __before__(self):
71 71 super(PullrequestsController, self).__before__()
72 72 c.REVIEW_STATUS_APPROVED = ChangesetStatus.STATUS_APPROVED
73 73 c.REVIEW_STATUS_REJECTED = ChangesetStatus.STATUS_REJECTED
74 74
75 def _extract_ordering(self, request):
76 column_index = safe_int(request.GET.get('order[0][column]'))
77 order_dir = request.GET.get('order[0][dir]', 'desc')
78 order_by = request.GET.get(
79 'columns[%s][data][sort]' % column_index, 'name_raw')
80 return order_by, order_dir
81
82 @LoginRequired()
83 @HasRepoPermissionAnyDecorator('repository.read', 'repository.write',
84 'repository.admin')
85 @HasAcceptedRepoType('git', 'hg')
86 def show_all(self, repo_name):
87 # filter types
88 c.active = 'open'
89 c.source = str2bool(request.GET.get('source'))
90 c.closed = str2bool(request.GET.get('closed'))
91 c.my = str2bool(request.GET.get('my'))
92 c.awaiting_review = str2bool(request.GET.get('awaiting_review'))
93 c.awaiting_my_review = str2bool(request.GET.get('awaiting_my_review'))
94 c.repo_name = repo_name
95
96 opened_by = None
97 if c.my:
98 c.active = 'my'
99 opened_by = [c.rhodecode_user.user_id]
100
101 statuses = [PullRequest.STATUS_NEW, PullRequest.STATUS_OPEN]
102 if c.closed:
103 c.active = 'closed'
104 statuses = [PullRequest.STATUS_CLOSED]
105
106 if c.awaiting_review and not c.source:
107 c.active = 'awaiting'
108 if c.source and not c.awaiting_review:
109 c.active = 'source'
110 if c.awaiting_my_review:
111 c.active = 'awaiting_my'
112
113 data = self._get_pull_requests_list(
114 repo_name=repo_name, opened_by=opened_by, statuses=statuses)
115 if not request.is_xhr:
116 c.data = json.dumps(data['data'])
117 c.records_total = data['recordsTotal']
118 return render('/pullrequests/pullrequests.mako')
119 else:
120 return json.dumps(data)
121
122 def _get_pull_requests_list(self, repo_name, opened_by, statuses):
123 # pagination
124 start = safe_int(request.GET.get('start'), 0)
125 length = safe_int(request.GET.get('length'), c.visual.dashboard_items)
126 order_by, order_dir = self._extract_ordering(request)
127
128 if c.awaiting_review:
129 pull_requests = PullRequestModel().get_awaiting_review(
130 repo_name, source=c.source, opened_by=opened_by,
131 statuses=statuses, offset=start, length=length,
132 order_by=order_by, order_dir=order_dir)
133 pull_requests_total_count = PullRequestModel(
134 ).count_awaiting_review(
135 repo_name, source=c.source, statuses=statuses,
136 opened_by=opened_by)
137 elif c.awaiting_my_review:
138 pull_requests = PullRequestModel().get_awaiting_my_review(
139 repo_name, source=c.source, opened_by=opened_by,
140 user_id=c.rhodecode_user.user_id, statuses=statuses,
141 offset=start, length=length, order_by=order_by,
142 order_dir=order_dir)
143 pull_requests_total_count = PullRequestModel(
144 ).count_awaiting_my_review(
145 repo_name, source=c.source, user_id=c.rhodecode_user.user_id,
146 statuses=statuses, opened_by=opened_by)
147 else:
148 pull_requests = PullRequestModel().get_all(
149 repo_name, source=c.source, opened_by=opened_by,
150 statuses=statuses, offset=start, length=length,
151 order_by=order_by, order_dir=order_dir)
152 pull_requests_total_count = PullRequestModel().count_all(
153 repo_name, source=c.source, statuses=statuses,
154 opened_by=opened_by)
155
156 from rhodecode.lib.utils import PartialRenderer
157 _render = PartialRenderer('data_table/_dt_elements.mako')
158 data = []
159 for pr in pull_requests:
160 comments = CommentsModel().get_all_comments(
161 c.rhodecode_db_repo.repo_id, pull_request=pr)
162
163 data.append({
164 'name': _render('pullrequest_name',
165 pr.pull_request_id, pr.target_repo.repo_name),
166 'name_raw': pr.pull_request_id,
167 'status': _render('pullrequest_status',
168 pr.calculated_review_status()),
169 'title': _render(
170 'pullrequest_title', pr.title, pr.description),
171 'description': h.escape(pr.description),
172 'updated_on': _render('pullrequest_updated_on',
173 h.datetime_to_time(pr.updated_on)),
174 'updated_on_raw': h.datetime_to_time(pr.updated_on),
175 'created_on': _render('pullrequest_updated_on',
176 h.datetime_to_time(pr.created_on)),
177 'created_on_raw': h.datetime_to_time(pr.created_on),
178 'author': _render('pullrequest_author',
179 pr.author.full_contact, ),
180 'author_raw': pr.author.full_name,
181 'comments': _render('pullrequest_comments', len(comments)),
182 'comments_raw': len(comments),
183 'closed': pr.is_closed(),
184 })
185 # json used to render the grid
186 data = ({
187 'data': data,
188 'recordsTotal': pull_requests_total_count,
189 'recordsFiltered': pull_requests_total_count,
190 })
191 return data
192
193 75 @LoginRequired()
194 76 @NotAnonymous()
195 77 @HasRepoPermissionAnyDecorator('repository.read', 'repository.write',
196 78 'repository.admin')
197 79 @HasAcceptedRepoType('git', 'hg')
198 80 def index(self):
199 81 source_repo = c.rhodecode_db_repo
200 82
201 83 try:
202 84 source_repo.scm_instance().get_commit()
203 85 except EmptyRepositoryError:
204 86 h.flash(h.literal(_('There are no commits yet')),
205 87 category='warning')
206 88 redirect(url('summary_home', repo_name=source_repo.repo_name))
207 89
208 90 commit_id = request.GET.get('commit')
209 91 branch_ref = request.GET.get('branch')
210 92 bookmark_ref = request.GET.get('bookmark')
211 93
212 94 try:
213 95 source_repo_data = PullRequestModel().generate_repo_data(
214 96 source_repo, commit_id=commit_id,
215 97 branch=branch_ref, bookmark=bookmark_ref)
216 98 except CommitDoesNotExistError as e:
217 99 log.exception(e)
218 100 h.flash(_('Commit does not exist'), 'error')
219 101 redirect(url('pullrequest_home', repo_name=source_repo.repo_name))
220 102
221 103 default_target_repo = source_repo
222 104
223 105 if source_repo.parent:
224 106 parent_vcs_obj = source_repo.parent.scm_instance()
225 107 if parent_vcs_obj and not parent_vcs_obj.is_empty():
226 108 # change default if we have a parent repo
227 109 default_target_repo = source_repo.parent
228 110
229 111 target_repo_data = PullRequestModel().generate_repo_data(
230 112 default_target_repo)
231 113
232 114 selected_source_ref = source_repo_data['refs']['selected_ref']
233 115
234 116 title_source_ref = selected_source_ref.split(':', 2)[1]
235 117 c.default_title = PullRequestModel().generate_pullrequest_title(
236 118 source=source_repo.repo_name,
237 119 source_ref=title_source_ref,
238 120 target=default_target_repo.repo_name
239 121 )
240 122
241 123 c.default_repo_data = {
242 124 'source_repo_name': source_repo.repo_name,
243 125 'source_refs_json': json.dumps(source_repo_data),
244 126 'target_repo_name': default_target_repo.repo_name,
245 127 'target_refs_json': json.dumps(target_repo_data),
246 128 }
247 129 c.default_source_ref = selected_source_ref
248 130
249 131 return render('/pullrequests/pullrequest.mako')
250 132
251 133 @LoginRequired()
252 134 @NotAnonymous()
253 135 @XHRRequired()
254 136 @HasRepoPermissionAnyDecorator('repository.read', 'repository.write',
255 137 'repository.admin')
256 138 @jsonify
257 139 def get_repo_refs(self, repo_name, target_repo_name):
258 140 repo = Repository.get_by_repo_name(target_repo_name)
259 141 if not repo:
260 142 raise HTTPNotFound
261 143 return PullRequestModel().generate_repo_data(repo)
262 144
263 145 @LoginRequired()
264 146 @NotAnonymous()
265 147 @XHRRequired()
266 148 @HasRepoPermissionAnyDecorator('repository.read', 'repository.write',
267 149 'repository.admin')
268 150 @jsonify
269 151 def get_repo_destinations(self, repo_name):
270 152 repo = Repository.get_by_repo_name(repo_name)
271 153 if not repo:
272 154 raise HTTPNotFound
273 155 filter_query = request.GET.get('query')
274 156
275 157 query = Repository.query() \
276 158 .order_by(func.length(Repository.repo_name)) \
277 159 .filter(or_(
278 160 Repository.repo_name == repo.repo_name,
279 161 Repository.fork_id == repo.repo_id))
280 162
281 163 if filter_query:
282 164 ilike_expression = u'%{}%'.format(safe_unicode(filter_query))
283 165 query = query.filter(
284 166 Repository.repo_name.ilike(ilike_expression))
285 167
286 168 add_parent = False
287 169 if repo.parent:
288 170 if filter_query in repo.parent.repo_name:
289 171 parent_vcs_obj = repo.parent.scm_instance()
290 172 if parent_vcs_obj and not parent_vcs_obj.is_empty():
291 173 add_parent = True
292 174
293 175 limit = 20 - 1 if add_parent else 20
294 176 all_repos = query.limit(limit).all()
295 177 if add_parent:
296 178 all_repos += [repo.parent]
297 179
298 180 repos = []
299 181 for obj in self.scm_model.get_repos(all_repos):
300 182 repos.append({
301 183 'id': obj['name'],
302 184 'text': obj['name'],
303 185 'type': 'repo',
304 186 'obj': obj['dbrepo']
305 187 })
306 188
307 189 data = {
308 190 'more': False,
309 191 'results': [{
310 192 'text': _('Repositories'),
311 193 'children': repos
312 194 }] if repos else []
313 195 }
314 196 return data
315 197
316 198 @LoginRequired()
317 199 @NotAnonymous()
318 200 @HasRepoPermissionAnyDecorator('repository.read', 'repository.write',
319 201 'repository.admin')
320 202 @HasAcceptedRepoType('git', 'hg')
321 203 @auth.CSRFRequired()
322 204 def create(self, repo_name):
323 205 repo = Repository.get_by_repo_name(repo_name)
324 206 if not repo:
325 207 raise HTTPNotFound
326 208
327 209 controls = peppercorn.parse(request.POST.items())
328 210
329 211 try:
330 212 _form = PullRequestForm(repo.repo_id)().to_python(controls)
331 213 except formencode.Invalid as errors:
332 214 if errors.error_dict.get('revisions'):
333 215 msg = 'Revisions: %s' % errors.error_dict['revisions']
334 216 elif errors.error_dict.get('pullrequest_title'):
335 217 msg = _('Pull request requires a title with min. 3 chars')
336 218 else:
337 219 msg = _('Error creating pull request: {}').format(errors)
338 220 log.exception(msg)
339 221 h.flash(msg, 'error')
340 222
341 223 # would rather just go back to form ...
342 224 return redirect(url('pullrequest_home', repo_name=repo_name))
343 225
344 226 source_repo = _form['source_repo']
345 227 source_ref = _form['source_ref']
346 228 target_repo = _form['target_repo']
347 229 target_ref = _form['target_ref']
348 230 commit_ids = _form['revisions'][::-1]
349 231 reviewers = [
350 232 (r['user_id'], r['reasons']) for r in _form['review_members']]
351 233
352 234 # find the ancestor for this pr
353 235 source_db_repo = Repository.get_by_repo_name(_form['source_repo'])
354 236 target_db_repo = Repository.get_by_repo_name(_form['target_repo'])
355 237
356 238 source_scm = source_db_repo.scm_instance()
357 239 target_scm = target_db_repo.scm_instance()
358 240
359 241 source_commit = source_scm.get_commit(source_ref.split(':')[-1])
360 242 target_commit = target_scm.get_commit(target_ref.split(':')[-1])
361 243
362 244 ancestor = source_scm.get_common_ancestor(
363 245 source_commit.raw_id, target_commit.raw_id, target_scm)
364 246
365 247 target_ref_type, target_ref_name, __ = _form['target_ref'].split(':')
366 248 target_ref = ':'.join((target_ref_type, target_ref_name, ancestor))
367 249
368 250 pullrequest_title = _form['pullrequest_title']
369 251 title_source_ref = source_ref.split(':', 2)[1]
370 252 if not pullrequest_title:
371 253 pullrequest_title = PullRequestModel().generate_pullrequest_title(
372 254 source=source_repo,
373 255 source_ref=title_source_ref,
374 256 target=target_repo
375 257 )
376 258
377 259 description = _form['pullrequest_desc']
378 260 try:
379 261 pull_request = PullRequestModel().create(
380 262 c.rhodecode_user.user_id, source_repo, source_ref, target_repo,
381 263 target_ref, commit_ids, reviewers, pullrequest_title,
382 264 description
383 265 )
384 266 Session().commit()
385 267 h.flash(_('Successfully opened new pull request'),
386 268 category='success')
387 269 except Exception as e:
388 270 msg = _('Error occurred during sending pull request')
389 271 log.exception(msg)
390 272 h.flash(msg, category='error')
391 273 return redirect(url('pullrequest_home', repo_name=repo_name))
392 274
393 275 return redirect(url('pullrequest_show', repo_name=target_repo,
394 276 pull_request_id=pull_request.pull_request_id))
395 277
396 278 @LoginRequired()
397 279 @NotAnonymous()
398 280 @HasRepoPermissionAnyDecorator('repository.read', 'repository.write',
399 281 'repository.admin')
400 282 @auth.CSRFRequired()
401 283 @jsonify
402 284 def update(self, repo_name, pull_request_id):
403 285 pull_request_id = safe_int(pull_request_id)
404 286 pull_request = PullRequest.get_or_404(pull_request_id)
405 287 # only owner or admin can update it
406 288 allowed_to_update = PullRequestModel().check_user_update(
407 289 pull_request, c.rhodecode_user)
408 290 if allowed_to_update:
409 291 controls = peppercorn.parse(request.POST.items())
410 292
411 293 if 'review_members' in controls:
412 294 self._update_reviewers(
413 295 pull_request_id, controls['review_members'])
414 296 elif str2bool(request.POST.get('update_commits', 'false')):
415 297 self._update_commits(pull_request)
416 298 elif str2bool(request.POST.get('close_pull_request', 'false')):
417 299 self._reject_close(pull_request)
418 300 elif str2bool(request.POST.get('edit_pull_request', 'false')):
419 301 self._edit_pull_request(pull_request)
420 302 else:
421 303 raise HTTPBadRequest()
422 304 return True
423 305 raise HTTPForbidden()
424 306
425 307 def _edit_pull_request(self, pull_request):
426 308 try:
427 309 PullRequestModel().edit(
428 310 pull_request, request.POST.get('title'),
429 311 request.POST.get('description'))
430 312 except ValueError:
431 313 msg = _(u'Cannot update closed pull requests.')
432 314 h.flash(msg, category='error')
433 315 return
434 316 else:
435 317 Session().commit()
436 318
437 319 msg = _(u'Pull request title & description updated.')
438 320 h.flash(msg, category='success')
439 321 return
440 322
441 323 def _update_commits(self, pull_request):
442 324 resp = PullRequestModel().update_commits(pull_request)
443 325
444 326 if resp.executed:
445 327
446 328 if resp.target_changed and resp.source_changed:
447 329 changed = 'target and source repositories'
448 330 elif resp.target_changed and not resp.source_changed:
449 331 changed = 'target repository'
450 332 elif not resp.target_changed and resp.source_changed:
451 333 changed = 'source repository'
452 334 else:
453 335 changed = 'nothing'
454 336
455 337 msg = _(
456 338 u'Pull request updated to "{source_commit_id}" with '
457 339 u'{count_added} added, {count_removed} removed commits. '
458 340 u'Source of changes: {change_source}')
459 341 msg = msg.format(
460 342 source_commit_id=pull_request.source_ref_parts.commit_id,
461 343 count_added=len(resp.changes.added),
462 344 count_removed=len(resp.changes.removed),
463 345 change_source=changed)
464 346 h.flash(msg, category='success')
465 347
466 348 registry = get_current_registry()
467 349 rhodecode_plugins = getattr(registry, 'rhodecode_plugins', {})
468 350 channelstream_config = rhodecode_plugins.get('channelstream', {})
469 351 if channelstream_config.get('enabled'):
470 352 message = msg + (
471 353 ' - <a onclick="window.location.reload()">'
472 354 '<strong>{}</strong></a>'.format(_('Reload page')))
473 355 channel = '/repo${}$/pr/{}'.format(
474 356 pull_request.target_repo.repo_name,
475 357 pull_request.pull_request_id
476 358 )
477 359 payload = {
478 360 'type': 'message',
479 361 'user': 'system',
480 362 'exclude_users': [request.user.username],
481 363 'channel': channel,
482 364 'message': {
483 365 'message': message,
484 366 'level': 'success',
485 367 'topic': '/notifications'
486 368 }
487 369 }
488 370 channelstream_request(
489 371 channelstream_config, [payload], '/message',
490 372 raise_exc=False)
491 373 else:
492 374 msg = PullRequestModel.UPDATE_STATUS_MESSAGES[resp.reason]
493 375 warning_reasons = [
494 376 UpdateFailureReason.NO_CHANGE,
495 377 UpdateFailureReason.WRONG_REF_TYPE,
496 378 ]
497 379 category = 'warning' if resp.reason in warning_reasons else 'error'
498 380 h.flash(msg, category=category)
499 381
500 382 @auth.CSRFRequired()
501 383 @LoginRequired()
502 384 @NotAnonymous()
503 385 @HasRepoPermissionAnyDecorator('repository.read', 'repository.write',
504 386 'repository.admin')
505 387 def merge(self, repo_name, pull_request_id):
506 388 """
507 389 POST /{repo_name}/pull-request/{pull_request_id}
508 390
509 391 Merge will perform a server-side merge of the specified
510 392 pull request, if the pull request is approved and mergeable.
511 393 After successful merging, the pull request is automatically
512 394 closed, with a relevant comment.
513 395 """
514 396 pull_request_id = safe_int(pull_request_id)
515 397 pull_request = PullRequest.get_or_404(pull_request_id)
516 398 user = c.rhodecode_user
517 399
518 400 check = MergeCheck.validate(pull_request, user)
519 401 merge_possible = not check.failed
520 402
521 403 for err_type, error_msg in check.errors:
522 404 h.flash(error_msg, category=err_type)
523 405
524 406 if merge_possible:
525 407 log.debug("Pre-conditions checked, trying to merge.")
526 408 extras = vcs_operation_context(
527 409 request.environ, repo_name=pull_request.target_repo.repo_name,
528 410 username=user.username, action='push',
529 411 scm=pull_request.target_repo.repo_type)
530 412 self._merge_pull_request(pull_request, user, extras)
531 413
532 414 return redirect(url(
533 415 'pullrequest_show',
534 416 repo_name=pull_request.target_repo.repo_name,
535 417 pull_request_id=pull_request.pull_request_id))
536 418
537 419 def _merge_pull_request(self, pull_request, user, extras):
538 420 merge_resp = PullRequestModel().merge(
539 421 pull_request, user, extras=extras)
540 422
541 423 if merge_resp.executed:
542 424 log.debug("The merge was successful, closing the pull request.")
543 425 PullRequestModel().close_pull_request(
544 426 pull_request.pull_request_id, user)
545 427 Session().commit()
546 428 msg = _('Pull request was successfully merged and closed.')
547 429 h.flash(msg, category='success')
548 430 else:
549 431 log.debug(
550 432 "The merge was not successful. Merge response: %s",
551 433 merge_resp)
552 434 msg = PullRequestModel().merge_status_message(
553 435 merge_resp.failure_reason)
554 436 h.flash(msg, category='error')
555 437
556 438 def _update_reviewers(self, pull_request_id, review_members):
557 439 reviewers = [
558 440 (int(r['user_id']), r['reasons']) for r in review_members]
559 441 PullRequestModel().update_reviewers(pull_request_id, reviewers)
560 442 Session().commit()
561 443
562 444 def _reject_close(self, pull_request):
563 445 if pull_request.is_closed():
564 446 raise HTTPForbidden()
565 447
566 448 PullRequestModel().close_pull_request_with_comment(
567 449 pull_request, c.rhodecode_user, c.rhodecode_db_repo)
568 450 Session().commit()
569 451
570 452 @LoginRequired()
571 453 @NotAnonymous()
572 454 @HasRepoPermissionAnyDecorator('repository.read', 'repository.write',
573 455 'repository.admin')
574 456 @auth.CSRFRequired()
575 457 @jsonify
576 458 def delete(self, repo_name, pull_request_id):
577 459 pull_request_id = safe_int(pull_request_id)
578 460 pull_request = PullRequest.get_or_404(pull_request_id)
579 461
580 462 pr_closed = pull_request.is_closed()
581 463 allowed_to_delete = PullRequestModel().check_user_delete(
582 464 pull_request, c.rhodecode_user) and not pr_closed
583 465
584 466 # only owner can delete it !
585 467 if allowed_to_delete:
586 468 PullRequestModel().delete(pull_request)
587 469 Session().commit()
588 470 h.flash(_('Successfully deleted pull request'),
589 471 category='success')
590 472 return redirect(url('my_account_pullrequests'))
591 473
592 474 h.flash(_('Your are not allowed to delete this pull request'),
593 475 category='error')
594 476 raise HTTPForbidden()
595 477
596 478 def _get_pr_version(self, pull_request_id, version=None):
597 479 pull_request_id = safe_int(pull_request_id)
598 480 at_version = None
599 481
600 482 if version and version == 'latest':
601 483 pull_request_ver = PullRequest.get(pull_request_id)
602 484 pull_request_obj = pull_request_ver
603 485 _org_pull_request_obj = pull_request_obj
604 486 at_version = 'latest'
605 487 elif version:
606 488 pull_request_ver = PullRequestVersion.get_or_404(version)
607 489 pull_request_obj = pull_request_ver
608 490 _org_pull_request_obj = pull_request_ver.pull_request
609 491 at_version = pull_request_ver.pull_request_version_id
610 492 else:
611 493 _org_pull_request_obj = pull_request_obj = PullRequest.get_or_404(pull_request_id)
612 494
613 495 pull_request_display_obj = PullRequest.get_pr_display_object(
614 496 pull_request_obj, _org_pull_request_obj)
615 497
616 498 return _org_pull_request_obj, pull_request_obj, \
617 499 pull_request_display_obj, at_version
618 500
619 501 def _get_diffset(
620 502 self, source_repo, source_ref_id, target_ref_id, target_commit,
621 503 source_commit, diff_limit, file_limit, display_inline_comments):
622 504 vcs_diff = PullRequestModel().get_diff(
623 505 source_repo, source_ref_id, target_ref_id)
624 506
625 507 diff_processor = diffs.DiffProcessor(
626 508 vcs_diff, format='newdiff', diff_limit=diff_limit,
627 509 file_limit=file_limit, show_full_diff=c.fulldiff)
628 510
629 511 _parsed = diff_processor.prepare()
630 512
631 513 def _node_getter(commit):
632 514 def get_node(fname):
633 515 try:
634 516 return commit.get_node(fname)
635 517 except NodeDoesNotExistError:
636 518 return None
637 519
638 520 return get_node
639 521
640 522 diffset = codeblocks.DiffSet(
641 523 repo_name=c.repo_name,
642 524 source_repo_name=c.source_repo.repo_name,
643 525 source_node_getter=_node_getter(target_commit),
644 526 target_node_getter=_node_getter(source_commit),
645 527 comments=display_inline_comments
646 528 )
647 529 diffset = diffset.render_patchset(
648 530 _parsed, target_commit.raw_id, source_commit.raw_id)
649 531
650 532 return diffset
651 533
652 534 @LoginRequired()
653 535 @HasRepoPermissionAnyDecorator('repository.read', 'repository.write',
654 536 'repository.admin')
655 537 def show(self, repo_name, pull_request_id):
656 538 pull_request_id = safe_int(pull_request_id)
657 539 version = request.GET.get('version')
658 540 from_version = request.GET.get('from_version') or version
659 541 merge_checks = request.GET.get('merge_checks')
660 542 c.fulldiff = str2bool(request.GET.get('fulldiff'))
661 543
662 544 (pull_request_latest,
663 545 pull_request_at_ver,
664 546 pull_request_display_obj,
665 547 at_version) = self._get_pr_version(
666 548 pull_request_id, version=version)
667 549 pr_closed = pull_request_latest.is_closed()
668 550
669 551 if pr_closed and (version or from_version):
670 552 # not allow to browse versions
671 553 return redirect(h.url('pullrequest_show', repo_name=repo_name,
672 554 pull_request_id=pull_request_id))
673 555
674 556 versions = pull_request_display_obj.versions()
675 557
676 558 c.at_version = at_version
677 559 c.at_version_num = (at_version
678 560 if at_version and at_version != 'latest'
679 561 else None)
680 562 c.at_version_pos = ChangesetComment.get_index_from_version(
681 563 c.at_version_num, versions)
682 564
683 565 (prev_pull_request_latest,
684 566 prev_pull_request_at_ver,
685 567 prev_pull_request_display_obj,
686 568 prev_at_version) = self._get_pr_version(
687 569 pull_request_id, version=from_version)
688 570
689 571 c.from_version = prev_at_version
690 572 c.from_version_num = (prev_at_version
691 573 if prev_at_version and prev_at_version != 'latest'
692 574 else None)
693 575 c.from_version_pos = ChangesetComment.get_index_from_version(
694 576 c.from_version_num, versions)
695 577
696 578 # define if we're in COMPARE mode or VIEW at version mode
697 579 compare = at_version != prev_at_version
698 580
699 581 # pull_requests repo_name we opened it against
700 582 # ie. target_repo must match
701 583 if repo_name != pull_request_at_ver.target_repo.repo_name:
702 584 raise HTTPNotFound
703 585
704 586 c.shadow_clone_url = PullRequestModel().get_shadow_clone_url(
705 587 pull_request_at_ver)
706 588
707 589 c.pull_request = pull_request_display_obj
708 590 c.pull_request_latest = pull_request_latest
709 591
710 592 if compare or (at_version and not at_version == 'latest'):
711 593 c.allowed_to_change_status = False
712 594 c.allowed_to_update = False
713 595 c.allowed_to_merge = False
714 596 c.allowed_to_delete = False
715 597 c.allowed_to_comment = False
716 598 c.allowed_to_close = False
717 599 else:
718 600 c.allowed_to_change_status = PullRequestModel(). \
719 601 check_user_change_status(pull_request_at_ver, c.rhodecode_user) \
720 602 and not pr_closed
721 603
722 604 c.allowed_to_update = PullRequestModel().check_user_update(
723 605 pull_request_latest, c.rhodecode_user) and not pr_closed
724 606 c.allowed_to_merge = PullRequestModel().check_user_merge(
725 607 pull_request_latest, c.rhodecode_user) and not pr_closed
726 608 c.allowed_to_delete = PullRequestModel().check_user_delete(
727 609 pull_request_latest, c.rhodecode_user) and not pr_closed
728 610 c.allowed_to_comment = not pr_closed
729 611 c.allowed_to_close = c.allowed_to_merge and not pr_closed
730 612
731 613 # check merge capabilities
732 614 _merge_check = MergeCheck.validate(
733 615 pull_request_latest, user=c.rhodecode_user)
734 616 c.pr_merge_errors = _merge_check.error_details
735 617 c.pr_merge_possible = not _merge_check.failed
736 618 c.pr_merge_message = _merge_check.merge_msg
737 619
738 620 c.pull_request_review_status = _merge_check.review_status
739 621 if merge_checks:
740 622 return render('/pullrequests/pullrequest_merge_checks.mako')
741 623
742 624 comments_model = CommentsModel()
743 625
744 626 # reviewers and statuses
745 627 c.pull_request_reviewers = pull_request_at_ver.reviewers_statuses()
746 628 allowed_reviewers = [x[0].user_id for x in c.pull_request_reviewers]
747 629
748 630 # GENERAL COMMENTS with versions #
749 631 q = comments_model._all_general_comments_of_pull_request(pull_request_latest)
750 632 q = q.order_by(ChangesetComment.comment_id.asc())
751 633 general_comments = q
752 634
753 635 # pick comments we want to render at current version
754 636 c.comment_versions = comments_model.aggregate_comments(
755 637 general_comments, versions, c.at_version_num)
756 638 c.comments = c.comment_versions[c.at_version_num]['until']
757 639
758 640 # INLINE COMMENTS with versions #
759 641 q = comments_model._all_inline_comments_of_pull_request(pull_request_latest)
760 642 q = q.order_by(ChangesetComment.comment_id.asc())
761 643 inline_comments = q
762 644
763 645 c.inline_versions = comments_model.aggregate_comments(
764 646 inline_comments, versions, c.at_version_num, inline=True)
765 647
766 648 # inject latest version
767 649 latest_ver = PullRequest.get_pr_display_object(
768 650 pull_request_latest, pull_request_latest)
769 651
770 652 c.versions = versions + [latest_ver]
771 653
772 654 # if we use version, then do not show later comments
773 655 # than current version
774 656 display_inline_comments = collections.defaultdict(
775 657 lambda: collections.defaultdict(list))
776 658 for co in inline_comments:
777 659 if c.at_version_num:
778 660 # pick comments that are at least UPTO given version, so we
779 661 # don't render comments for higher version
780 662 should_render = co.pull_request_version_id and \
781 663 co.pull_request_version_id <= c.at_version_num
782 664 else:
783 665 # showing all, for 'latest'
784 666 should_render = True
785 667
786 668 if should_render:
787 669 display_inline_comments[co.f_path][co.line_no].append(co)
788 670
789 671 # load diff data into template context, if we use compare mode then
790 672 # diff is calculated based on changes between versions of PR
791 673
792 674 source_repo = pull_request_at_ver.source_repo
793 675 source_ref_id = pull_request_at_ver.source_ref_parts.commit_id
794 676
795 677 target_repo = pull_request_at_ver.target_repo
796 678 target_ref_id = pull_request_at_ver.target_ref_parts.commit_id
797 679
798 680 if compare:
799 681 # in compare switch the diff base to latest commit from prev version
800 682 target_ref_id = prev_pull_request_display_obj.revisions[0]
801 683
802 684 # despite opening commits for bookmarks/branches/tags, we always
803 685 # convert this to rev to prevent changes after bookmark or branch change
804 686 c.source_ref_type = 'rev'
805 687 c.source_ref = source_ref_id
806 688
807 689 c.target_ref_type = 'rev'
808 690 c.target_ref = target_ref_id
809 691
810 692 c.source_repo = source_repo
811 693 c.target_repo = target_repo
812 694
813 695 # diff_limit is the old behavior, will cut off the whole diff
814 696 # if the limit is applied otherwise will just hide the
815 697 # big files from the front-end
816 698 diff_limit = self.cut_off_limit_diff
817 699 file_limit = self.cut_off_limit_file
818 700
819 701 c.commit_ranges = []
820 702 source_commit = EmptyCommit()
821 703 target_commit = EmptyCommit()
822 704 c.missing_requirements = False
823 705
824 706 source_scm = source_repo.scm_instance()
825 707 target_scm = target_repo.scm_instance()
826 708
827 709 # try first shadow repo, fallback to regular repo
828 710 try:
829 711 commits_source_repo = pull_request_latest.get_shadow_repo()
830 712 except Exception:
831 713 log.debug('Failed to get shadow repo', exc_info=True)
832 714 commits_source_repo = source_scm
833 715
834 716 c.commits_source_repo = commits_source_repo
835 717 commit_cache = {}
836 718 try:
837 719 pre_load = ["author", "branch", "date", "message"]
838 720 show_revs = pull_request_at_ver.revisions
839 721 for rev in show_revs:
840 722 comm = commits_source_repo.get_commit(
841 723 commit_id=rev, pre_load=pre_load)
842 724 c.commit_ranges.append(comm)
843 725 commit_cache[comm.raw_id] = comm
844 726
845 727 target_commit = commits_source_repo.get_commit(
846 728 commit_id=safe_str(target_ref_id))
847 729 source_commit = commits_source_repo.get_commit(
848 730 commit_id=safe_str(source_ref_id))
849 731 except CommitDoesNotExistError:
850 732 pass
851 733 except RepositoryRequirementError:
852 734 log.warning(
853 735 'Failed to get all required data from repo', exc_info=True)
854 736 c.missing_requirements = True
855 737
856 738 c.ancestor = None # set it to None, to hide it from PR view
857 739
858 740 try:
859 741 ancestor_id = source_scm.get_common_ancestor(
860 742 source_commit.raw_id, target_commit.raw_id, target_scm)
861 743 c.ancestor_commit = source_scm.get_commit(ancestor_id)
862 744 except Exception:
863 745 c.ancestor_commit = None
864 746
865 747 c.statuses = source_repo.statuses(
866 748 [x.raw_id for x in c.commit_ranges])
867 749
868 750 # auto collapse if we have more than limit
869 751 collapse_limit = diffs.DiffProcessor._collapse_commits_over
870 752 c.collapse_all_commits = len(c.commit_ranges) > collapse_limit
871 753 c.compare_mode = compare
872 754
873 755 c.missing_commits = False
874 756 if (c.missing_requirements or isinstance(source_commit, EmptyCommit)
875 757 or source_commit == target_commit):
876 758
877 759 c.missing_commits = True
878 760 else:
879 761
880 762 c.diffset = self._get_diffset(
881 763 commits_source_repo, source_ref_id, target_ref_id,
882 764 target_commit, source_commit,
883 765 diff_limit, file_limit, display_inline_comments)
884 766
885 767 c.limited_diff = c.diffset.limited_diff
886 768
887 769 # calculate removed files that are bound to comments
888 770 comment_deleted_files = [
889 771 fname for fname in display_inline_comments
890 772 if fname not in c.diffset.file_stats]
891 773
892 774 c.deleted_files_comments = collections.defaultdict(dict)
893 775 for fname, per_line_comments in display_inline_comments.items():
894 776 if fname in comment_deleted_files:
895 777 c.deleted_files_comments[fname]['stats'] = 0
896 778 c.deleted_files_comments[fname]['comments'] = list()
897 779 for lno, comments in per_line_comments.items():
898 780 c.deleted_files_comments[fname]['comments'].extend(
899 781 comments)
900 782
901 783 # this is a hack to properly display links, when creating PR, the
902 784 # compare view and others uses different notation, and
903 785 # compare_commits.mako renders links based on the target_repo.
904 786 # We need to swap that here to generate it properly on the html side
905 787 c.target_repo = c.source_repo
906 788
907 789 c.commit_statuses = ChangesetStatus.STATUSES
908 790
909 791 c.show_version_changes = not pr_closed
910 792 if c.show_version_changes:
911 793 cur_obj = pull_request_at_ver
912 794 prev_obj = prev_pull_request_at_ver
913 795
914 796 old_commit_ids = prev_obj.revisions
915 797 new_commit_ids = cur_obj.revisions
916 798 commit_changes = PullRequestModel()._calculate_commit_id_changes(
917 799 old_commit_ids, new_commit_ids)
918 800 c.commit_changes_summary = commit_changes
919 801
920 802 # calculate the diff for commits between versions
921 803 c.commit_changes = []
922 804 mark = lambda cs, fw: list(
923 805 h.itertools.izip_longest([], cs, fillvalue=fw))
924 806 for c_type, raw_id in mark(commit_changes.added, 'a') \
925 807 + mark(commit_changes.removed, 'r') \
926 808 + mark(commit_changes.common, 'c'):
927 809
928 810 if raw_id in commit_cache:
929 811 commit = commit_cache[raw_id]
930 812 else:
931 813 try:
932 814 commit = commits_source_repo.get_commit(raw_id)
933 815 except CommitDoesNotExistError:
934 816 # in case we fail extracting still use "dummy" commit
935 817 # for display in commit diff
936 818 commit = h.AttributeDict(
937 819 {'raw_id': raw_id,
938 820 'message': 'EMPTY or MISSING COMMIT'})
939 821 c.commit_changes.append([c_type, commit])
940 822
941 823 # current user review statuses for each version
942 824 c.review_versions = {}
943 825 if c.rhodecode_user.user_id in allowed_reviewers:
944 826 for co in general_comments:
945 827 if co.author.user_id == c.rhodecode_user.user_id:
946 828 # each comment has a status change
947 829 status = co.status_change
948 830 if status:
949 831 _ver_pr = status[0].comment.pull_request_version_id
950 832 c.review_versions[_ver_pr] = status[0]
951 833
952 834 return render('/pullrequests/pullrequest_show.mako')
953 835
954 836 @LoginRequired()
955 837 @NotAnonymous()
956 838 @HasRepoPermissionAnyDecorator(
957 839 'repository.read', 'repository.write', 'repository.admin')
958 840 @auth.CSRFRequired()
959 841 @jsonify
960 842 def comment(self, repo_name, pull_request_id):
961 843 pull_request_id = safe_int(pull_request_id)
962 844 pull_request = PullRequest.get_or_404(pull_request_id)
963 845 if pull_request.is_closed():
964 846 raise HTTPForbidden()
965 847
966 848 status = request.POST.get('changeset_status', None)
967 849 text = request.POST.get('text')
968 850 comment_type = request.POST.get('comment_type')
969 851 resolves_comment_id = request.POST.get('resolves_comment_id', None)
970 852 close_pull_request = request.POST.get('close_pull_request')
971 853
972 854 close_pr = False
973 855 # only owner or admin or person with write permissions
974 856 allowed_to_close = PullRequestModel().check_user_update(
975 857 pull_request, c.rhodecode_user)
976 858
977 859 if close_pull_request and allowed_to_close:
978 860 close_pr = True
979 861 pull_request_review_status = pull_request.calculated_review_status()
980 862 if pull_request_review_status == ChangesetStatus.STATUS_APPROVED:
981 863 # approved only if we have voting consent
982 864 status = ChangesetStatus.STATUS_APPROVED
983 865 else:
984 866 status = ChangesetStatus.STATUS_REJECTED
985 867
986 868 allowed_to_change_status = PullRequestModel().check_user_change_status(
987 869 pull_request, c.rhodecode_user)
988 870
989 871 if status and allowed_to_change_status:
990 872 message = (_('Status change %(transition_icon)s %(status)s')
991 873 % {'transition_icon': '>',
992 874 'status': ChangesetStatus.get_status_lbl(status)})
993 875 if close_pr:
994 876 message = _('Closing with') + ' ' + message
995 877 text = text or message
996 878 comm = CommentsModel().create(
997 879 text=text,
998 880 repo=c.rhodecode_db_repo.repo_id,
999 881 user=c.rhodecode_user.user_id,
1000 882 pull_request=pull_request_id,
1001 883 f_path=request.POST.get('f_path'),
1002 884 line_no=request.POST.get('line'),
1003 885 status_change=(ChangesetStatus.get_status_lbl(status)
1004 886 if status and allowed_to_change_status else None),
1005 887 status_change_type=(status
1006 888 if status and allowed_to_change_status else None),
1007 889 closing_pr=close_pr,
1008 890 comment_type=comment_type,
1009 891 resolves_comment_id=resolves_comment_id
1010 892 )
1011 893
1012 894 if allowed_to_change_status:
1013 895 old_calculated_status = pull_request.calculated_review_status()
1014 896 # get status if set !
1015 897 if status:
1016 898 ChangesetStatusModel().set_status(
1017 899 c.rhodecode_db_repo.repo_id,
1018 900 status,
1019 901 c.rhodecode_user.user_id,
1020 902 comm,
1021 903 pull_request=pull_request_id
1022 904 )
1023 905
1024 906 Session().flush()
1025 907 events.trigger(events.PullRequestCommentEvent(pull_request, comm))
1026 908 # we now calculate the status of pull request, and based on that
1027 909 # calculation we set the commits status
1028 910 calculated_status = pull_request.calculated_review_status()
1029 911 if old_calculated_status != calculated_status:
1030 912 PullRequestModel()._trigger_pull_request_hook(
1031 913 pull_request, c.rhodecode_user, 'review_status_change')
1032 914
1033 915 calculated_status_lbl = ChangesetStatus.get_status_lbl(
1034 916 calculated_status)
1035 917
1036 918 if close_pr:
1037 919 status_completed = (
1038 920 calculated_status in [ChangesetStatus.STATUS_APPROVED,
1039 921 ChangesetStatus.STATUS_REJECTED])
1040 922 if close_pull_request or status_completed:
1041 923 PullRequestModel().close_pull_request(
1042 924 pull_request_id, c.rhodecode_user)
1043 925 else:
1044 926 h.flash(_('Closing pull request on other statuses than '
1045 927 'rejected or approved is forbidden. '
1046 928 'Calculated status from all reviewers '
1047 929 'is currently: %s') % calculated_status_lbl,
1048 930 category='warning')
1049 931
1050 932 Session().commit()
1051 933
1052 934 if not request.is_xhr:
1053 935 return redirect(h.url('pullrequest_show', repo_name=repo_name,
1054 936 pull_request_id=pull_request_id))
1055 937
1056 938 data = {
1057 939 'target_id': h.safeid(h.safe_unicode(request.POST.get('f_path'))),
1058 940 }
1059 941 if comm:
1060 942 c.co = comm
1061 943 c.inline_comment = True if comm.line_no else False
1062 944 data.update(comm.get_dict())
1063 945 data.update({'rendered_text':
1064 946 render('changeset/changeset_comment_block.mako')})
1065 947
1066 948 return data
1067 949
1068 950 @LoginRequired()
1069 951 @NotAnonymous()
1070 952 @HasRepoPermissionAnyDecorator('repository.read', 'repository.write',
1071 953 'repository.admin')
1072 954 @auth.CSRFRequired()
1073 955 @jsonify
1074 956 def delete_comment(self, repo_name, comment_id):
1075 957 return self._delete_comment(comment_id)
1076 958
1077 959 def _delete_comment(self, comment_id):
1078 960 comment_id = safe_int(comment_id)
1079 961 co = ChangesetComment.get_or_404(comment_id)
1080 962 if co.pull_request.is_closed():
1081 963 # don't allow deleting comments on closed pull request
1082 964 raise HTTPForbidden()
1083 965
1084 966 is_owner = co.author.user_id == c.rhodecode_user.user_id
1085 967 is_repo_admin = h.HasRepoPermissionAny('repository.admin')(c.repo_name)
1086 968 if h.HasPermissionAny('hg.admin')() or is_repo_admin or is_owner:
1087 969 old_calculated_status = co.pull_request.calculated_review_status()
1088 970 CommentsModel().delete(comment=co)
1089 971 Session().commit()
1090 972 calculated_status = co.pull_request.calculated_review_status()
1091 973 if old_calculated_status != calculated_status:
1092 974 PullRequestModel()._trigger_pull_request_hook(
1093 975 co.pull_request, c.rhodecode_user, 'review_status_change')
1094 976 return True
1095 977 else:
1096 978 raise HTTPForbidden()
@@ -1,85 +1,89 b''
1 1 // See panels-bootstrap.less
2 2 // These provide overrides for custom styling of Bootstrap panels
3 3
4 4 .panel {
5 5 &:extend(.clearfix);
6 6
7 7 width: 100%;
8 8 margin: 0 0 25px 0;
9 9 .border-radius(@border-radius);
10 10 .box-shadow(none);
11 11
12 12 .permalink {
13 13 visibility: hidden;
14 14 }
15 15
16 16 &:hover .permalink {
17 17 visibility: visible;
18 18 color: @rcblue;
19 19 }
20 20
21 21 .panel-heading {
22 22 position: relative;
23 23 min-height: 1em;
24 24 padding: @padding @panel-padding;
25 25 border-bottom: none;
26 26
27 27 .panel-title,
28 28 h3.panel-title {
29 29 float: left;
30 30 padding: 0 @padding 0 0;
31 31 line-height: 1;
32 32 font-size: @panel-title;
33 33 color: @grey1;
34 34 }
35 35
36 36 .panel-edit {
37 37 float: right;
38 38 line-height: 1;
39 39 font-size: @panel-title;
40 40 }
41 41 }
42 42
43 43 .panel-body {
44 44 padding: @panel-padding;
45
46 &.panel-body-min-height {
47 min-height: 150px
48 }
45 49 }
46 50
47 51 .panel-footer {
48 52 background-color: white;
49 53 padding: .65em @panel-padding .5em;
50 54 font-size: @panel-footer;
51 55 color: @text-muted;
52 56 }
53 57
54 58 .q_filter_box {
55 59 min-width: 40%;
56 60 }
57 61
58 62 // special cases
59 63 &.user-profile {
60 64 float: left;
61 65
62 66 .panel-heading {
63 67 margin-bottom: @padding;
64 68 }
65 69
66 70 .panel-body {
67 71 &:extend(.clearfix);
68 72 }
69 73 }
70 74 }
71 75
72 76 .main-content h3.panel-title {
73 77 font-size: @panel-title;
74 78 color: @grey1;
75 79 }
76 80
77 81 .panel-body-title-text {
78 82 margin: 0 0 20px 0;
79 83 }
80 84
81 85 // play nice with the current form and field css
82 86 .field.panel-default,
83 87 .form.panel-default {
84 88 width: auto;
85 89 } No newline at end of file
@@ -1,134 +1,135 b''
1 1
2 2 /******************************************************************************
3 3 * *
4 4 * DO NOT CHANGE THIS FILE MANUALLY *
5 5 * *
6 6 * *
7 7 * This file is automatically generated when the app starts up with *
8 8 * generate_js_files = true *
9 9 * *
10 10 * To add a route here pass jsroute=True to the route definition in the app *
11 11 * *
12 12 ******************************************************************************/
13 13 function registerRCRoutes() {
14 14 // routes registration
15 15 pyroutes.register('new_repo', '/_admin/create_repository', []);
16 16 pyroutes.register('edit_user', '/_admin/users/%(user_id)s/edit', ['user_id']);
17 17 pyroutes.register('edit_user_group_members', '/_admin/user_groups/%(user_group_id)s/edit/members', ['user_group_id']);
18 18 pyroutes.register('gists', '/_admin/gists', []);
19 19 pyroutes.register('new_gist', '/_admin/gists/new', []);
20 20 pyroutes.register('toggle_following', '/_admin/toggle_following', []);
21 21 pyroutes.register('repo_stats', '/%(repo_name)s/repo_stats/%(commit_id)s', ['repo_name', 'commit_id']);
22 22 pyroutes.register('repo_refs_data', '/%(repo_name)s/refs-data', ['repo_name']);
23 23 pyroutes.register('repo_refs_changelog_data', '/%(repo_name)s/refs-data-changelog', ['repo_name']);
24 24 pyroutes.register('repo_default_reviewers_data', '/%(repo_name)s/default-reviewers', ['repo_name']);
25 25 pyroutes.register('changeset_home', '/%(repo_name)s/changeset/%(revision)s', ['repo_name', 'revision']);
26 26 pyroutes.register('changeset_comment', '/%(repo_name)s/changeset/%(revision)s/comment', ['repo_name', 'revision']);
27 27 pyroutes.register('changeset_comment_preview', '/%(repo_name)s/changeset/comment/preview', ['repo_name']);
28 28 pyroutes.register('changeset_comment_delete', '/%(repo_name)s/changeset/comment/%(comment_id)s/delete', ['repo_name', 'comment_id']);
29 29 pyroutes.register('changeset_info', '/%(repo_name)s/changeset_info/%(revision)s', ['repo_name', 'revision']);
30 30 pyroutes.register('compare_url', '/%(repo_name)s/compare/%(source_ref_type)s@%(source_ref)s...%(target_ref_type)s@%(target_ref)s', ['repo_name', 'source_ref_type', 'source_ref', 'target_ref_type', 'target_ref']);
31 31 pyroutes.register('pullrequest_home', '/%(repo_name)s/pull-request/new', ['repo_name']);
32 32 pyroutes.register('pullrequest', '/%(repo_name)s/pull-request/new', ['repo_name']);
33 33 pyroutes.register('pullrequest_repo_refs', '/%(repo_name)s/pull-request/refs/%(target_repo_name)s', ['repo_name', 'target_repo_name']);
34 34 pyroutes.register('pullrequest_repo_destinations', '/%(repo_name)s/pull-request/repo-destinations', ['repo_name']);
35 35 pyroutes.register('pullrequest_show', '/%(repo_name)s/pull-request/%(pull_request_id)s', ['repo_name', 'pull_request_id']);
36 36 pyroutes.register('pullrequest_update', '/%(repo_name)s/pull-request/%(pull_request_id)s', ['repo_name', 'pull_request_id']);
37 pyroutes.register('pullrequest_show_all', '/%(repo_name)s/pull-request', ['repo_name']);
38 37 pyroutes.register('pullrequest_comment', '/%(repo_name)s/pull-request-comment/%(pull_request_id)s', ['repo_name', 'pull_request_id']);
39 38 pyroutes.register('pullrequest_comment_delete', '/%(repo_name)s/pull-request-comment/%(comment_id)s/delete', ['repo_name', 'comment_id']);
40 39 pyroutes.register('changelog_home', '/%(repo_name)s/changelog', ['repo_name']);
41 40 pyroutes.register('changelog_file_home', '/%(repo_name)s/changelog/%(revision)s/%(f_path)s', ['repo_name', 'revision', 'f_path']);
42 41 pyroutes.register('changelog_elements', '/%(repo_name)s/changelog_details', ['repo_name']);
43 42 pyroutes.register('files_home', '/%(repo_name)s/files/%(revision)s/%(f_path)s', ['repo_name', 'revision', 'f_path']);
44 43 pyroutes.register('files_history_home', '/%(repo_name)s/history/%(revision)s/%(f_path)s', ['repo_name', 'revision', 'f_path']);
45 44 pyroutes.register('files_authors_home', '/%(repo_name)s/authors/%(revision)s/%(f_path)s', ['repo_name', 'revision', 'f_path']);
46 45 pyroutes.register('files_annotate_home', '/%(repo_name)s/annotate/%(revision)s/%(f_path)s', ['repo_name', 'revision', 'f_path']);
47 46 pyroutes.register('files_annotate_previous', '/%(repo_name)s/annotate-previous/%(revision)s/%(f_path)s', ['repo_name', 'revision', 'f_path']);
48 47 pyroutes.register('files_archive_home', '/%(repo_name)s/archive/%(fname)s', ['repo_name', 'fname']);
49 48 pyroutes.register('files_nodelist_home', '/%(repo_name)s/nodelist/%(revision)s/%(f_path)s', ['repo_name', 'revision', 'f_path']);
50 49 pyroutes.register('files_nodetree_full', '/%(repo_name)s/nodetree_full/%(commit_id)s/%(f_path)s', ['repo_name', 'commit_id', 'f_path']);
51 50 pyroutes.register('summary_home_slash', '/%(repo_name)s/', ['repo_name']);
52 51 pyroutes.register('summary_home', '/%(repo_name)s', ['repo_name']);
53 52 pyroutes.register('favicon', '/favicon.ico', []);
54 53 pyroutes.register('robots', '/robots.txt', []);
55 54 pyroutes.register('auth_home', '/_admin/auth*traverse', []);
56 55 pyroutes.register('global_integrations_new', '/_admin/integrations/new', []);
57 56 pyroutes.register('global_integrations_home', '/_admin/integrations', []);
58 57 pyroutes.register('global_integrations_list', '/_admin/integrations/%(integration)s', ['integration']);
59 58 pyroutes.register('global_integrations_create', '/_admin/integrations/%(integration)s/new', ['integration']);
60 59 pyroutes.register('global_integrations_edit', '/_admin/integrations/%(integration)s/%(integration_id)s', ['integration', 'integration_id']);
61 60 pyroutes.register('repo_group_integrations_home', '%(repo_group_name)s/settings/integrations', ['repo_group_name']);
62 61 pyroutes.register('repo_group_integrations_list', '%(repo_group_name)s/settings/integrations/%(integration)s', ['repo_group_name', 'integration']);
63 62 pyroutes.register('repo_group_integrations_new', '%(repo_group_name)s/settings/integrations/new', ['repo_group_name']);
64 63 pyroutes.register('repo_group_integrations_create', '%(repo_group_name)s/settings/integrations/%(integration)s/new', ['repo_group_name', 'integration']);
65 64 pyroutes.register('repo_group_integrations_edit', '%(repo_group_name)s/settings/integrations/%(integration)s/%(integration_id)s', ['repo_group_name', 'integration', 'integration_id']);
66 65 pyroutes.register('repo_integrations_home', '%(repo_name)s/settings/integrations', ['repo_name']);
67 66 pyroutes.register('repo_integrations_list', '%(repo_name)s/settings/integrations/%(integration)s', ['repo_name', 'integration']);
68 67 pyroutes.register('repo_integrations_new', '%(repo_name)s/settings/integrations/new', ['repo_name']);
69 68 pyroutes.register('repo_integrations_create', '%(repo_name)s/settings/integrations/%(integration)s/new', ['repo_name', 'integration']);
70 69 pyroutes.register('repo_integrations_edit', '%(repo_name)s/settings/integrations/%(integration)s/%(integration_id)s', ['repo_name', 'integration', 'integration_id']);
71 70 pyroutes.register('ops_ping', '_admin/ops/ping', []);
72 71 pyroutes.register('admin_home', '/_admin', []);
73 72 pyroutes.register('admin_audit_logs', '_admin/audit_logs', []);
74 73 pyroutes.register('pull_requests_global_0', '_admin/pull_requests/%(pull_request_id)s', ['pull_request_id']);
75 74 pyroutes.register('pull_requests_global_1', '_admin/pull-requests/%(pull_request_id)s', ['pull_request_id']);
76 75 pyroutes.register('pull_requests_global', '_admin/pull-request/%(pull_request_id)s', ['pull_request_id']);
77 76 pyroutes.register('admin_settings_open_source', '_admin/settings/open_source', []);
78 77 pyroutes.register('admin_settings_vcs_svn_generate_cfg', '_admin/settings/vcs/svn_generate_cfg', []);
79 78 pyroutes.register('admin_settings_system', '_admin/settings/system', []);
80 79 pyroutes.register('admin_settings_system_update', '_admin/settings/system/updates', []);
81 80 pyroutes.register('admin_settings_sessions', '_admin/settings/sessions', []);
82 81 pyroutes.register('admin_settings_sessions_cleanup', '_admin/settings/sessions/cleanup', []);
83 82 pyroutes.register('users', '_admin/users', []);
84 83 pyroutes.register('users_data', '_admin/users_data', []);
85 84 pyroutes.register('edit_user_auth_tokens', '_admin/users/%(user_id)s/edit/auth_tokens', ['user_id']);
86 85 pyroutes.register('edit_user_auth_tokens_add', '_admin/users/%(user_id)s/edit/auth_tokens/new', ['user_id']);
87 86 pyroutes.register('edit_user_auth_tokens_delete', '_admin/users/%(user_id)s/edit/auth_tokens/delete', ['user_id']);
88 87 pyroutes.register('edit_user_groups_management', '_admin/users/%(user_id)s/edit/groups_management', ['user_id']);
89 88 pyroutes.register('edit_user_groups_management_updates', '_admin/users/%(user_id)s/edit/edit_user_groups_management/updates', ['user_id']);
90 89 pyroutes.register('edit_user_audit_logs', '_admin/users/%(user_id)s/edit/audit', ['user_id']);
91 90 pyroutes.register('channelstream_connect', '/_admin/channelstream/connect', []);
92 91 pyroutes.register('channelstream_subscribe', '/_admin/channelstream/subscribe', []);
93 92 pyroutes.register('channelstream_proxy', '/_channelstream', []);
94 93 pyroutes.register('login', '/_admin/login', []);
95 94 pyroutes.register('logout', '/_admin/logout', []);
96 95 pyroutes.register('register', '/_admin/register', []);
97 96 pyroutes.register('reset_password', '/_admin/password_reset', []);
98 97 pyroutes.register('reset_password_confirmation', '/_admin/password_reset_confirmation', []);
99 98 pyroutes.register('home', '/', []);
100 99 pyroutes.register('user_autocomplete_data', '/_users', []);
101 100 pyroutes.register('user_group_autocomplete_data', '/_user_groups', []);
102 101 pyroutes.register('repo_list_data', '/_repos', []);
103 102 pyroutes.register('goto_switcher_data', '/_goto_data', []);
104 103 pyroutes.register('repo_summary_explicit', '/%(repo_name)s/summary', ['repo_name']);
105 104 pyroutes.register('tags_home', '/%(repo_name)s/tags', ['repo_name']);
106 105 pyroutes.register('branches_home', '/%(repo_name)s/branches', ['repo_name']);
107 106 pyroutes.register('bookmarks_home', '/%(repo_name)s/bookmarks', ['repo_name']);
108 107 pyroutes.register('pullrequest_show', '/%(repo_name)s/pull-request/%(pull_request_id)s', ['repo_name', 'pull_request_id']);
108 pyroutes.register('pullrequest_show_all', '/%(repo_name)s/pull-request', ['repo_name']);
109 pyroutes.register('pullrequest_show_all_data', '/%(repo_name)s/pull-request-data', ['repo_name']);
109 110 pyroutes.register('edit_repo', '/%(repo_name)s/settings', ['repo_name']);
110 111 pyroutes.register('edit_repo_advanced', '/%(repo_name)s/settings/advanced', ['repo_name']);
111 112 pyroutes.register('edit_repo_advanced_delete', '/%(repo_name)s/settings/advanced/delete', ['repo_name']);
112 113 pyroutes.register('edit_repo_advanced_locking', '/%(repo_name)s/settings/advanced/locking', ['repo_name']);
113 114 pyroutes.register('edit_repo_advanced_journal', '/%(repo_name)s/settings/advanced/journal', ['repo_name']);
114 115 pyroutes.register('edit_repo_advanced_fork', '/%(repo_name)s/settings/advanced/fork', ['repo_name']);
115 116 pyroutes.register('edit_repo_caches', '/%(repo_name)s/settings/caches', ['repo_name']);
116 117 pyroutes.register('edit_repo_perms', '/%(repo_name)s/settings/permissions', ['repo_name']);
117 118 pyroutes.register('repo_reviewers', '/%(repo_name)s/settings/review/rules', ['repo_name']);
118 119 pyroutes.register('repo_maintenance', '/%(repo_name)s/settings/maintenance', ['repo_name']);
119 120 pyroutes.register('repo_maintenance_execute', '/%(repo_name)s/settings/maintenance/execute', ['repo_name']);
120 121 pyroutes.register('strip', '/%(repo_name)s/settings/strip', ['repo_name']);
121 122 pyroutes.register('strip_check', '/%(repo_name)s/settings/strip_check', ['repo_name']);
122 123 pyroutes.register('strip_execute', '/%(repo_name)s/settings/strip_execute', ['repo_name']);
123 124 pyroutes.register('search', '/_admin/search', []);
124 125 pyroutes.register('search_repo', '/%(repo_name)s/search', ['repo_name']);
125 126 pyroutes.register('user_profile', '/_profiles/%(username)s', ['username']);
126 127 pyroutes.register('my_account_profile', '/_admin/my_account/profile', []);
127 128 pyroutes.register('my_account_password', '/_admin/my_account/password', []);
128 129 pyroutes.register('my_account_password_update', '/_admin/my_account/password', []);
129 130 pyroutes.register('my_account_auth_tokens', '/_admin/my_account/auth_tokens', []);
130 131 pyroutes.register('my_account_auth_tokens_add', '/_admin/my_account/auth_tokens/new', []);
131 132 pyroutes.register('my_account_auth_tokens_delete', '/_admin/my_account/auth_tokens/delete', []);
132 133 pyroutes.register('my_account_notifications_test_channelstream', '/_admin/my_account/test_channelstream', []);
133 134 pyroutes.register('apiv2', '/_admin/api', []);
134 135 }
@@ -1,602 +1,602 b''
1 1 ## -*- coding: utf-8 -*-
2 2 <%inherit file="root.mako"/>
3 3
4 4 <div class="outerwrapper">
5 5 <!-- HEADER -->
6 6 <div class="header">
7 7 <div id="header-inner" class="wrapper">
8 8 <div id="logo">
9 9 <div class="logo-wrapper">
10 10 <a href="${h.url('home')}"><img src="${h.asset('images/rhodecode-logo-white-216x60.png')}" alt="RhodeCode"/></a>
11 11 </div>
12 12 %if c.rhodecode_name:
13 13 <div class="branding">- ${h.branding(c.rhodecode_name)}</div>
14 14 %endif
15 15 </div>
16 16 <!-- MENU BAR NAV -->
17 17 ${self.menu_bar_nav()}
18 18 <!-- END MENU BAR NAV -->
19 19 </div>
20 20 </div>
21 21 ${self.menu_bar_subnav()}
22 22 <!-- END HEADER -->
23 23
24 24 <!-- CONTENT -->
25 25 <div id="content" class="wrapper">
26 26
27 27 <rhodecode-toast id="notifications"></rhodecode-toast>
28 28
29 29 <div class="main">
30 30 ${next.main()}
31 31 </div>
32 32 </div>
33 33 <!-- END CONTENT -->
34 34
35 35 </div>
36 36 <!-- FOOTER -->
37 37 <div id="footer">
38 38 <div id="footer-inner" class="title wrapper">
39 39 <div>
40 40 <p class="footer-link-right">
41 41 % if c.visual.show_version:
42 42 RhodeCode Enterprise ${c.rhodecode_version} ${c.rhodecode_edition}
43 43 % endif
44 44 &copy; 2010-${h.datetime.today().year}, <a href="${h.route_url('rhodecode_official')}" target="_blank">RhodeCode GmbH</a>. All rights reserved.
45 45 % if c.visual.rhodecode_support_url:
46 46 <a href="${c.visual.rhodecode_support_url}" target="_blank">${_('Support')}</a>
47 47 % endif
48 48 </p>
49 49 <% sid = 'block' if request.GET.get('showrcid') else 'none' %>
50 50 <p class="server-instance" style="display:${sid}">
51 51 ## display hidden instance ID if specially defined
52 52 % if c.rhodecode_instanceid:
53 53 ${_('RhodeCode instance id: %s') % c.rhodecode_instanceid}
54 54 % endif
55 55 </p>
56 56 </div>
57 57 </div>
58 58 </div>
59 59
60 60 <!-- END FOOTER -->
61 61
62 62 ### MAKO DEFS ###
63 63
64 64 <%def name="menu_bar_subnav()">
65 65 </%def>
66 66
67 67 <%def name="breadcrumbs(class_='breadcrumbs')">
68 68 <div class="${class_}">
69 69 ${self.breadcrumbs_links()}
70 70 </div>
71 71 </%def>
72 72
73 73 <%def name="admin_menu()">
74 74 <ul class="admin_menu submenu">
75 75 <li><a href="${h.route_path('admin_audit_logs')}">${_('Admin audit logs')}</a></li>
76 76 <li><a href="${h.url('repos')}">${_('Repositories')}</a></li>
77 77 <li><a href="${h.url('repo_groups')}">${_('Repository groups')}</a></li>
78 78 <li><a href="${h.route_path('users')}">${_('Users')}</a></li>
79 79 <li><a href="${h.url('users_groups')}">${_('User groups')}</a></li>
80 80 <li><a href="${h.url('admin_permissions_application')}">${_('Permissions')}</a></li>
81 81 <li><a href="${h.route_path('auth_home', traverse='')}">${_('Authentication')}</a></li>
82 82 <li><a href="${h.route_path('global_integrations_home')}">${_('Integrations')}</a></li>
83 83 <li><a href="${h.url('admin_defaults_repositories')}">${_('Defaults')}</a></li>
84 84 <li class="last"><a href="${h.url('admin_settings')}">${_('Settings')}</a></li>
85 85 </ul>
86 86 </%def>
87 87
88 88
89 89 <%def name="dt_info_panel(elements)">
90 90 <dl class="dl-horizontal">
91 91 %for dt, dd, title, show_items in elements:
92 92 <dt>${dt}:</dt>
93 93 <dd title="${title}">
94 94 %if callable(dd):
95 95 ## allow lazy evaluation of elements
96 96 ${dd()}
97 97 %else:
98 98 ${dd}
99 99 %endif
100 100 %if show_items:
101 101 <span class="btn-collapse" data-toggle="item-${h.md5_safe(dt)[:6]}-details">${_('Show More')} </span>
102 102 %endif
103 103 </dd>
104 104
105 105 %if show_items:
106 106 <div class="collapsable-content" data-toggle="item-${h.md5_safe(dt)[:6]}-details" style="display: none">
107 107 %for item in show_items:
108 108 <dt></dt>
109 109 <dd>${item}</dd>
110 110 %endfor
111 111 </div>
112 112 %endif
113 113
114 114 %endfor
115 115 </dl>
116 116 </%def>
117 117
118 118
119 119 <%def name="gravatar(email, size=16)">
120 120 <%
121 121 if (size > 16):
122 122 gravatar_class = 'gravatar gravatar-large'
123 123 else:
124 124 gravatar_class = 'gravatar'
125 125 %>
126 126 <%doc>
127 127 TODO: johbo: For now we serve double size images to make it smooth
128 128 for retina. This is how it worked until now. Should be replaced
129 129 with a better solution at some point.
130 130 </%doc>
131 131 <img class="${gravatar_class}" src="${h.gravatar_url(email, size * 2)}" height="${size}" width="${size}">
132 132 </%def>
133 133
134 134
135 135 <%def name="gravatar_with_user(contact, size=16, show_disabled=False)">
136 136 <% email = h.email_or_none(contact) %>
137 137 <div class="rc-user tooltip" title="${h.author_string(email)}">
138 138 ${self.gravatar(email, size)}
139 139 <span class="${'user user-disabled' if show_disabled else 'user'}"> ${h.link_to_user(contact)}</span>
140 140 </div>
141 141 </%def>
142 142
143 143
144 144 ## admin menu used for people that have some admin resources
145 145 <%def name="admin_menu_simple(repositories=None, repository_groups=None, user_groups=None)">
146 146 <ul class="submenu">
147 147 %if repositories:
148 148 <li class="local-admin-repos"><a href="${h.url('repos')}">${_('Repositories')}</a></li>
149 149 %endif
150 150 %if repository_groups:
151 151 <li class="local-admin-repo-groups"><a href="${h.url('repo_groups')}">${_('Repository groups')}</a></li>
152 152 %endif
153 153 %if user_groups:
154 154 <li class="local-admin-user-groups"><a href="${h.url('users_groups')}">${_('User groups')}</a></li>
155 155 %endif
156 156 </ul>
157 157 </%def>
158 158
159 159 <%def name="repo_page_title(repo_instance)">
160 160 <div class="title-content">
161 161 <div class="title-main">
162 162 ## SVN/HG/GIT icons
163 163 %if h.is_hg(repo_instance):
164 164 <i class="icon-hg"></i>
165 165 %endif
166 166 %if h.is_git(repo_instance):
167 167 <i class="icon-git"></i>
168 168 %endif
169 169 %if h.is_svn(repo_instance):
170 170 <i class="icon-svn"></i>
171 171 %endif
172 172
173 173 ## public/private
174 174 %if repo_instance.private:
175 175 <i class="icon-repo-private"></i>
176 176 %else:
177 177 <i class="icon-repo-public"></i>
178 178 %endif
179 179
180 180 ## repo name with group name
181 181 ${h.breadcrumb_repo_link(c.rhodecode_db_repo)}
182 182
183 183 </div>
184 184
185 185 ## FORKED
186 186 %if repo_instance.fork:
187 187 <p>
188 188 <i class="icon-code-fork"></i> ${_('Fork of')}
189 189 <a href="${h.url('summary_home',repo_name=repo_instance.fork.repo_name)}">${repo_instance.fork.repo_name}</a>
190 190 </p>
191 191 %endif
192 192
193 193 ## IMPORTED FROM REMOTE
194 194 %if repo_instance.clone_uri:
195 195 <p>
196 196 <i class="icon-code-fork"></i> ${_('Clone from')}
197 197 <a href="${h.url(h.safe_str(h.hide_credentials(repo_instance.clone_uri)))}">${h.hide_credentials(repo_instance.clone_uri)}</a>
198 198 </p>
199 199 %endif
200 200
201 201 ## LOCKING STATUS
202 202 %if repo_instance.locked[0]:
203 203 <p class="locking_locked">
204 204 <i class="icon-repo-lock"></i>
205 205 ${_('Repository locked by %(user)s') % {'user': h.person_by_id(repo_instance.locked[0])}}
206 206 </p>
207 207 %elif repo_instance.enable_locking:
208 208 <p class="locking_unlocked">
209 209 <i class="icon-repo-unlock"></i>
210 210 ${_('Repository not locked. Pull repository to lock it.')}
211 211 </p>
212 212 %endif
213 213
214 214 </div>
215 215 </%def>
216 216
217 217 <%def name="repo_menu(active=None)">
218 218 <%
219 219 def is_active(selected):
220 220 if selected == active:
221 221 return "active"
222 222 %>
223 223
224 224 <!--- CONTEXT BAR -->
225 225 <div id="context-bar">
226 226 <div class="wrapper">
227 227 <ul id="context-pages" class="horizontal-list navigation">
228 228 <li class="${is_active('summary')}"><a class="menulink" href="${h.url('summary_home', repo_name=c.repo_name)}"><div class="menulabel">${_('Summary')}</div></a></li>
229 229 <li class="${is_active('changelog')}"><a class="menulink" href="${h.url('changelog_home', repo_name=c.repo_name)}"><div class="menulabel">${_('Changelog')}</div></a></li>
230 230 <li class="${is_active('files')}"><a class="menulink" href="${h.url('files_home', repo_name=c.repo_name, revision=c.rhodecode_db_repo.landing_rev[1])}"><div class="menulabel">${_('Files')}</div></a></li>
231 231 <li class="${is_active('compare')}">
232 232 <a class="menulink" href="${h.url('compare_home',repo_name=c.repo_name)}"><div class="menulabel">${_('Compare')}</div></a>
233 233 </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 <a class="menulink" href="${h.url('pullrequest_show_all',repo_name=c.repo_name)}" title="${_('Show Pull Requests for %s') % c.repo_name}">
237 <a class="menulink" href="${h.route_path('pullrequest_show_all', repo_name=c.repo_name)}" title="${_('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" href="#" class="dropdown"><div class="menulabel">${_('Options')} <div class="show_more"></div></div></a>
247 247 <ul class="submenu">
248 248 %if h.HasRepoPermissionAll('repository.admin')(c.repo_name):
249 249 <li><a href="${h.route_path('edit_repo',repo_name=c.repo_name)}">${_('Settings')}</a></li>
250 250 %endif
251 251 %if c.rhodecode_db_repo.fork:
252 252 <li><a href="${h.url('compare_url',repo_name=c.rhodecode_db_repo.fork.repo_name,source_ref_type=c.rhodecode_db_repo.landing_rev[0],source_ref=c.rhodecode_db_repo.landing_rev[1], target_repo=c.repo_name,target_ref_type='branch' if request.GET.get('branch') else c.rhodecode_db_repo.landing_rev[0],target_ref=request.GET.get('branch') or c.rhodecode_db_repo.landing_rev[1], merge=1)}">
253 253 ${_('Compare fork')}</a></li>
254 254 %endif
255 255
256 256 <li><a href="${h.route_path('search_repo',repo_name=c.repo_name)}">${_('Search')}</a></li>
257 257
258 258 %if h.HasRepoPermissionAny('repository.write','repository.admin')(c.repo_name) and c.rhodecode_db_repo.enable_locking:
259 259 %if c.rhodecode_db_repo.locked[0]:
260 260 <li><a class="locking_del" href="${h.url('toggle_locking',repo_name=c.repo_name)}">${_('Unlock')}</a></li>
261 261 %else:
262 262 <li><a class="locking_add" href="${h.url('toggle_locking',repo_name=c.repo_name)}">${_('Lock')}</a></li>
263 263 %endif
264 264 %endif
265 265 %if c.rhodecode_user.username != h.DEFAULT_USER:
266 266 %if c.rhodecode_db_repo.repo_type in ['git','hg']:
267 267 <li><a href="${h.url('repo_fork_home',repo_name=c.repo_name)}">${_('Fork')}</a></li>
268 268 <li><a href="${h.url('pullrequest_home',repo_name=c.repo_name)}">${_('Create Pull Request')}</a></li>
269 269 %endif
270 270 %endif
271 271 </ul>
272 272 </li>
273 273 </ul>
274 274 </div>
275 275 <div class="clear"></div>
276 276 </div>
277 277 <!--- END CONTEXT BAR -->
278 278
279 279 </%def>
280 280
281 281 <%def name="usermenu(active=False)">
282 282 ## USER MENU
283 283 <li id="quick_login_li" class="${'active' if active else ''}">
284 284 <a id="quick_login_link" class="menulink childs">
285 285 ${gravatar(c.rhodecode_user.email, 20)}
286 286 <span class="user">
287 287 %if c.rhodecode_user.username != h.DEFAULT_USER:
288 288 <span class="menu_link_user">${c.rhodecode_user.username}</span><div class="show_more"></div>
289 289 %else:
290 290 <span>${_('Sign in')}</span>
291 291 %endif
292 292 </span>
293 293 </a>
294 294
295 295 <div class="user-menu submenu">
296 296 <div id="quick_login">
297 297 %if c.rhodecode_user.username == h.DEFAULT_USER:
298 298 <h4>${_('Sign in to your account')}</h4>
299 299 ${h.form(h.route_path('login', _query={'came_from': h.url.current()}), needs_csrf_token=False)}
300 300 <div class="form form-vertical">
301 301 <div class="fields">
302 302 <div class="field">
303 303 <div class="label">
304 304 <label for="username">${_('Username')}:</label>
305 305 </div>
306 306 <div class="input">
307 307 ${h.text('username',class_='focus',tabindex=1)}
308 308 </div>
309 309
310 310 </div>
311 311 <div class="field">
312 312 <div class="label">
313 313 <label for="password">${_('Password')}:</label>
314 314 %if h.HasPermissionAny('hg.password_reset.enabled')():
315 315 <span class="forgot_password">${h.link_to(_('(Forgot password?)'),h.route_path('reset_password'), class_='pwd_reset')}</span>
316 316 %endif
317 317 </div>
318 318 <div class="input">
319 319 ${h.password('password',class_='focus',tabindex=2)}
320 320 </div>
321 321 </div>
322 322 <div class="buttons">
323 323 <div class="register">
324 324 %if h.HasPermissionAny('hg.admin', 'hg.register.auto_activate', 'hg.register.manual_activate')():
325 325 ${h.link_to(_("Don't have an account?"),h.route_path('register'))} <br/>
326 326 %endif
327 327 ${h.link_to(_("Using external auth? Sign In here."),h.route_path('login'))}
328 328 </div>
329 329 <div class="submit">
330 330 ${h.submit('sign_in',_('Sign In'),class_="btn btn-small",tabindex=3)}
331 331 </div>
332 332 </div>
333 333 </div>
334 334 </div>
335 335 ${h.end_form()}
336 336 %else:
337 337 <div class="">
338 338 <div class="big_gravatar">${gravatar(c.rhodecode_user.email, 48)}</div>
339 339 <div class="full_name">${c.rhodecode_user.full_name_or_username}</div>
340 340 <div class="email">${c.rhodecode_user.email}</div>
341 341 </div>
342 342 <div class="">
343 343 <ol class="links">
344 344 <li>${h.link_to(_(u'My account'),h.route_path('my_account_profile'))}</li>
345 345 % if c.rhodecode_user.personal_repo_group:
346 346 <li>${h.link_to(_(u'My personal group'), h.url('repo_group_home', group_name=c.rhodecode_user.personal_repo_group.group_name))}</li>
347 347 % endif
348 348 <li class="logout">
349 349 ${h.secure_form(h.route_path('logout'))}
350 350 ${h.submit('log_out', _(u'Sign Out'),class_="btn btn-primary")}
351 351 ${h.end_form()}
352 352 </li>
353 353 </ol>
354 354 </div>
355 355 %endif
356 356 </div>
357 357 </div>
358 358 %if c.rhodecode_user.username != h.DEFAULT_USER:
359 359 <div class="pill_container">
360 360 % if c.unread_notifications == 0:
361 361 <a class="menu_link_notifications empty" href="${h.url('notifications')}">${c.unread_notifications}</a>
362 362 % else:
363 363 <a class="menu_link_notifications" href="${h.url('notifications')}">${c.unread_notifications}</a>
364 364 % endif
365 365 </div>
366 366 % endif
367 367 </li>
368 368 </%def>
369 369
370 370 <%def name="menu_items(active=None)">
371 371 <%
372 372 def is_active(selected):
373 373 if selected == active:
374 374 return "active"
375 375 return ""
376 376 %>
377 377 <ul id="quick" class="main_nav navigation horizontal-list">
378 378 <!-- repo switcher -->
379 379 <li class="${is_active('repositories')} repo_switcher_li has_select2">
380 380 <input id="repo_switcher" name="repo_switcher" type="hidden">
381 381 </li>
382 382
383 383 ## ROOT MENU
384 384 %if c.rhodecode_user.username != h.DEFAULT_USER:
385 385 <li class="${is_active('journal')}">
386 386 <a class="menulink" title="${_('Show activity journal')}" href="${h.url('journal')}">
387 387 <div class="menulabel">${_('Journal')}</div>
388 388 </a>
389 389 </li>
390 390 %else:
391 391 <li class="${is_active('journal')}">
392 392 <a class="menulink" title="${_('Show Public activity journal')}" href="${h.url('public_journal')}">
393 393 <div class="menulabel">${_('Public journal')}</div>
394 394 </a>
395 395 </li>
396 396 %endif
397 397 <li class="${is_active('gists')}">
398 398 <a class="menulink childs" title="${_('Show Gists')}" href="${h.url('gists')}">
399 399 <div class="menulabel">${_('Gists')}</div>
400 400 </a>
401 401 </li>
402 402 <li class="${is_active('search')}">
403 403 <a class="menulink" title="${_('Search in repositories you have access to')}" href="${h.route_path('search')}">
404 404 <div class="menulabel">${_('Search')}</div>
405 405 </a>
406 406 </li>
407 407 % if h.HasPermissionAll('hg.admin')('access admin main page'):
408 408 <li class="${is_active('admin')}">
409 409 <a class="menulink childs" title="${_('Admin settings')}" href="#" onclick="return false;">
410 410 <div class="menulabel">${_('Admin')} <div class="show_more"></div></div>
411 411 </a>
412 412 ${admin_menu()}
413 413 </li>
414 414 % elif c.rhodecode_user.repositories_admin or c.rhodecode_user.repository_groups_admin or c.rhodecode_user.user_groups_admin:
415 415 <li class="${is_active('admin')}">
416 416 <a class="menulink childs" title="${_('Delegated Admin settings')}">
417 417 <div class="menulabel">${_('Admin')} <div class="show_more"></div></div>
418 418 </a>
419 419 ${admin_menu_simple(c.rhodecode_user.repositories_admin,
420 420 c.rhodecode_user.repository_groups_admin,
421 421 c.rhodecode_user.user_groups_admin or h.HasPermissionAny('hg.usergroup.create.true')())}
422 422 </li>
423 423 % endif
424 424 % if c.debug_style:
425 425 <li class="${is_active('debug_style')}">
426 426 <a class="menulink" title="${_('Style')}" href="${h.url('debug_style_home')}">
427 427 <div class="menulabel">${_('Style')}</div>
428 428 </a>
429 429 </li>
430 430 % endif
431 431 ## render extra user menu
432 432 ${usermenu(active=(active=='my_account'))}
433 433 </ul>
434 434
435 435 <script type="text/javascript">
436 436 var visual_show_public_icon = "${c.visual.show_public_icon}" == "True";
437 437
438 438 /*format the look of items in the list*/
439 439 var format = function(state, escapeMarkup){
440 440 if (!state.id){
441 441 return state.text; // optgroup
442 442 }
443 443 var obj_dict = state.obj;
444 444 var tmpl = '';
445 445
446 446 if(obj_dict && state.type == 'repo'){
447 447 if(obj_dict['repo_type'] === 'hg'){
448 448 tmpl += '<i class="icon-hg"></i> ';
449 449 }
450 450 else if(obj_dict['repo_type'] === 'git'){
451 451 tmpl += '<i class="icon-git"></i> ';
452 452 }
453 453 else if(obj_dict['repo_type'] === 'svn'){
454 454 tmpl += '<i class="icon-svn"></i> ';
455 455 }
456 456 if(obj_dict['private']){
457 457 tmpl += '<i class="icon-lock" ></i> ';
458 458 }
459 459 else if(visual_show_public_icon){
460 460 tmpl += '<i class="icon-unlock-alt"></i> ';
461 461 }
462 462 }
463 463 if(obj_dict && state.type == 'commit') {
464 464 tmpl += '<i class="icon-tag"></i>';
465 465 }
466 466 if(obj_dict && state.type == 'group'){
467 467 tmpl += '<i class="icon-folder-close"></i> ';
468 468 }
469 469 tmpl += escapeMarkup(state.text);
470 470 return tmpl;
471 471 };
472 472
473 473 var formatResult = function(result, container, query, escapeMarkup) {
474 474 return format(result, escapeMarkup);
475 475 };
476 476
477 477 var formatSelection = function(data, container, escapeMarkup) {
478 478 return format(data, escapeMarkup);
479 479 };
480 480
481 481 $("#repo_switcher").select2({
482 482 cachedDataSource: {},
483 483 minimumInputLength: 2,
484 484 placeholder: '<div class="menulabel">${_('Go to')} <div class="show_more"></div></div>',
485 485 dropdownAutoWidth: true,
486 486 formatResult: formatResult,
487 487 formatSelection: formatSelection,
488 488 containerCssClass: "repo-switcher",
489 489 dropdownCssClass: "repo-switcher-dropdown",
490 490 escapeMarkup: function(m){
491 491 // don't escape our custom placeholder
492 492 if(m.substr(0,23) == '<div class="menulabel">'){
493 493 return m;
494 494 }
495 495
496 496 return Select2.util.escapeMarkup(m);
497 497 },
498 498 query: $.debounce(250, function(query){
499 499 self = this;
500 500 var cacheKey = query.term;
501 501 var cachedData = self.cachedDataSource[cacheKey];
502 502
503 503 if (cachedData) {
504 504 query.callback({results: cachedData.results});
505 505 } else {
506 506 $.ajax({
507 507 url: pyroutes.url('goto_switcher_data'),
508 508 data: {'query': query.term},
509 509 dataType: 'json',
510 510 type: 'GET',
511 511 success: function(data) {
512 512 self.cachedDataSource[cacheKey] = data;
513 513 query.callback({results: data.results});
514 514 },
515 515 error: function(data, textStatus, errorThrown) {
516 516 alert("Error while fetching entries.\nError code {0} ({1}).".format(data.status, data.statusText));
517 517 }
518 518 })
519 519 }
520 520 })
521 521 });
522 522
523 523 $("#repo_switcher").on('select2-selecting', function(e){
524 524 e.preventDefault();
525 525 window.location = e.choice.url;
526 526 });
527 527
528 528 </script>
529 529 <script src="${h.asset('js/rhodecode/base/keyboard-bindings.js', ver=c.rhodecode_version_hash)}"></script>
530 530 </%def>
531 531
532 532 <div class="modal" id="help_kb" tabindex="-1" role="dialog" aria-labelledby="myModalLabel" aria-hidden="true">
533 533 <div class="modal-dialog">
534 534 <div class="modal-content">
535 535 <div class="modal-header">
536 536 <button type="button" class="close" data-dismiss="modal" aria-hidden="true">&times;</button>
537 537 <h4 class="modal-title" id="myModalLabel">${_('Keyboard shortcuts')}</h4>
538 538 </div>
539 539 <div class="modal-body">
540 540 <div class="block-left">
541 541 <table class="keyboard-mappings">
542 542 <tbody>
543 543 <tr>
544 544 <th></th>
545 545 <th>${_('Site-wide shortcuts')}</th>
546 546 </tr>
547 547 <%
548 548 elems = [
549 549 ('/', 'Open quick search box'),
550 550 ('g h', 'Goto home page'),
551 551 ('g g', 'Goto my private gists page'),
552 552 ('g G', 'Goto my public gists page'),
553 553 ('n r', 'New repository page'),
554 554 ('n g', 'New gist page'),
555 555 ]
556 556 %>
557 557 %for key, desc in elems:
558 558 <tr>
559 559 <td class="keys">
560 560 <span class="key tag">${key}</span>
561 561 </td>
562 562 <td>${desc}</td>
563 563 </tr>
564 564 %endfor
565 565 </tbody>
566 566 </table>
567 567 </div>
568 568 <div class="block-left">
569 569 <table class="keyboard-mappings">
570 570 <tbody>
571 571 <tr>
572 572 <th></th>
573 573 <th>${_('Repositories')}</th>
574 574 </tr>
575 575 <%
576 576 elems = [
577 577 ('g s', 'Goto summary page'),
578 578 ('g c', 'Goto changelog page'),
579 579 ('g f', 'Goto files page'),
580 580 ('g F', 'Goto files page with file search activated'),
581 581 ('g p', 'Goto pull requests page'),
582 582 ('g o', 'Goto repository settings'),
583 583 ('g O', 'Goto repository permissions settings'),
584 584 ]
585 585 %>
586 586 %for key, desc in elems:
587 587 <tr>
588 588 <td class="keys">
589 589 <span class="key tag">${key}</span>
590 590 </td>
591 591 <td>${desc}</td>
592 592 </tr>
593 593 %endfor
594 594 </tbody>
595 595 </table>
596 596 </div>
597 597 </div>
598 598 <div class="modal-footer">
599 599 </div>
600 600 </div><!-- /.modal-content -->
601 601 </div><!-- /.modal-dialog -->
602 602 </div><!-- /.modal -->
@@ -1,132 +1,146 b''
1 1 <%inherit file="/base/base.mako"/>
2 2
3 3 <%def name="title()">
4 4 ${_('%s Pull Requests') % c.repo_name}
5 5 %if c.rhodecode_name:
6 6 &middot; ${h.branding(c.rhodecode_name)}
7 7 %endif
8 8 </%def>
9 9
10 10 <%def name="breadcrumbs_links()">
11 11
12 12 </%def>
13 13
14 14 <%def name="menu_bar_nav()">
15 15 ${self.menu_items(active='repositories')}
16 16 </%def>
17 17
18 18
19 19 <%def name="menu_bar_subnav()">
20 20 ${self.repo_menu(active='showpullrequest')}
21 21 </%def>
22 22
23 23
24 24 <%def name="main()">
25 25 <div class="box">
26 26 <div class="title">
27 27 ${self.repo_page_title(c.rhodecode_db_repo)}
28 28
29 29 <ul class="links">
30 30 <li>
31 31 %if c.rhodecode_user.username != h.DEFAULT_USER:
32 32 <span>
33 33 <a id="open_new_pull_request" class="btn btn-small btn-success" href="${h.url('pullrequest_home',repo_name=c.repo_name)}">
34 34 ${_('Open new Pull Request')}
35 35 </a>
36 36 </span>
37 37 %endif
38 38 </li>
39 39 </ul>
40 40
41 41 ${self.breadcrumbs()}
42 42 </div>
43 43
44 44 <div class="sidebar-col-wrapper">
45 45 ##main
46 46 <div class="sidebar">
47 47 <ul class="nav nav-pills nav-stacked">
48 <li class="${'active' if c.active=='open' else ''}"><a href="${h.url('pullrequest_show_all',repo_name=c.repo_name,source=0)}">${_('Opened')}</a></li>
49 <li class="${'active' if c.active=='my' else ''}"><a href="${h.url('pullrequest_show_all',repo_name=c.repo_name,source=0,my=1)}">${_('Opened by me')}</a></li>
50 <li class="${'active' if c.active=='awaiting' else ''}"><a href="${h.url('pullrequest_show_all',repo_name=c.repo_name,source=0,awaiting_review=1)}">${_('Awaiting review')}</a></li>
51 <li class="${'active' if c.active=='awaiting_my' else ''}"><a href="${h.url('pullrequest_show_all',repo_name=c.repo_name,source=0,awaiting_my_review=1)}">${_('Awaiting my review')}</a></li>
52 <li class="${'active' if c.active=='closed' else ''}"><a href="${h.url('pullrequest_show_all',repo_name=c.repo_name,source=0,closed=1)}">${_('Closed')}</a></li>
53 <li class="${'active' if c.active=='source' else ''}"><a href="${h.url('pullrequest_show_all',repo_name=c.repo_name,source=1)}">${_('From this repo')}</a></li>
48 <li class="${'active' if c.active=='open' else ''}"><a href="${h.route_path('pullrequest_show_all',repo_name=c.repo_name, _query={'source':0})}">${_('Opened')}</a></li>
49 <li class="${'active' if c.active=='my' else ''}"><a href="${h.route_path('pullrequest_show_all',repo_name=c.repo_name, _query={'source':0,'my':1})}">${_('Opened by me')}</a></li>
50 <li class="${'active' if c.active=='awaiting' else ''}"><a href="${h.route_path('pullrequest_show_all',repo_name=c.repo_name, _query={'source':0,'awaiting_review':1})}">${_('Awaiting review')}</a></li>
51 <li class="${'active' if c.active=='awaiting_my' else ''}"><a href="${h.route_path('pullrequest_show_all',repo_name=c.repo_name, _query={'source':0,'awaiting_my_review':1})}">${_('Awaiting my review')}</a></li>
52 <li class="${'active' if c.active=='closed' else ''}"><a href="${h.route_path('pullrequest_show_all',repo_name=c.repo_name, _query={'source':0,'closed':1})}">${_('Closed')}</a></li>
53 <li class="${'active' if c.active=='source' else ''}"><a href="${h.route_path('pullrequest_show_all',repo_name=c.repo_name, _query={'source':1})}">${_('From this repo')}</a></li>
54 54 </ul>
55 55 </div>
56 56
57 57 <div class="main-content-full-width">
58 58 <div class="panel panel-default">
59 59 <div class="panel-heading">
60 60 <h3 class="panel-title">
61 61 %if c.source:
62 62 ${_('Pull Requests from %(repo_name)s repository') % {'repo_name': c.repo_name}}
63 63 %elif c.closed:
64 64 ${_('Closed Pull Requests to repository %(repo_name)s') % {'repo_name': c.repo_name}}
65 65 %elif c.my:
66 66 ${_('Pull Requests to %(repo_name)s repository opened by me') % {'repo_name': c.repo_name}}
67 67 %elif c.awaiting_review:
68 68 ${_('Pull Requests to %(repo_name)s repository awaiting review') % {'repo_name': c.repo_name}}
69 69 %elif c.awaiting_my_review:
70 70 ${_('Pull Requests to %(repo_name)s repository awaiting my review') % {'repo_name': c.repo_name}}
71 71 %else:
72 72 ${_('Pull Requests to %(repo_name)s repository') % {'repo_name': c.repo_name}}
73 73 %endif
74 74 </h3>
75 75 </div>
76 <div class="panel-body">
76 <div class="panel-body panel-body-min-height">
77 77 <table id="pull_request_list_table" class="display"></table>
78 78 </div>
79 79 </div>
80 80 </div>
81 81 </div>
82 82 </div>
83 83
84 84 <script type="text/javascript">
85 85 $(document).ready(function() {
86
87 var $pullRequestListTable = $('#pull_request_list_table');
88
86 89 // object list
87 $('#pull_request_list_table').DataTable({
88 data: ${c.data|n},
89 processing: false,
90 $pullRequestListTable.DataTable({
91 processing: true,
90 92 serverSide: true,
91 deferLoading: ${c.records_total},
92 ajax: "",
93 dom: 'tp',
93 ajax: {
94 "url": "${h.route_path('pullrequest_show_all_data', repo_name=c.repo_name)}",
95 "data": function (d) {
96 d.source = "${c.source}";
97 d.closed = "${c.closed}";
98 d.my = "${c.my}";
99 d.awaiting_review = "${c.awaiting_review}";
100 d.awaiting_my_review = "${c.awaiting_my_review}";
101 }
102 },
103 dom: 'rtp',
94 104 pageLength: ${c.visual.dashboard_items},
95 105 order: [[ 1, "desc" ]],
96 106 columns: [
97 107 { data: {"_": "status",
98 108 "sort": "status"}, title: "", className: "td-status", orderable: false},
99 109 { data: {"_": "name",
100 110 "sort": "name_raw"}, title: "${_('Name')}", className: "td-componentname", "type": "num" },
101 111 { data: {"_": "author",
102 112 "sort": "author_raw"}, title: "${_('Author')}", className: "td-user", orderable: false },
103 113 { data: {"_": "title",
104 114 "sort": "title"}, title: "${_('Title')}", className: "td-description" },
105 115 { data: {"_": "comments",
106 116 "sort": "comments_raw"}, title: "", className: "td-comments", orderable: false},
107 117 { data: {"_": "updated_on",
108 118 "sort": "updated_on_raw"}, title: "${_('Last Update')}", className: "td-time" }
109 119 ],
110 120 language: {
111 121 paginate: DEFAULT_GRID_PAGINATION,
122 sProcessing: _gettext('loading...'),
112 123 emptyTable: _gettext("No pull requests available yet.")
113 124 },
114 125 "drawCallback": function( settings, json ) {
115 126 timeagoActivate();
116 127 },
117 128 "createdRow": function ( row, data, index ) {
118 129 if (data['closed']) {
119 130 $(row).addClass('closed');
120 131 }
121 132 }
122 133 });
123 });
124 $('#pull_request_list_table').on('xhr.dt', function(e, settings, json, xhr){
125 $('#pull_request_list_table').css('opacity', 1);
134
135 $pullRequestListTable.on('xhr.dt', function(e, settings, json, xhr){
136 $pullRequestListTable.css('opacity', 1);
126 137 });
127 138
128 $('#pull_request_list_table').on('preXhr.dt', function(e, settings, data){
129 $('#pull_request_list_table').css('opacity', 0.3);
139 $pullRequestListTable.on('preXhr.dt', function(e, settings, data){
140 $pullRequestListTable.css('opacity', 0.3);
130 141 });
142
143 });
144
131 145 </script>
132 146 </%def>
General Comments 0
You need to be logged in to leave comments. Login now